@podosoft/podokit 1.4.2 → 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 -17
- 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 +28 -20
- package/dist/deploy-schema.d.ts +61 -0
- package/dist/deploy-schema.js +157 -1
- 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 +77 -5
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +12 -4
- 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 +44 -7
- 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.extensions.ts +7 -4
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +47 -28
- 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 +46 -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 +8 -2
- package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +17 -6
- package/dist/templates/fullstack/apps/api/src/config/server-options.ts +5 -8
- 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/client-address.spec.ts +82 -0
- package/dist/templates/fullstack/apps/api/src/core/client-address.ts +68 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +43 -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/Dockerfile +4 -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 +370 -36
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +250 -41
- package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +5 -2
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +23 -0
- 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 +7 -2
- package/dist/templates/fullstack/dot-env.example +21 -1
- package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
- package/dist/templates/fullstack/infra/k3s/configmap.yaml +4 -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/api/src/account-sessions/account-sessions.routes.spec.ts +152 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.ts +112 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +18 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +36 -13
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +13 -1
- 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/account-sessions.test.ts +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.ts +34 -0
- 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 +73 -39
- 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/table-toolbar.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/account-sessions.api.spec.ts +55 -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 +29 -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 +55 -0
- 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/seed.setup.ts +3 -10
- 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-sessions.ui.spec.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +62 -59
- 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 +33 -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-hook.ts +2 -1
- 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 +33 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +20 -1
- 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 +6 -1
- 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.service.spec.ts +54 -6
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -8
- package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.spec.ts +56 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.ts +45 -0
- 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/apps/api/src/rate-limit/rate-limit.config.spec.ts +2 -59
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +0 -65
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +82 -21
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +54 -15
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +43 -5
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +10 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +61 -3
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +48 -15
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +63 -27
- package/dist/templates/modules/rate-limit/module.manifest.json +6 -9
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
- package/dist/templates/todo/AGENTS.md +46 -6
- 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
package/dist/update.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type TemplateVars } from "@podosoft/podokit-template-engine";
|
|
2
|
-
import { type Tier } from "./lockfile";
|
|
1
|
+
import { type TemplateVars, type VfsTree } from "@podosoft/podokit-template-engine";
|
|
2
|
+
import { type Tier, type FilesLock, type ManifestModule, type PodokitManifest } from "./lockfile";
|
|
3
3
|
import type { ProviderCapability } from "@podosoft/podokit-runtime";
|
|
4
4
|
import type { Toolchain } from "./toolchain";
|
|
5
5
|
import { type ApiPathResult } from "./module-api-paths";
|
|
@@ -9,7 +9,7 @@ import { type ApiPathResult } from "./module-api-paths";
|
|
|
9
9
|
* working copy, classifying each file. Applying the plan (writing files, 3-way
|
|
10
10
|
* merging) is a later step (see ADR-0009).
|
|
11
11
|
*/
|
|
12
|
-
export type Action = "update" | "merge" | "conflict" | "add" | "move" | "remove" | "skip" | "up-to-date";
|
|
12
|
+
export type Action = "review" | "update" | "merge" | "conflict" | "add" | "move" | "remove" | "skip" | "up-to-date";
|
|
13
13
|
export interface FileChange {
|
|
14
14
|
path: string;
|
|
15
15
|
/** Previous path when this change is an update migration move. */
|
|
@@ -36,10 +36,38 @@ export interface UpdatePlan {
|
|
|
36
36
|
modules: string[];
|
|
37
37
|
/** Modules the update adds, ordered ahead of the modules that need them. */
|
|
38
38
|
addedModules: AddedModule[];
|
|
39
|
+
/**
|
|
40
|
+
* External modules whose installed package is older than the version the
|
|
41
|
+
* project last applied. Applying would write the older module's files, so
|
|
42
|
+
* `applyUpdate` refuses while this is not empty.
|
|
43
|
+
*/
|
|
44
|
+
moduleDowngrades: ModuleDowngrade[];
|
|
39
45
|
changes: FileChange[];
|
|
40
46
|
/** Direct API paths that modules newly declare, per configuration file. */
|
|
41
47
|
apiPaths: ApiPathResult;
|
|
48
|
+
/**
|
|
49
|
+
* Owned files that differ from this version's template while the lock has no
|
|
50
|
+
* record of the template they came from, so PodoKit cannot tell whether the
|
|
51
|
+
* template changed. Applying the update records it for the next one.
|
|
52
|
+
*/
|
|
53
|
+
ownedWithoutTemplate: string[];
|
|
42
54
|
}
|
|
55
|
+
/** An external module installed at an older version than the project last applied. */
|
|
56
|
+
export interface ModuleDowngrade {
|
|
57
|
+
name: string;
|
|
58
|
+
packageName: string;
|
|
59
|
+
/** Version recorded in the manifest. */
|
|
60
|
+
recorded: string;
|
|
61
|
+
/** Version installed in node_modules. */
|
|
62
|
+
installed: string;
|
|
63
|
+
}
|
|
64
|
+
/** Semantic version order of two exact versions: negative when `a` is older. */
|
|
65
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
66
|
+
/** Owned paths the template and the installed modules declare, without the app's own. */
|
|
67
|
+
export declare function declaredOwnedGlobs(projectRoot: string, templatesDir: string, modules: {
|
|
68
|
+
name: string;
|
|
69
|
+
packageName?: string;
|
|
70
|
+
}[]): string[];
|
|
43
71
|
/**
|
|
44
72
|
* Build the update plan. `templatesDir` is the installed CLI's template set (the
|
|
45
73
|
* new version); the lock records what PodoKit last wrote (to detect user edits).
|
|
@@ -52,6 +80,20 @@ export interface PlanOptions {
|
|
|
52
80
|
/** Restrict planning to selected paths, used by focused conversions. */
|
|
53
81
|
onlyPaths?: string[];
|
|
54
82
|
}
|
|
83
|
+
/** The project as the installed CLI's templates would generate it. */
|
|
84
|
+
export interface TargetProject {
|
|
85
|
+
manifest: PodokitManifest;
|
|
86
|
+
lock: FilesLock;
|
|
87
|
+
/** Recorded modules plus any the target version requires, in configure order. */
|
|
88
|
+
modules: ManifestModule[];
|
|
89
|
+
addedModules: AddedModule[];
|
|
90
|
+
/** External modules installed at an older version than the project applied. */
|
|
91
|
+
moduleDowngrades: ModuleDowngrade[];
|
|
92
|
+
tree: VfsTree;
|
|
93
|
+
ownedGlobs: string[];
|
|
94
|
+
managedOverrides: string[];
|
|
95
|
+
}
|
|
96
|
+
export declare function targetProject(projectRoot: string, templatesDir: string, targetAnswers?: TemplateVars): TargetProject;
|
|
55
97
|
export declare function planUpdate(projectRoot: string, templatesDir: string, options?: PlanOptions): UpdatePlan;
|
|
56
98
|
export interface ApplyOptions {
|
|
57
99
|
/** Templates for the version the project is currently on, for 3-way merges. */
|
|
@@ -80,9 +122,23 @@ export interface ApplyResult {
|
|
|
80
122
|
merged: string[];
|
|
81
123
|
/** Files written with conflict markers for manual resolution. */
|
|
82
124
|
conflicts: string[];
|
|
125
|
+
/** Owned files whose template changed; they are left as they are. */
|
|
126
|
+
review: string[];
|
|
83
127
|
/** Direct API paths added to `.podokit/dev.json` and deployment profiles. */
|
|
84
128
|
apiPaths: ApiPathResult;
|
|
85
129
|
}
|
|
130
|
+
export interface UpstreamChange {
|
|
131
|
+
path: string;
|
|
132
|
+
/** Unified diff from the previous template to the new one; empty when the
|
|
133
|
+
* previous templates did not generate the file. */
|
|
134
|
+
diff: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* What the template changed between the project's version and this one for
|
|
138
|
+
* each owned file the plan marks for review, given the previous version's
|
|
139
|
+
* templates.
|
|
140
|
+
*/
|
|
141
|
+
export declare function upstreamChanges(projectRoot: string, templatesDir: string, plan: UpdatePlan, oldTemplatesDir: string, oldExternalModulesRoot?: string): UpstreamChange[];
|
|
86
142
|
/**
|
|
87
143
|
* Apply an update to the working copy. Clean updates and additions are written,
|
|
88
144
|
* upstream removals deleted, and user-edited files 3-way merged against the old
|
|
@@ -91,6 +147,8 @@ export interface ApplyResult {
|
|
|
91
147
|
* stamps the new version.
|
|
92
148
|
*/
|
|
93
149
|
export declare function applyUpdate(projectRoot: string, templatesDir: string, options?: ApplyOptions): ApplyResult;
|
|
150
|
+
/** One line per external module installed at an older version than recorded. */
|
|
151
|
+
export declare function describeDowngrades(downgrades: ModuleDowngrade[]): string;
|
|
94
152
|
/** Why an update adds a module, for the plan report. */
|
|
95
153
|
export declare function addedModuleReason(module: AddedModule): string;
|
|
96
154
|
/** Count actions for a one-line summary. */
|
package/dist/update.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compareVersions = compareVersions;
|
|
4
|
+
exports.declaredOwnedGlobs = declaredOwnedGlobs;
|
|
5
|
+
exports.targetProject = targetProject;
|
|
3
6
|
exports.planUpdate = planUpdate;
|
|
7
|
+
exports.upstreamChanges = upstreamChanges;
|
|
4
8
|
exports.applyUpdate = applyUpdate;
|
|
9
|
+
exports.describeDowngrades = describeDowngrades;
|
|
5
10
|
exports.addedModuleReason = addedModuleReason;
|
|
6
11
|
exports.summarize = summarize;
|
|
7
12
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -10,6 +15,7 @@ const node_os_1 = require("node:os");
|
|
|
10
15
|
const node_path_1 = require("node:path");
|
|
11
16
|
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
12
17
|
const assemble_1 = require("./assemble");
|
|
18
|
+
const text_diff_1 = require("./text-diff");
|
|
13
19
|
const lockfile_1 = require("./lockfile");
|
|
14
20
|
const inspect_1 = require("./inspect");
|
|
15
21
|
const add_1 = require("./add");
|
|
@@ -25,6 +31,31 @@ function diskContent(projectRoot, path) {
|
|
|
25
31
|
const abs = (0, node_path_1.join)(projectRoot, path);
|
|
26
32
|
return (0, node_fs_1.existsSync)(abs) ? (0, node_fs_1.readFileSync)(abs) : null;
|
|
27
33
|
}
|
|
34
|
+
function versionParts(version) {
|
|
35
|
+
const [withoutBuild = ""] = version.split("+");
|
|
36
|
+
const dash = withoutBuild.indexOf("-");
|
|
37
|
+
const core = (dash === -1 ? withoutBuild : withoutBuild.slice(0, dash))
|
|
38
|
+
.split(".")
|
|
39
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
40
|
+
return { core, prerelease: dash === -1 ? null : withoutBuild.slice(dash + 1) };
|
|
41
|
+
}
|
|
42
|
+
/** Semantic version order of two exact versions: negative when `a` is older. */
|
|
43
|
+
function compareVersions(a, b) {
|
|
44
|
+
const left = versionParts(a);
|
|
45
|
+
const right = versionParts(b);
|
|
46
|
+
for (let index = 0; index < Math.max(left.core.length, right.core.length); index += 1) {
|
|
47
|
+
const difference = (left.core[index] ?? 0) - (right.core[index] ?? 0);
|
|
48
|
+
if (difference !== 0)
|
|
49
|
+
return difference;
|
|
50
|
+
}
|
|
51
|
+
if (left.prerelease === right.prerelease)
|
|
52
|
+
return 0;
|
|
53
|
+
if (left.prerelease === null)
|
|
54
|
+
return 1;
|
|
55
|
+
if (right.prerelease === null)
|
|
56
|
+
return -1;
|
|
57
|
+
return left.prerelease.localeCompare(right.prerelease, "en", { numeric: true });
|
|
58
|
+
}
|
|
28
59
|
/**
|
|
29
60
|
* Expand the recorded module list against the target manifests. A module may
|
|
30
61
|
* gain a new requirement after it was first added to a project, either another
|
|
@@ -39,6 +70,7 @@ function targetModules(projectRoot, templatesDir, modules, providers) {
|
|
|
39
70
|
const recorded = new Map(modules.map((module) => [module.name, module]));
|
|
40
71
|
const ordered = [];
|
|
41
72
|
const added = [];
|
|
73
|
+
const downgrades = [];
|
|
42
74
|
const visiting = new Set();
|
|
43
75
|
const visited = new Set();
|
|
44
76
|
const visit = (name, requiredBy, capability) => {
|
|
@@ -54,6 +86,17 @@ function targetModules(projectRoot, templatesDir, modules, providers) {
|
|
|
54
86
|
? `Module "${requiredBy}" requires unknown module "${name}".`
|
|
55
87
|
: `Cannot resolve module "${name}" while updating the project.`);
|
|
56
88
|
}
|
|
89
|
+
if (existing?.moduleVersion &&
|
|
90
|
+
resolved.packageName &&
|
|
91
|
+
resolved.moduleVersion &&
|
|
92
|
+
compareVersions(resolved.moduleVersion, existing.moduleVersion) < 0) {
|
|
93
|
+
downgrades.push({
|
|
94
|
+
name: resolved.name,
|
|
95
|
+
packageName: resolved.packageName,
|
|
96
|
+
recorded: existing.moduleVersion,
|
|
97
|
+
installed: resolved.moduleVersion,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
57
100
|
visiting.add(name);
|
|
58
101
|
const moduleManifest = (0, add_1.readModuleManifest)(resolved.dir);
|
|
59
102
|
for (const required of moduleManifest.requires ?? [])
|
|
@@ -89,7 +132,7 @@ function targetModules(projectRoot, templatesDir, modules, providers) {
|
|
|
89
132
|
for (const module of [...modules].sort((a, b) => a.order - b.order)) {
|
|
90
133
|
visit(module.name);
|
|
91
134
|
}
|
|
92
|
-
return { modules: ordered, added };
|
|
135
|
+
return { modules: ordered, added, downgrades };
|
|
93
136
|
}
|
|
94
137
|
function targetApiPaths(projectRoot, templatesDir, modules) {
|
|
95
138
|
const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
|
|
@@ -104,7 +147,7 @@ function targetApiPaths(projectRoot, templatesDir, modules) {
|
|
|
104
147
|
return { declared, pending };
|
|
105
148
|
}
|
|
106
149
|
function targetManagedOverrides(projectRoot, templatesDir, modules, current = []) {
|
|
107
|
-
const overrides = new Set(current);
|
|
150
|
+
const overrides = new Set([...lockfile_1.DEFAULT_MANAGED_OVERRIDES, ...current]);
|
|
108
151
|
const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
|
|
109
152
|
for (const module of modules) {
|
|
110
153
|
const resolved = (0, add_1.resolveModule)(module.packageName ?? module.name, modulesDir, projectRoot);
|
|
@@ -127,21 +170,36 @@ function targetOwnedGlobs(projectRoot, templatesDir, modules, current) {
|
|
|
127
170
|
}
|
|
128
171
|
return [...owned];
|
|
129
172
|
}
|
|
130
|
-
|
|
173
|
+
/** Owned paths the template and the installed modules declare, without the app's own. */
|
|
174
|
+
function declaredOwnedGlobs(projectRoot, templatesDir, modules) {
|
|
175
|
+
return targetOwnedGlobs(projectRoot, templatesDir, modules, []);
|
|
176
|
+
}
|
|
177
|
+
function targetProject(projectRoot, templatesDir, targetAnswers) {
|
|
131
178
|
const manifest = (0, lockfile_1.readManifest)(projectRoot);
|
|
132
179
|
const lock = (0, lockfile_1.readFilesLock)(projectRoot);
|
|
133
180
|
if (!manifest || !lock)
|
|
134
181
|
throw new inspect_1.NotAProjectError();
|
|
135
|
-
const { modules, added } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
|
|
136
|
-
const
|
|
182
|
+
const { modules, added, downgrades } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
|
|
183
|
+
const tree = (0, assemble_1.assembleProject)({
|
|
137
184
|
templatesDir,
|
|
138
185
|
template: manifest.template,
|
|
139
|
-
answers:
|
|
186
|
+
answers: targetAnswers ?? (0, lockfile_1.manifestTemplateVars)(manifest),
|
|
140
187
|
modules,
|
|
141
188
|
projectRoot,
|
|
142
189
|
});
|
|
143
|
-
|
|
144
|
-
|
|
190
|
+
return {
|
|
191
|
+
manifest,
|
|
192
|
+
lock,
|
|
193
|
+
modules,
|
|
194
|
+
addedModules: added,
|
|
195
|
+
moduleDowngrades: downgrades,
|
|
196
|
+
tree,
|
|
197
|
+
ownedGlobs: targetOwnedGlobs(projectRoot, templatesDir, modules, manifest.ownedGlobs),
|
|
198
|
+
managedOverrides: targetManagedOverrides(projectRoot, templatesDir, modules, manifest.managedOverrides),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function planUpdate(projectRoot, templatesDir, options = {}) {
|
|
202
|
+
const { manifest, lock, modules, addedModules: added, moduleDowngrades, tree: newTree, ownedGlobs, managedOverrides, } = targetProject(projectRoot, templatesDir, options.targetAnswers);
|
|
145
203
|
const pendingMigrations = (0, module_migrations_1.collectPendingMigrations)(projectRoot, templatesDir, modules);
|
|
146
204
|
const migrationPlan = (0, module_migrations_1.planModuleMigrations)(projectRoot, newTree, lock, pendingMigrations);
|
|
147
205
|
const changes = migrationPlan.moves.map((move) => ({
|
|
@@ -159,6 +217,7 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
159
217
|
note: conflict,
|
|
160
218
|
});
|
|
161
219
|
}
|
|
220
|
+
const ownedWithoutTemplate = [];
|
|
162
221
|
const movedSources = new Set(migrationPlan.moves.map((move) => move.from));
|
|
163
222
|
const movedTargets = new Set(migrationPlan.moves.map((move) => move.to));
|
|
164
223
|
const paths = new Set([...newTree.keys(), ...Object.keys(lock.files)]);
|
|
@@ -192,6 +251,24 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
192
251
|
changes.push({ path, tier, action: "add", note: "new owned seed in this version" });
|
|
193
252
|
continue;
|
|
194
253
|
}
|
|
254
|
+
if (newText !== null && disk !== null) {
|
|
255
|
+
const newHash = (0, podokit_template_engine_1.hashContent)(newText);
|
|
256
|
+
const recorded = (0, lockfile_1.recordedTemplateHash)(locked);
|
|
257
|
+
if ((0, podokit_template_engine_1.hashContent)(disk) !== newHash) {
|
|
258
|
+
if (recorded && recorded !== newHash) {
|
|
259
|
+
changes.push({
|
|
260
|
+
path,
|
|
261
|
+
tier,
|
|
262
|
+
action: "review",
|
|
263
|
+
note: `owned; the template changed, see podo diff ${path}`,
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// The lock never records files under .podokit/, so only judge tracked files.
|
|
268
|
+
if (!recorded && locked)
|
|
269
|
+
ownedWithoutTemplate.push(path);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
195
272
|
const explicitlyOwned = ownedGlobs.some((glob) => !glob.includes("*") && (0, lockfile_1.matchGlob)(path, glob));
|
|
196
273
|
changes.push({
|
|
197
274
|
path,
|
|
@@ -251,8 +328,10 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
251
328
|
template: manifest.template,
|
|
252
329
|
modules: modules.map((module) => module.name),
|
|
253
330
|
addedModules: added,
|
|
331
|
+
moduleDowngrades,
|
|
254
332
|
changes,
|
|
255
333
|
apiPaths,
|
|
334
|
+
ownedWithoutTemplate,
|
|
256
335
|
};
|
|
257
336
|
}
|
|
258
337
|
function assertRecordedExternalModules(moduleRoot, templatesDir, modules) {
|
|
@@ -297,6 +376,48 @@ function previousExternalModules(projectRoot, templatesDir, modules, providedRoo
|
|
|
297
376
|
}
|
|
298
377
|
return { root, cleanup: true };
|
|
299
378
|
}
|
|
379
|
+
/** The project as the templates of the version it is on generated it. */
|
|
380
|
+
function previousTree(projectRoot, manifest, oldTemplatesDir, oldExternalModulesRoot) {
|
|
381
|
+
const previousModules = previousExternalModules(projectRoot, oldTemplatesDir, manifest.modules, oldExternalModulesRoot);
|
|
382
|
+
try {
|
|
383
|
+
return (0, assemble_1.assembleProject)({
|
|
384
|
+
templatesDir: oldTemplatesDir,
|
|
385
|
+
template: manifest.template,
|
|
386
|
+
answers: (0, lockfile_1.manifestTemplateVars)(manifest),
|
|
387
|
+
modules: manifest.modules,
|
|
388
|
+
projectRoot: previousModules.root,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
if (previousModules.cleanup)
|
|
393
|
+
(0, node_fs_1.rmSync)(previousModules.root, { recursive: true, force: true });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* What the template changed between the project's version and this one for
|
|
398
|
+
* each owned file the plan marks for review, given the previous version's
|
|
399
|
+
* templates.
|
|
400
|
+
*/
|
|
401
|
+
function upstreamChanges(projectRoot, templatesDir, plan, oldTemplatesDir, oldExternalModulesRoot) {
|
|
402
|
+
const review = plan.changes.filter((change) => change.action === "review");
|
|
403
|
+
if (!review.length)
|
|
404
|
+
return [];
|
|
405
|
+
const target = targetProject(projectRoot, templatesDir);
|
|
406
|
+
const oldTree = previousTree(projectRoot, target.manifest, oldTemplatesDir, oldExternalModulesRoot);
|
|
407
|
+
return review.map((change) => {
|
|
408
|
+
const before = treeText(oldTree, change.path);
|
|
409
|
+
const after = treeText(target.tree, change.path) ?? "";
|
|
410
|
+
return {
|
|
411
|
+
path: change.path,
|
|
412
|
+
diff: before === null
|
|
413
|
+
? ""
|
|
414
|
+
: (0, text_diff_1.unifiedDiff)(before, after, {
|
|
415
|
+
fromLabel: `podokit-${plan.fromVersion}/${change.path}`,
|
|
416
|
+
toLabel: `podokit-${plan.toVersion}/${change.path}`,
|
|
417
|
+
}),
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
}
|
|
300
421
|
function writeFile(projectRoot, path, content) {
|
|
301
422
|
const abs = (0, node_path_1.join)(projectRoot, path);
|
|
302
423
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(abs), { recursive: true });
|
|
@@ -328,6 +449,8 @@ function updatedFilesLock(projectRoot, newTree, previous, ownedGlobs, managedOve
|
|
|
328
449
|
}
|
|
329
450
|
entry.outHash = (0, podokit_template_engine_1.hashContent)(newText);
|
|
330
451
|
}
|
|
452
|
+
if (entry.tier === "owned")
|
|
453
|
+
entry.templateHash = (0, podokit_template_engine_1.hashContent)(newText);
|
|
331
454
|
continue;
|
|
332
455
|
}
|
|
333
456
|
if (entry.tier === "owned")
|
|
@@ -344,16 +467,19 @@ function updatedFilesLock(projectRoot, newTree, previous, ownedGlobs, managedOve
|
|
|
344
467
|
delete next.files[path];
|
|
345
468
|
}
|
|
346
469
|
}
|
|
347
|
-
// Keep a tombstone for an owned seed that the application deleted or moved
|
|
348
|
-
//
|
|
349
|
-
//
|
|
470
|
+
// Keep a tombstone for an owned seed that the application deleted or moved,
|
|
471
|
+
// including a file this update turns from managed into owned. Without the
|
|
472
|
+
// entry, the next update would mistake the missing path for a newly
|
|
473
|
+
// introduced seed and recreate it.
|
|
350
474
|
for (const [path, entry] of Object.entries(previous.files)) {
|
|
351
|
-
if (
|
|
475
|
+
if (next.files[path])
|
|
352
476
|
continue;
|
|
353
477
|
const newText = treeText(newTree, path);
|
|
354
478
|
if (newText !== null &&
|
|
355
479
|
(0, lockfile_1.classifyTier)(path, newText, ownedGlobs, managedOverrides) === "owned") {
|
|
356
|
-
next.files[path] = entry
|
|
480
|
+
next.files[path] = entry.tier === "owned"
|
|
481
|
+
? entry
|
|
482
|
+
: { tier: "owned", outHash: entry.outHash, templateHash: (0, podokit_template_engine_1.hashContent)(newText) };
|
|
357
483
|
}
|
|
358
484
|
}
|
|
359
485
|
return next;
|
|
@@ -375,6 +501,10 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
375
501
|
forceManagedPaths: options.forceManagedPaths,
|
|
376
502
|
onlyPaths: options.onlyPaths,
|
|
377
503
|
});
|
|
504
|
+
if (plan.moduleDowngrades.length) {
|
|
505
|
+
throw new Error(`Refusing to downgrade external modules:\n${describeDowngrades(plan.moduleDowngrades)}\n` +
|
|
506
|
+
"Install at least the recorded version (for example bun install), then run podo update again.");
|
|
507
|
+
}
|
|
378
508
|
const { modules } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
|
|
379
509
|
const newTree = (0, assemble_1.assembleProject)({
|
|
380
510
|
templatesDir,
|
|
@@ -401,20 +531,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
401
531
|
const needsMergeBase = plan.changes.some((change) => change.action === "conflict");
|
|
402
532
|
let oldTree = null;
|
|
403
533
|
if (options.oldTemplatesDir && needsMergeBase) {
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
oldTree = (0, assemble_1.assembleProject)({
|
|
407
|
-
templatesDir: options.oldTemplatesDir,
|
|
408
|
-
template: manifest.template,
|
|
409
|
-
answers: (0, lockfile_1.manifestTemplateVars)(manifest),
|
|
410
|
-
modules: manifest.modules,
|
|
411
|
-
projectRoot: previousModules.root,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
finally {
|
|
415
|
-
if (previousModules.cleanup)
|
|
416
|
-
(0, node_fs_1.rmSync)(previousModules.root, { recursive: true, force: true });
|
|
417
|
-
}
|
|
534
|
+
oldTree = previousTree(projectRoot, manifest, options.oldTemplatesDir, options.oldExternalModulesRoot);
|
|
418
535
|
}
|
|
419
536
|
if (options.abortOnConflict) {
|
|
420
537
|
const unresolved = [];
|
|
@@ -444,6 +561,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
444
561
|
removed: [],
|
|
445
562
|
merged: [],
|
|
446
563
|
conflicts: [],
|
|
564
|
+
review: plan.changes.filter((change) => change.action === "review").map((change) => change.path),
|
|
447
565
|
apiPaths: { changed: [], skipped: [] },
|
|
448
566
|
};
|
|
449
567
|
for (const change of plan.changes) {
|
|
@@ -523,6 +641,12 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
523
641
|
});
|
|
524
642
|
return result;
|
|
525
643
|
}
|
|
644
|
+
/** One line per external module installed at an older version than recorded. */
|
|
645
|
+
function describeDowngrades(downgrades) {
|
|
646
|
+
return downgrades
|
|
647
|
+
.map((entry) => ` ${entry.packageName} ${entry.installed} is installed; the project applied ${entry.recorded}`)
|
|
648
|
+
.join("\n");
|
|
649
|
+
}
|
|
526
650
|
/** Why an update adds a module, for the plan report. */
|
|
527
651
|
function addedModuleReason(module) {
|
|
528
652
|
return module.capability
|
|
@@ -532,6 +656,7 @@ function addedModuleReason(module) {
|
|
|
532
656
|
/** Count actions for a one-line summary. */
|
|
533
657
|
function summarize(plan) {
|
|
534
658
|
const counts = {
|
|
659
|
+
review: 0,
|
|
535
660
|
update: 0,
|
|
536
661
|
merge: 0,
|
|
537
662
|
conflict: 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podosoft/podokit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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.5.0",
|
|
49
49
|
"@podosoft/podokit-template-engine": "^0.4.2"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "@playwright/test";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, utimesSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { rm } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { exclusive, lockPath } from "../helpers/exclusive";
|
|
7
|
-
|
|
8
|
-
// Checks the lock that specs changing shared settings hold. Names are unique per
|
|
9
|
-
// run so these never wait on, or release, a lock a real spec holds.
|
|
10
|
-
const unique = (label: string): string => `self-test-${label}-${process.pid}-${Date.now()}`;
|
|
11
|
-
|
|
12
|
-
test("exclusive releases its locks when the body fails", async () => {
|
|
13
|
-
const first = unique("first");
|
|
14
|
-
const second = unique("second");
|
|
15
|
-
await expect(
|
|
16
|
-
exclusive([second, first], async () => {
|
|
17
|
-
expect(existsSync(lockPath(first))).toBe(true);
|
|
18
|
-
expect(existsSync(lockPath(second))).toBe(true);
|
|
19
|
-
throw new Error("body failed");
|
|
20
|
-
}),
|
|
21
|
-
).rejects.toThrow("body failed");
|
|
22
|
-
expect(existsSync(lockPath(first))).toBe(false);
|
|
23
|
-
expect(existsSync(lockPath(second))).toBe(false);
|
|
24
|
-
|
|
25
|
-
// The next holder gets the lock without waiting.
|
|
26
|
-
const started = Date.now();
|
|
27
|
-
await exclusive(first, async () => undefined);
|
|
28
|
-
expect(Date.now() - started).toBeLessThan(1_000);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("exclusive runs holders of one lock one at a time", async () => {
|
|
32
|
-
const name = unique("serial");
|
|
33
|
-
const events: string[] = [];
|
|
34
|
-
const hold = (id: string): Promise<void> =>
|
|
35
|
-
exclusive(name, async () => {
|
|
36
|
-
events.push(`${id} start`);
|
|
37
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
38
|
-
events.push(`${id} end`);
|
|
39
|
-
});
|
|
40
|
-
await Promise.all([hold("a"), hold("b")]);
|
|
41
|
-
expect(events.map((event) => event.split(" ")[1])).toEqual(["start", "end", "start", "end"]);
|
|
42
|
-
expect(existsSync(lockPath(name))).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("exclusive takes over a lock left by a run that was killed", async () => {
|
|
46
|
-
const name = unique("stale");
|
|
47
|
-
mkdirSync(lockPath(name));
|
|
48
|
-
const longAgo = new Date(Date.now() - 10 * 60_000);
|
|
49
|
-
utimesSync(lockPath(name), longAgo, longAgo);
|
|
50
|
-
await expect(exclusive(name, async () => "acquired")).resolves.toBe("acquired");
|
|
51
|
-
expect(existsSync(lockPath(name))).toBe(false);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("exclusive takes over a lock whose holder exited, such as after Ctrl-C", async () => {
|
|
55
|
-
const name = unique("exited");
|
|
56
|
-
const exited = spawnSync(process.execPath, ["-e", ""]).pid;
|
|
57
|
-
mkdirSync(lockPath(name));
|
|
58
|
-
writeFileSync(join(lockPath(name), "owner"), String(exited));
|
|
59
|
-
const started = Date.now();
|
|
60
|
-
await expect(exclusive(name, async () => "acquired")).resolves.toBe("acquired");
|
|
61
|
-
expect(Date.now() - started).toBeLessThan(1_000);
|
|
62
|
-
expect(existsSync(lockPath(name))).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("exclusive waits for a lock whose holder is still running", async () => {
|
|
66
|
-
const name = unique("alive");
|
|
67
|
-
mkdirSync(lockPath(name));
|
|
68
|
-
writeFileSync(join(lockPath(name), "owner"), String(process.pid));
|
|
69
|
-
const waiting = exclusive(name, async () => "acquired");
|
|
70
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
71
|
-
expect(existsSync(lockPath(name))).toBe(true);
|
|
72
|
-
// The holder finishes; the waiter then gets the lock.
|
|
73
|
-
await rm(lockPath(name), { recursive: true, force: true });
|
|
74
|
-
await expect(waiting).resolves.toBe("acquired");
|
|
75
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
// Locks for server-wide settings that specs in more than one file change. The
|
|
6
|
-
// suite runs on one worker by default; with --workers above 1, specs of the api
|
|
7
|
-
// and ui projects run at the same time, and two that flip the same setting
|
|
8
|
-
// would undo each other mid-test:
|
|
9
|
-
//
|
|
10
|
-
// - `require2fa`, `magic-link`, `oidc-provider`: feature flags
|
|
11
|
-
// - `session-idle-timeout`: automatic logout in the auth configuration
|
|
12
|
-
// - `social-providers`: any social sign-in provider in the auth configuration
|
|
13
|
-
// - `site-general`: the General settings form, which saves all of its fields
|
|
14
|
-
// (name, footer, locale, maintenance mode, public sign-up, ...) at once
|
|
15
|
-
// - `site-theme`: the Appearance settings
|
|
16
|
-
//
|
|
17
|
-
// A spec that holds a lock calls `test.slow()`, since waiting for another
|
|
18
|
-
// holder counts against its timeout.
|
|
19
|
-
|
|
20
|
-
// Longer than any spec holds a lock. A lock whose holder process has exited is
|
|
21
|
-
// taken over at once; this bounds the wait when the holder cannot be checked.
|
|
22
|
-
const STALE_MS = 120_000;
|
|
23
|
-
const OWNER_FILE = "owner";
|
|
24
|
-
|
|
25
|
-
/** The lock directory for `name`, shared by every worker on this machine. */
|
|
26
|
-
export function lockPath(name: string): string {
|
|
27
|
-
return join(tmpdir(), `podokit-e2e-${name}.lock`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Whether the worker that holds `lock` has exited, such as after Ctrl-C. */
|
|
31
|
-
async function holderExited(lock: string): Promise<boolean> {
|
|
32
|
-
const pid = Number(await readFile(join(lock, OWNER_FILE), "utf8").catch(() => ""));
|
|
33
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
34
|
-
try {
|
|
35
|
-
process.kill(pid, 0);
|
|
36
|
-
return false;
|
|
37
|
-
} catch (error) {
|
|
38
|
-
return (error as NodeJS.ErrnoException).code === "ESRCH";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function acquire(lock: string): Promise<void> {
|
|
43
|
-
for (;;) {
|
|
44
|
-
const created = await mkdir(lock).then(
|
|
45
|
-
() => true,
|
|
46
|
-
(error: NodeJS.ErrnoException) => {
|
|
47
|
-
if (error.code === "EEXIST") return false;
|
|
48
|
-
throw error;
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
if (created) {
|
|
52
|
-
await writeFile(join(lock, OWNER_FILE), String(process.pid)).catch(async (error: unknown) => {
|
|
53
|
-
await rm(lock, { recursive: true, force: true });
|
|
54
|
-
throw error;
|
|
55
|
-
});
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const age = await stat(lock).then((info) => Date.now() - info.mtimeMs, () => 0);
|
|
59
|
-
if (age > STALE_MS || (await holderExited(lock))) await rm(lock, { recursive: true, force: true });
|
|
60
|
-
else await new Promise((resolve) => setTimeout(resolve, 200));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Runs `body` while no other Playwright worker on this machine holds any of
|
|
65
|
-
* the named locks, and releases them however `body` ends. */
|
|
66
|
-
export async function exclusive<T>(names: string | string[], body: () => Promise<T>): Promise<T> {
|
|
67
|
-
// One acquisition order for every caller, so two specs that each need
|
|
68
|
-
// several locks cannot wait on each other.
|
|
69
|
-
const locks = [...new Set([names].flat())].sort().map(lockPath);
|
|
70
|
-
const held: string[] = [];
|
|
71
|
-
try {
|
|
72
|
-
for (const lock of locks) {
|
|
73
|
-
await acquire(lock);
|
|
74
|
-
held.push(lock);
|
|
75
|
-
}
|
|
76
|
-
return await body();
|
|
77
|
-
} finally {
|
|
78
|
-
for (const lock of held.reverse()) await rm(lock, { recursive: true, force: true });
|
|
79
|
-
}
|
|
80
|
-
}
|