@podosoft/podokit 1.1.6 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add.d.ts +3 -1
- package/dist/add.js +116 -14
- package/dist/deploy-render.js +5 -0
- package/dist/index.js +8 -0
- package/dist/lib.d.ts +2 -2
- package/dist/lib.js +3 -2
- package/dist/lockfile.d.ts +3 -1
- package/dist/lockfile.js +17 -6
- package/dist/templates/fullstack/apps/api/package.json +2 -2
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +81 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +53 -5
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +10 -1
- package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +30 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +19 -3
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +149 -4
- package/dist/templates/fullstack/apps/api/src/database/database.ts +66 -13
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +86 -0
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +4 -9
- package/dist/templates/fullstack/apps/api/src/health/readiness.service.spec.ts +141 -1
- package/dist/templates/fullstack/apps/api/src/health/readiness.service.ts +86 -12
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +188 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +30 -0
- package/dist/templates/fullstack/apps/web/src/routes/api/[...path]/+server.ts +3 -3
- package/dist/templates/fullstack/apps/web/src/routes/api/auth/[...all]/+server.ts +3 -3
- package/dist/templates/fullstack/compose.dev.yaml +21 -0
- package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +8 -1
- package/dist/templates/fullstack/dot-env.docker +3 -0
- package/dist/templates/fullstack/dot-env.example +14 -0
- package/dist/templates/fullstack/infra/k3s/api-deployment.yaml +2 -0
- package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.spec.ts +25 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.spec.ts +122 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +187 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +160 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +22 -28
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +3 -5
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +6 -17
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.spec.ts +167 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.ts +50 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.spec.ts +158 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +78 -28
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +19 -11
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +6 -32
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.test.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.ts +73 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +102 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +141 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.test.ts +68 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.ts +108 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +14 -8
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/session-idle-timeout.svelte +74 -49
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.test.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +23 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.test.ts +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.test.ts +98 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.ts +54 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +7 -50
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/audit/+page.svelte +88 -45
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +76 -75
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.server.ts +15 -14
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +79 -37
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +10 -17
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.ts +10 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +4 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +14 -8
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-sessions.api.spec.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +75 -12
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +35 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +7 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +72 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +36 -2
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -13
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +16 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +45 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +64 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +8 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +32 -1
- package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/README.md +46 -5
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-events.ts +19 -7
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +126 -3
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +13 -11
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.spec.ts +115 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.ts +44 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +421 -43
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +382 -35
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +57 -0
- package/dist/templates/modules/audit-log/module.manifest.json +7 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +20 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +24 -29
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +28 -13
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.spec.ts +56 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.ts +22 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +81 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -9
- package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.spec.ts +38 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +24 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +31 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.spec.ts +58 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +46 -57
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +21 -37
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.spec.ts +23 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.spec.ts +81 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +37 -34
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
- package/dist/templates/modules/auth/module.manifest.json +2 -2
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +136 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +34 -13
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.spec.ts +60 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.ts +51 -0
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +70 -9
- package/dist/templates/modules/file-upload/module.manifest.json +9 -3
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +26 -32
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +16 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +31 -4
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +145 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.ts +37 -0
- package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +94 -4
- package/dist/templates/modules/job-progress/module.manifest.json +3 -3
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.spec.ts +54 -0
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.ts +47 -25
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.spec.ts +86 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +159 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-demo.ts +38 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +172 -4
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +110 -33
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +88 -0
- package/dist/templates/modules/object-storage-local/module.manifest.json +6 -1
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +80 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +13 -7
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +156 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.ts +38 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +39 -21
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +101 -2
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +84 -9
- package/dist/templates/modules/object-storage-s3/module.manifest.json +8 -3
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +9 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +7 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +139 -21
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +80 -11
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +12 -6
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +15 -2
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +46 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +4 -3
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +125 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +27 -13
- package/dist/templates/modules/sse/files/apps/api/src/events/event-envelope.ts +123 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +191 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-topics.ts +64 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +250 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +121 -15
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +407 -18
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +153 -40
- package/dist/templates/modules/sse/files/apps/web/src/lib/events.test.ts +216 -0
- package/dist/templates/modules/sse/files/apps/web/src/lib/events.ts +207 -0
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +227 -70
- package/dist/templates/modules/sse/module.manifest.json +20 -5
- package/dist/update.d.ts +17 -0
- package/dist/update.js +33 -8
- package/package.json +2 -2
|
@@ -1,87 +1,244 @@
|
|
|
1
|
-
import { expect, test } from "@playwright/test";
|
|
1
|
+
import { expect, test, type APIRequestContext, type PlaywrightWorkerArgs } from "@playwright/test";
|
|
2
2
|
|
|
3
3
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
4
4
|
const secondaryApi = process.env.E2E_SECONDARY_API_URL;
|
|
5
5
|
const origin = { origin: base };
|
|
6
|
+
// The administrator seeded by the admin-dashboard Playwright setup. Inlined so this
|
|
7
|
+
// spec also compiles in apps that do not install that module.
|
|
8
|
+
const admin = {
|
|
9
|
+
email: "admin@example.com",
|
|
10
|
+
password: process.env.E2E_ADMIN_PASSWORD || "Podokit3e-Str0ng!pw",
|
|
11
|
+
};
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
type Playwright = PlaywrightWorkerArgs["playwright"];
|
|
14
|
+
|
|
15
|
+
interface SessionUser {
|
|
16
|
+
id: string;
|
|
17
|
+
roles: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function newContext(playwright: Playwright): Promise<APIRequestContext> {
|
|
21
|
+
return playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function sessionUser(ctx: APIRequestContext): Promise<SessionUser | undefined> {
|
|
25
|
+
const response = await ctx.get("/api/auth/get-session");
|
|
26
|
+
if (!response.ok()) return undefined;
|
|
27
|
+
const session = await response.json() as { user?: { id?: string; role?: string | null } } | null;
|
|
28
|
+
if (!session?.user?.id) return undefined;
|
|
29
|
+
const roles = (session.user.role ?? "").split(",").map((role) => role.trim()).filter(Boolean);
|
|
30
|
+
return { id: session.user.id, roles };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolves undefined when the auth module is missing or sign-up does not start a session. */
|
|
34
|
+
async function signUp(ctx: APIRequestContext): Promise<SessionUser | undefined> {
|
|
9
35
|
const email = `sse-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
|
|
10
|
-
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "E" } })
|
|
11
|
-
return ctx;
|
|
36
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "E" } });
|
|
37
|
+
return sessionUser(ctx);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolves false when no administrator session is available. */
|
|
41
|
+
async function signInAdmin(ctx: APIRequestContext): Promise<boolean> {
|
|
42
|
+
const response = await ctx.post("/api/auth/sign-in/email", { data: admin });
|
|
43
|
+
return response.ok() && ((await sessionUser(ctx))?.roles ?? []).includes("admin");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function cookieHeader(ctx: APIRequestContext): Promise<string> {
|
|
47
|
+
return (await ctx.storageState()).cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function uniqueTopic(): string {
|
|
51
|
+
return `spec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
12
52
|
}
|
|
13
53
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
54
|
+
class EventStreamReader {
|
|
55
|
+
private readonly decoder = new TextDecoder();
|
|
56
|
+
private buffered = "";
|
|
57
|
+
|
|
58
|
+
private constructor(
|
|
59
|
+
private readonly reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
60
|
+
private readonly controller: AbortController,
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
/** Opens a stream and waits for its retry hint, which proves the subscription is live. */
|
|
64
|
+
static async open(ctx: APIRequestContext, topics: string): Promise<EventStreamReader> {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const response = await fetch(`${base}/api/events/stream?topics=${encodeURIComponent(topics)}`, {
|
|
67
|
+
headers: { ...origin, cookie: await cookieHeader(ctx) },
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
expect(response.status).toBe(200);
|
|
71
|
+
expect(response.headers.get("content-type")).toContain("text/event-stream");
|
|
72
|
+
const reader = response.body?.getReader();
|
|
73
|
+
if (!reader) throw new Error("event stream has no body");
|
|
74
|
+
const stream = new EventStreamReader(reader, controller);
|
|
75
|
+
await stream.until("retry:");
|
|
76
|
+
return stream;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Returns everything received up to and including `text`. */
|
|
80
|
+
async until(text: string, timeoutMs = 8_000): Promise<string> {
|
|
81
|
+
const deadline = Date.now() + timeoutMs;
|
|
82
|
+
while (!this.buffered.includes(text)) {
|
|
83
|
+
const remaining = deadline - Date.now();
|
|
84
|
+
if (remaining <= 0) throw new Error(`timed out waiting for ${text}`);
|
|
85
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
86
|
+
const timeout = new Promise<"timeout">((resolve) => {
|
|
87
|
+
timer = setTimeout(() => resolve("timeout"), remaining);
|
|
88
|
+
});
|
|
89
|
+
const result = await Promise.race([this.reader.read(), timeout]);
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (result === "timeout") continue;
|
|
92
|
+
if (result.done) throw new Error(`stream ended before ${text}`);
|
|
93
|
+
this.buffered += this.decoder.decode(result.value, { stream: true });
|
|
94
|
+
}
|
|
95
|
+
const end = this.buffered.indexOf(text) + text.length;
|
|
96
|
+
const seen = this.buffered.slice(0, end);
|
|
97
|
+
this.buffered = this.buffered.slice(end);
|
|
98
|
+
return seen;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
close(): void {
|
|
102
|
+
this.controller.abort();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
test("sse: streams require a session and a valid topic list", async ({ playwright }) => {
|
|
107
|
+
const anonymous = await newContext(playwright);
|
|
108
|
+
const member = await newContext(playwright);
|
|
109
|
+
try {
|
|
110
|
+
test.skip(!(await signUp(member)), "requires the auth module");
|
|
111
|
+
const unauthenticated = await fetch(`${base}/api/events/stream?topics=message`, { headers: origin });
|
|
112
|
+
expect(unauthenticated.status).toBe(401);
|
|
113
|
+
const invalid = await member.get("/api/events/stream?topics=bad%20topic");
|
|
114
|
+
expect(invalid.status()).toBe(400);
|
|
115
|
+
expect((await invalid.json()).error.code).toBe("EVENTS_TOPICS_INVALID");
|
|
116
|
+
} finally {
|
|
117
|
+
await anonymous.dispose();
|
|
118
|
+
await member.dispose();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function openStream(cookie: string, controller: AbortController): Promise<Response> {
|
|
123
|
+
return fetch(`${base}/api/events/stream?topics=${uniqueTopic()}`, {
|
|
124
|
+
headers: { ...origin, cookie },
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
test("sse: a user's open streams are capped per replica", async ({ playwright }) => {
|
|
130
|
+
const member = await newContext(playwright);
|
|
131
|
+
const other = await newContext(playwright);
|
|
132
|
+
const controllers: AbortController[] = [];
|
|
133
|
+
try {
|
|
134
|
+
test.skip(!(await signUp(member)), "requires the auth module");
|
|
135
|
+
expect(await signUp(other)).toBeDefined();
|
|
136
|
+
const cookie = await cookieHeader(member);
|
|
137
|
+
// The limit is SSE_MAX_STREAMS_PER_USER on the server, so open until refused.
|
|
138
|
+
let refused: Response | undefined;
|
|
139
|
+
for (let attempt = 0; attempt < 64 && !refused; attempt += 1) {
|
|
140
|
+
const controller = new AbortController();
|
|
141
|
+
const response = await openStream(cookie, controller);
|
|
142
|
+
if (response.status === 200) controllers.push(controller);
|
|
143
|
+
else refused = response;
|
|
144
|
+
}
|
|
145
|
+
expect(controllers.length).toBeGreaterThan(0);
|
|
146
|
+
expect(refused?.status).toBe(429);
|
|
147
|
+
expect((await refused?.json()).error.code).toBe("EVENTS_STREAM_LIMIT");
|
|
148
|
+
|
|
149
|
+
// Another user is unaffected, and closing a stream frees its slot.
|
|
150
|
+
const otherController = new AbortController();
|
|
151
|
+
controllers.push(otherController);
|
|
152
|
+
expect((await openStream(await cookieHeader(other), otherController)).status).toBe(200);
|
|
153
|
+
controllers.shift()?.abort();
|
|
154
|
+
await expect.poll(async () => {
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const response = await openStream(cookie, controller);
|
|
157
|
+
if (response.status === 200) controllers.push(controller);
|
|
158
|
+
else controller.abort();
|
|
159
|
+
return response.status;
|
|
160
|
+
}, { timeout: 8_000 }).toBe(200);
|
|
161
|
+
} finally {
|
|
162
|
+
for (const controller of controllers) controller.abort();
|
|
163
|
+
await member.dispose();
|
|
164
|
+
await other.dispose();
|
|
165
|
+
}
|
|
20
166
|
});
|
|
21
167
|
|
|
22
|
-
test("sse:
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
168
|
+
test("sse: only administrators may publish", async ({ playwright }) => {
|
|
169
|
+
const member = await newContext(playwright);
|
|
170
|
+
try {
|
|
171
|
+
test.skip(!(await signUp(member)), "requires the auth module");
|
|
172
|
+
const published = await member.post("/api/events", { data: { message: "hello" } });
|
|
173
|
+
expect(published.status()).toBe(403);
|
|
174
|
+
expect((await published.json()).error.code).toBe("ADMIN_REQUIRED");
|
|
175
|
+
} finally {
|
|
176
|
+
await member.dispose();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("sse: an admin publish reaches only the addressed user @smoke", async ({ playwright }) => {
|
|
181
|
+
const adminCtx = await newContext(playwright);
|
|
182
|
+
const aliceCtx = await newContext(playwright);
|
|
183
|
+
const bobCtx = await newContext(playwright);
|
|
184
|
+
const streams: EventStreamReader[] = [];
|
|
185
|
+
try {
|
|
186
|
+
test.skip(!(await signInAdmin(adminCtx)), "requires the auth module and the seeded administrator");
|
|
187
|
+
const alice = await signUp(aliceCtx);
|
|
188
|
+
expect(alice).toBeDefined();
|
|
189
|
+
expect(await signUp(bobCtx)).toBeDefined();
|
|
190
|
+
const topic = uniqueTopic();
|
|
191
|
+
const aliceStream = await EventStreamReader.open(aliceCtx, topic);
|
|
192
|
+
const bobStream = await EventStreamReader.open(bobCtx, topic);
|
|
193
|
+
streams.push(aliceStream, bobStream);
|
|
194
|
+
|
|
195
|
+
const secret = `secret-${Date.now()}`;
|
|
196
|
+
const done = `done-${Date.now()}`;
|
|
197
|
+
const targeted = await adminCtx.post("/api/events", {
|
|
198
|
+
data: { topic, audience: { userIds: [alice?.id] }, message: secret },
|
|
199
|
+
});
|
|
200
|
+
expect(targeted.status()).toBe(201);
|
|
201
|
+
expect(await targeted.json()).toEqual({ ok: true });
|
|
202
|
+
expect((await adminCtx.post("/api/events", { data: { topic, message: done } })).status()).toBe(201);
|
|
203
|
+
|
|
204
|
+
const aliceSaw = await aliceStream.until(done);
|
|
205
|
+
expect(aliceSaw).toContain(JSON.stringify({ topic, data: { message: secret } }));
|
|
206
|
+
expect(await bobStream.until(done)).not.toContain(secret);
|
|
207
|
+
} finally {
|
|
208
|
+
for (const stream of streams) stream.close();
|
|
209
|
+
await adminCtx.dispose();
|
|
210
|
+
await aliceCtx.dispose();
|
|
211
|
+
await bobCtx.dispose();
|
|
43
212
|
}
|
|
44
|
-
ac.abort();
|
|
45
|
-
expect(seen).toBe(true);
|
|
46
|
-
await ctx.dispose();
|
|
47
213
|
});
|
|
48
214
|
|
|
49
215
|
test("sse: a second API replica delivers through Redis", async ({ playwright }) => {
|
|
50
216
|
test.skip(!secondaryApi, "a secondary API replica is not configured");
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const deadline = Date.now() + 8000;
|
|
78
|
-
while (Date.now() < deadline && seen === 0) {
|
|
79
|
-
const { value, done } = await reader.read();
|
|
80
|
-
if (done) break;
|
|
81
|
-
buffered += decoder.decode(value, { stream: true });
|
|
82
|
-
seen = buffered.split(msg).length - 1;
|
|
217
|
+
const adminCtx = await newContext(playwright);
|
|
218
|
+
const memberCtx = await newContext(playwright);
|
|
219
|
+
let stream: EventStreamReader | undefined;
|
|
220
|
+
try {
|
|
221
|
+
test.skip(!(await signInAdmin(adminCtx)), "requires the auth module and the seeded administrator");
|
|
222
|
+
const member = await signUp(memberCtx);
|
|
223
|
+
expect(member).toBeDefined();
|
|
224
|
+
const topic = uniqueTopic();
|
|
225
|
+
stream = await EventStreamReader.open(memberCtx, topic);
|
|
226
|
+
|
|
227
|
+
const message = `replica-${Date.now()}`;
|
|
228
|
+
const published = await fetch(`${secondaryApi}/events`, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: { ...origin, cookie: await cookieHeader(adminCtx), "content-type": "application/json" },
|
|
231
|
+
body: JSON.stringify({ topic, audience: { userIds: [member?.id] }, message }),
|
|
232
|
+
});
|
|
233
|
+
expect(published.status).toBe(201);
|
|
234
|
+
|
|
235
|
+
const done = `done-${Date.now()}`;
|
|
236
|
+
expect((await adminCtx.post("/api/events", { data: { topic, message: done } })).status()).toBe(201);
|
|
237
|
+
const seen = await stream.until(done);
|
|
238
|
+
expect(seen.split(message).length - 1).toBe(1);
|
|
239
|
+
} finally {
|
|
240
|
+
stream?.close();
|
|
241
|
+
await adminCtx.dispose();
|
|
242
|
+
await memberCtx.dispose();
|
|
83
243
|
}
|
|
84
|
-
ac.abort();
|
|
85
|
-
expect(seen).toBe(1);
|
|
86
|
-
await ctx.dispose();
|
|
87
244
|
});
|
|
@@ -6,19 +6,34 @@
|
|
|
6
6
|
"requiredProjectFiles": [
|
|
7
7
|
"apps/api/src/health/readiness.service.ts"
|
|
8
8
|
],
|
|
9
|
+
"requiredProjectContents": {
|
|
10
|
+
"apps/api/src/core/services.ts": [
|
|
11
|
+
"isAdmin(request: Request): Promise<boolean>;",
|
|
12
|
+
"roles(request: Request): Promise<string[]>;",
|
|
13
|
+
"activeOrganizationId(request: Request): Promise<string | undefined>;",
|
|
14
|
+
"tryResolve<T>(key: ServiceKey<T>): T | undefined {"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
9
17
|
"targetApp": "api",
|
|
10
18
|
"env": [
|
|
11
19
|
"# Maximum encoded Server-Sent Event payload",
|
|
12
|
-
"SSE_MAX_EVENT_BYTES=65536"
|
|
20
|
+
"SSE_MAX_EVENT_BYTES=65536",
|
|
21
|
+
"# Open event streams per user on each API replica",
|
|
22
|
+
"SSE_MAX_STREAMS_PER_USER=8"
|
|
13
23
|
],
|
|
14
24
|
"inject": [
|
|
15
25
|
{ "file": "apps/api/src/app.ts", "marker": "// podokit:end:imports", "text": "import { eventsModule } from \"./events/events.module\";" },
|
|
16
26
|
{ "file": "apps/api/src/app.ts", "marker": "// podokit:end:modules", "text": "eventsModule," }
|
|
17
27
|
],
|
|
18
28
|
"instructions": [
|
|
19
|
-
"Stream
|
|
20
|
-
"Publish
|
|
21
|
-
"
|
|
22
|
-
"
|
|
29
|
+
"Stream as a signed-in user: curl -N -b cookies.txt 'localhost:5002/events/stream?topics=message'",
|
|
30
|
+
"Publish as an administrator (in another terminal): curl -b admin-cookies.txt -XPOST localhost:5002/events -H 'content-type: application/json' -d '{\"message\":\"hello\"}'",
|
|
31
|
+
"Each data event is a default SSE message whose data is {\"topic\",\"data\"}. A resync event means events were dropped, so reload state.",
|
|
32
|
+
"Publish from a module with EventsService.publish({ topic, audience, data }). An audience of userIds, roles, or organizationId limits who receives it.",
|
|
33
|
+
"Use publishAsync for confirmed delivery, publish for best-effort delivery, or publishLocal for events already fanned out to every replica.",
|
|
34
|
+
"Restrict who may subscribe to a topic with events.topics.register(topic, authorizer).",
|
|
35
|
+
"Streams take the subscriber's roles and active organization from the request identity. Register EVENT_IDENTITY in app.extensions.ts to derive them differently.",
|
|
36
|
+
"In the browser, subscribe(topic, handler) from $lib/events shares one EventSource per tab.",
|
|
37
|
+
"Select events=redis for multiple replicas or events=memory for a single process."
|
|
23
38
|
]
|
|
24
39
|
}
|
package/dist/update.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type TemplateVars } from "@podosoft/podokit-template-engine";
|
|
2
2
|
import { type Tier } from "./lockfile";
|
|
3
|
+
import type { ProviderCapability } from "@podosoft/podokit-runtime";
|
|
3
4
|
import type { Toolchain } from "./toolchain";
|
|
4
5
|
/**
|
|
5
6
|
* `podo update` planner. For now it produces a dry-run plan only: it assembles
|
|
@@ -17,11 +18,23 @@ export interface FileChange {
|
|
|
17
18
|
/** Why the action was chosen (for the report). */
|
|
18
19
|
note: string;
|
|
19
20
|
}
|
|
21
|
+
/** A module the target version needs that the project does not record yet. */
|
|
22
|
+
export interface AddedModule {
|
|
23
|
+
name: string;
|
|
24
|
+
/** Installed or added module that needs it. */
|
|
25
|
+
requiredBy: string;
|
|
26
|
+
/** Provider capability it implements, when added for a capability requirement. */
|
|
27
|
+
capability?: ProviderCapability;
|
|
28
|
+
/** Selected provider it implements. */
|
|
29
|
+
provider?: string;
|
|
30
|
+
}
|
|
20
31
|
export interface UpdatePlan {
|
|
21
32
|
fromVersion: string;
|
|
22
33
|
toVersion: string;
|
|
23
34
|
template: string;
|
|
24
35
|
modules: string[];
|
|
36
|
+
/** Modules the update adds, ordered ahead of the modules that need them. */
|
|
37
|
+
addedModules: AddedModule[];
|
|
25
38
|
changes: FileChange[];
|
|
26
39
|
}
|
|
27
40
|
/**
|
|
@@ -51,6 +64,8 @@ export interface ApplyOptions {
|
|
|
51
64
|
abortOnConflict?: boolean;
|
|
52
65
|
}
|
|
53
66
|
export interface ApplyResult {
|
|
67
|
+
/** Modules added because the target version requires them. */
|
|
68
|
+
addedModules: string[];
|
|
54
69
|
written: string[];
|
|
55
70
|
/** Files relocated by module update migrations. */
|
|
56
71
|
moved: {
|
|
@@ -71,5 +86,7 @@ export interface ApplyResult {
|
|
|
71
86
|
* stamps the new version.
|
|
72
87
|
*/
|
|
73
88
|
export declare function applyUpdate(projectRoot: string, templatesDir: string, options?: ApplyOptions): ApplyResult;
|
|
89
|
+
/** Why an update adds a module, for the plan report. */
|
|
90
|
+
export declare function addedModuleReason(module: AddedModule): string;
|
|
74
91
|
/** Count actions for a one-line summary. */
|
|
75
92
|
export declare function summarize(plan: UpdatePlan): Record<Action, number>;
|
package/dist/update.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.planUpdate = planUpdate;
|
|
4
4
|
exports.applyUpdate = applyUpdate;
|
|
5
|
+
exports.addedModuleReason = addedModuleReason;
|
|
5
6
|
exports.summarize = summarize;
|
|
6
7
|
const node_child_process_1 = require("node:child_process");
|
|
7
8
|
const node_fs_1 = require("node:fs");
|
|
@@ -25,17 +26,21 @@ function diskContent(projectRoot, path) {
|
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* Expand the recorded module list against the target manifests. A module may
|
|
28
|
-
* gain a new requirement after it was first added to a project
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* gain a new requirement after it was first added to a project, either another
|
|
30
|
+
* module or a provider capability. Updates must apply that requirement before
|
|
31
|
+
* the dependent module: modules configure in this order, and the target tree
|
|
32
|
+
* would otherwise wire files that were never installed. A capability resolves
|
|
33
|
+
* to the module implementing the project's selected provider, as `podo add`
|
|
34
|
+
* would choose it.
|
|
31
35
|
*/
|
|
32
|
-
function targetModules(projectRoot, templatesDir, modules) {
|
|
36
|
+
function targetModules(projectRoot, templatesDir, modules, providers) {
|
|
33
37
|
const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
|
|
34
38
|
const recorded = new Map(modules.map((module) => [module.name, module]));
|
|
35
39
|
const ordered = [];
|
|
40
|
+
const added = [];
|
|
36
41
|
const visiting = new Set();
|
|
37
42
|
const visited = new Set();
|
|
38
|
-
const visit = (name, requiredBy) => {
|
|
43
|
+
const visit = (name, requiredBy, capability) => {
|
|
39
44
|
if (visited.has(name))
|
|
40
45
|
return;
|
|
41
46
|
if (visiting.has(name)) {
|
|
@@ -52,12 +57,24 @@ function targetModules(projectRoot, templatesDir, modules) {
|
|
|
52
57
|
const moduleManifest = (0, add_1.readModuleManifest)(resolved.dir);
|
|
53
58
|
for (const required of moduleManifest.requires ?? [])
|
|
54
59
|
visit(required, name);
|
|
60
|
+
for (const required of moduleManifest.requiresCapabilities ?? []) {
|
|
61
|
+
visit((0, add_1.selectedProviderModule)(modulesDir, projectRoot, providers, required), name, required);
|
|
62
|
+
}
|
|
55
63
|
visiting.delete(name);
|
|
56
64
|
visited.add(name);
|
|
65
|
+
if (!existing && requiredBy) {
|
|
66
|
+
added.push({
|
|
67
|
+
name: resolved.name,
|
|
68
|
+
requiredBy,
|
|
69
|
+
...(capability ? { capability, provider: providers[capability] } : {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
57
72
|
ordered.push({
|
|
58
73
|
...(existing ?? {
|
|
59
74
|
name: resolved.name,
|
|
60
75
|
addedWith: (0, lockfile_1.podokitVersion)(),
|
|
76
|
+
// A freshly added module ships its current layout, as `podo add` records it.
|
|
77
|
+
appliedMigrations: (moduleManifest.migrations ?? []).map((migration) => migration.id),
|
|
61
78
|
}),
|
|
62
79
|
order: ordered.length,
|
|
63
80
|
...(resolved.packageName
|
|
@@ -71,7 +88,7 @@ function targetModules(projectRoot, templatesDir, modules) {
|
|
|
71
88
|
for (const module of [...modules].sort((a, b) => a.order - b.order)) {
|
|
72
89
|
visit(module.name);
|
|
73
90
|
}
|
|
74
|
-
return ordered;
|
|
91
|
+
return { modules: ordered, added };
|
|
75
92
|
}
|
|
76
93
|
function targetManagedOverrides(projectRoot, templatesDir, modules, current = []) {
|
|
77
94
|
const overrides = new Set(current);
|
|
@@ -102,7 +119,7 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
102
119
|
const lock = (0, lockfile_1.readFilesLock)(projectRoot);
|
|
103
120
|
if (!manifest || !lock)
|
|
104
121
|
throw new inspect_1.NotAProjectError();
|
|
105
|
-
const modules = targetModules(projectRoot, templatesDir, manifest.modules);
|
|
122
|
+
const { modules, added } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
|
|
106
123
|
const newTree = (0, assemble_1.assembleProject)({
|
|
107
124
|
templatesDir,
|
|
108
125
|
template: manifest.template,
|
|
@@ -210,6 +227,7 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
210
227
|
toVersion: (0, lockfile_1.podokitVersion)(),
|
|
211
228
|
template: manifest.template,
|
|
212
229
|
modules: modules.map((module) => module.name),
|
|
230
|
+
addedModules: added,
|
|
213
231
|
changes,
|
|
214
232
|
};
|
|
215
233
|
}
|
|
@@ -333,7 +351,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
333
351
|
forceManagedPaths: options.forceManagedPaths,
|
|
334
352
|
onlyPaths: options.onlyPaths,
|
|
335
353
|
});
|
|
336
|
-
const modules = targetModules(projectRoot, templatesDir, manifest.modules);
|
|
354
|
+
const { modules } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
|
|
337
355
|
const newTree = (0, assemble_1.assembleProject)({
|
|
338
356
|
templatesDir,
|
|
339
357
|
template: manifest.template,
|
|
@@ -396,6 +414,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
396
414
|
}
|
|
397
415
|
}
|
|
398
416
|
const result = {
|
|
417
|
+
addedModules: plan.addedModules.map((module) => module.name),
|
|
399
418
|
written: [],
|
|
400
419
|
moved: (0, module_migrations_1.applyModuleMigrations)(projectRoot, migrationPlan),
|
|
401
420
|
removed: [],
|
|
@@ -469,6 +488,12 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
469
488
|
});
|
|
470
489
|
return result;
|
|
471
490
|
}
|
|
491
|
+
/** Why an update adds a module, for the plan report. */
|
|
492
|
+
function addedModuleReason(module) {
|
|
493
|
+
return module.capability
|
|
494
|
+
? `${module.capability} provider "${module.provider}" required by ${module.requiredBy}`
|
|
495
|
+
: `required by ${module.requiredBy}`;
|
|
496
|
+
}
|
|
472
497
|
/** Count actions for a one-line summary. */
|
|
473
498
|
function summarize(plan) {
|
|
474
499
|
const counts = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podosoft/podokit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "An opinionated Bun and Elysia full-stack TypeScript starter toolkit and CLI with SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"test": "vitest run"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@podosoft/podokit-runtime": "^0.
|
|
48
|
+
"@podosoft/podokit-runtime": "^0.3.0",
|
|
49
49
|
"@podosoft/podokit-template-engine": "^0.4.2"
|
|
50
50
|
}
|
|
51
51
|
}
|