@podosoft/podokit 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/app-proxy-timeouts.d.ts +27 -0
- package/dist/app-proxy-timeouts.js +180 -0
- package/dist/assemble.d.ts +5 -2
- package/dist/assemble.js +14 -4
- package/dist/deploy-checks.d.ts +44 -0
- package/dist/deploy-checks.js +267 -0
- package/dist/deploy-compose-profile.d.ts +6 -2
- package/dist/deploy-compose-profile.js +15 -6
- package/dist/deploy-compose-render.d.ts +12 -4
- package/dist/deploy-compose-render.js +47 -18
- package/dist/deploy-compose.d.ts +2 -5
- package/dist/deploy-compose.js +7 -2
- package/dist/deploy-profile.d.ts +6 -2
- package/dist/deploy-profile.js +11 -7
- package/dist/deploy-render.d.ts +4 -1
- package/dist/deploy-render.js +27 -21
- package/dist/deploy-schema.d.ts +60 -2
- package/dist/deploy-schema.js +141 -14
- package/dist/deploy.d.ts +2 -5
- package/dist/deploy.js +5 -2
- package/dist/dev.d.ts +21 -0
- package/dist/dev.js +78 -3
- package/dist/eject.js +2 -0
- package/dist/index.js +85 -5
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +5 -2
- package/dist/lockfile.d.ts +24 -0
- package/dist/lockfile.js +74 -4
- package/dist/provider.js +10 -5
- package/dist/remove.js +1 -1
- package/dist/template-files.d.ts +42 -0
- package/dist/template-files.js +102 -0
- package/dist/templates/fullstack/AGENTS.md +30 -4
- package/dist/templates/fullstack/README.md +8 -7
- package/dist/templates/fullstack/apps/api/package.json +4 -4
- package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
- package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
- package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +7 -2
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +41 -25
- package/dist/templates/fullstack/apps/api/src/build.spec.ts +121 -0
- package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +18 -3
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
- package/dist/templates/fullstack/apps/api/src/config/providers.ts +7 -3
- package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +72 -4
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +78 -6
- package/dist/templates/fullstack/apps/api/src/core/services.ts +13 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
- package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
- package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
- package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
- package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/app.css +3 -0
- package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +55 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +2 -1
- package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
- package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
- package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
- package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
- package/dist/templates/fullstack/compose.dev.yaml +7 -4
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +13 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
- package/dist/templates/fullstack/dot-env.docker.example +5 -2
- package/dist/templates/fullstack/dot-env.example +4 -0
- package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
- package/dist/templates/fullstack/infra/k3s/configmap.yaml +1 -0
- package/dist/templates/fullstack/tests/README.md +50 -1
- package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
- package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
- package/dist/templates/fullstack/tests/helpers/seeded-admin.ts +15 -0
- package/dist/templates/fullstack/tests/package.json +3 -3
- package/dist/templates/fullstack/tests/playwright.config.ts +10 -4
- package/dist/templates/fullstack/tests/playwright.extensions.ts +149 -1
- package/dist/templates/fullstack/tests/run-e2e.ts +28 -0
- package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +53 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +11 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/theme-toggle.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +3 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +41 -11
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/hydration.ts +13 -4
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +14 -6
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +55 -55
- package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
- package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +8 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
- package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +17 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +18 -0
- package/dist/templates/modules/audit-log/module.manifest.json +15 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +2 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +7 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
- package/dist/templates/modules/auth/module.manifest.json +8 -2
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +107 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +113 -9
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
- package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
- package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +174 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +150 -3
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +3 -6
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +3 -6
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +45 -20
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +3 -6
- package/dist/templates/todo/AGENTS.md +5 -1
- package/dist/templates/todo/README.md +6 -4
- package/dist/text-diff.d.ts +12 -0
- package/dist/text-diff.js +111 -0
- package/dist/update.d.ts +61 -3
- package/dist/update.js +152 -27
- package/package.json +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { expect, test, type APIRequestContext, type PlaywrightWorkerArgs } from "@playwright/test";
|
|
2
|
+
import { seededAdmin } from "../helpers/seeded-admin";
|
|
2
3
|
|
|
3
4
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
4
5
|
const secondaryApi = process.env.E2E_SECONDARY_API_URL;
|
|
5
6
|
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
|
-
};
|
|
12
7
|
|
|
13
8
|
type Playwright = PlaywrightWorkerArgs["playwright"];
|
|
14
9
|
|
|
@@ -39,6 +34,8 @@ async function signUp(ctx: APIRequestContext): Promise<SessionUser | undefined>
|
|
|
39
34
|
|
|
40
35
|
/** Resolves false when no administrator session is available. */
|
|
41
36
|
async function signInAdmin(ctx: APIRequestContext): Promise<boolean> {
|
|
37
|
+
const admin = await seededAdmin();
|
|
38
|
+
if (!admin) return false;
|
|
42
39
|
const response = await ctx.post("/api/auth/sign-in/email", { data: admin });
|
|
43
40
|
return response.ok() && ((await sessionUser(ctx))?.roles ?? []).includes("admin");
|
|
44
41
|
}
|
|
@@ -87,7 +87,11 @@ Merge added module settings from `.env.example` into an existing `.env`.
|
|
|
87
87
|
Better Auth schema before the application migrations. `{{apiRun}} migration:run` runs
|
|
88
88
|
only the application migrations and never creates Better Auth tables;
|
|
89
89
|
`{{apiRun}} migration:revert` reverts the latest application migration.
|
|
90
|
-
- e2e (Playwright, ships in `tests/`): `{{rootRun}} test:e2e`.
|
|
90
|
+
- e2e (Playwright, ships in `tests/`): `{{rootRun}} test:e2e`. Run the whole suite on more
|
|
91
|
+
workers with `E2E_WORKERS=2`, never `--workers`. Tag a test `{ tag: "@exclusive" }` when
|
|
92
|
+
it changes state other tests rely on (a server-wide setting, a seeded account's profile);
|
|
93
|
+
tagged tests run alone after the rest. Give each test its own mail and SMS recipients and
|
|
94
|
+
never empty the shared Mailpit inbox or SMS sink. See `tests/README.md`.
|
|
91
95
|
- Playwright officially runs on Node, so `bunx playwright` respects its Node
|
|
92
96
|
shebang. Node LTS is a test-tool dependency only; API, web, worker, migrations,
|
|
93
97
|
builds, and unit tests run on Bun.
|
|
@@ -55,8 +55,9 @@ cp .env.example .env
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
Open the URL printed by the command; a new project defaults to
|
|
58
|
-
**http://{{
|
|
59
|
-
source of truth if you change the hostname
|
|
58
|
+
**http://{{devHostname}}**. The committed `.podokit/dev.json` is the
|
|
59
|
+
source of truth if you change the hostname: `podo dev` sets `CORS_ORIGIN` and
|
|
60
|
+
`BETTER_AUTH_URL` for the web and API containers from it. The first running PodoKit project starts
|
|
60
61
|
one user-level Traefik gateway on `127.0.0.1:80`; additional projects reuse it and
|
|
61
62
|
route by that hostname. Even a single app uses
|
|
62
63
|
the same topology, with one route and no project-specific host port.
|
|
@@ -164,5 +165,6 @@ PodoKit [deployment guide](https://github.com/podosoft-dev/podokit/blob/main/doc
|
|
|
164
165
|
For a Docker Compose deployment with API WebSockets, list their exact paths under
|
|
165
166
|
`exposure.webSocketPaths`. The driver keeps one published port, routes those paths
|
|
166
167
|
to API, and sends every other request to web. Large-upload routes listed under
|
|
167
|
-
`exposure.apiPaths` reach the API the same way, with `/api` removed.
|
|
168
|
-
|
|
168
|
+
`exposure.apiPaths` reach the API the same way, with `/api` removed. Set `exposure.edgeProxies` to the
|
|
169
|
+
number of reverse proxies in front of the deployment, and with the gateway list only their direct CIDR
|
|
170
|
+
blocks in `exposure.trustedProxyCidrs`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-based unified diff for `podo diff <path>`. It is dependency-free and
|
|
3
|
+
* meant for source files of a few thousand lines; above `MAX_CELLS` it reports
|
|
4
|
+
* the whole file as replaced instead of building the LCS table.
|
|
5
|
+
*/
|
|
6
|
+
export interface UnifiedDiffOptions {
|
|
7
|
+
fromLabel: string;
|
|
8
|
+
toLabel: string;
|
|
9
|
+
context?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Unified diff from `from` to `to`, or an empty string when they are equal. */
|
|
12
|
+
export declare function unifiedDiff(from: string, to: string, options: UnifiedDiffOptions): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Line-based unified diff for `podo diff <path>`. It is dependency-free and
|
|
4
|
+
* meant for source files of a few thousand lines; above `MAX_CELLS` it reports
|
|
5
|
+
* the whole file as replaced instead of building the LCS table.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.unifiedDiff = unifiedDiff;
|
|
9
|
+
const MAX_CELLS = 25_000_000;
|
|
10
|
+
function splitLines(text) {
|
|
11
|
+
if (text === "")
|
|
12
|
+
return [];
|
|
13
|
+
const lines = text.split("\n");
|
|
14
|
+
if (lines[lines.length - 1] === "")
|
|
15
|
+
lines.pop();
|
|
16
|
+
return lines;
|
|
17
|
+
}
|
|
18
|
+
function operations(a, b) {
|
|
19
|
+
let start = 0;
|
|
20
|
+
while (start < a.length && start < b.length && a[start] === b[start])
|
|
21
|
+
start += 1;
|
|
22
|
+
let endA = a.length;
|
|
23
|
+
let endB = b.length;
|
|
24
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
25
|
+
endA -= 1;
|
|
26
|
+
endB -= 1;
|
|
27
|
+
}
|
|
28
|
+
const head = a.slice(0, start).map((line) => ({ kind: " ", line }));
|
|
29
|
+
const tail = a.slice(endA).map((line) => ({ kind: " ", line }));
|
|
30
|
+
const midA = a.slice(start, endA);
|
|
31
|
+
const midB = b.slice(start, endB);
|
|
32
|
+
const m = midA.length;
|
|
33
|
+
const n = midB.length;
|
|
34
|
+
const middle = [];
|
|
35
|
+
if ((m + 1) * (n + 1) > MAX_CELLS) {
|
|
36
|
+
middle.push(...midA.map((line) => ({ kind: "-", line })));
|
|
37
|
+
middle.push(...midB.map((line) => ({ kind: "+", line })));
|
|
38
|
+
return [...head, ...middle, ...tail];
|
|
39
|
+
}
|
|
40
|
+
const width = n + 1;
|
|
41
|
+
const table = new Uint32Array((m + 1) * width);
|
|
42
|
+
for (let i = m - 1; i >= 0; i -= 1) {
|
|
43
|
+
for (let j = n - 1; j >= 0; j -= 1) {
|
|
44
|
+
table[i * width + j] = midA[i] === midB[j]
|
|
45
|
+
? table[(i + 1) * width + j + 1] + 1
|
|
46
|
+
: Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let i = 0;
|
|
50
|
+
let j = 0;
|
|
51
|
+
while (i < m || j < n) {
|
|
52
|
+
if (i < m && j < n && midA[i] === midB[j]) {
|
|
53
|
+
middle.push({ kind: " ", line: midA[i] });
|
|
54
|
+
i += 1;
|
|
55
|
+
j += 1;
|
|
56
|
+
}
|
|
57
|
+
else if (i < m && (j === n || table[(i + 1) * width + j] >= table[i * width + j + 1])) {
|
|
58
|
+
middle.push({ kind: "-", line: midA[i] });
|
|
59
|
+
i += 1;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
middle.push({ kind: "+", line: midB[j] });
|
|
63
|
+
j += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return [...head, ...middle, ...tail];
|
|
67
|
+
}
|
|
68
|
+
function range(start, count) {
|
|
69
|
+
if (count === 0)
|
|
70
|
+
return `${start},0`;
|
|
71
|
+
return count === 1 ? `${start + 1}` : `${start + 1},${count}`;
|
|
72
|
+
}
|
|
73
|
+
/** Unified diff from `from` to `to`, or an empty string when they are equal. */
|
|
74
|
+
function unifiedDiff(from, to, options) {
|
|
75
|
+
if (from === to)
|
|
76
|
+
return "";
|
|
77
|
+
const context = options.context ?? 3;
|
|
78
|
+
const ops = operations(splitLines(from), splitLines(to));
|
|
79
|
+
const changed = ops.flatMap((op, index) => (op.kind === " " ? [] : [index]));
|
|
80
|
+
const out = [`--- ${options.fromLabel}`, `+++ ${options.toLabel}`];
|
|
81
|
+
if (!changed.length) {
|
|
82
|
+
// Only the trailing newline differs.
|
|
83
|
+
out.push(from.endsWith("\n") ? "\\ the template ends with a newline" : "\\ the template has no newline at end of file", to.endsWith("\n") ? "\\ the file ends with a newline" : "\\ the file has no newline at end of file");
|
|
84
|
+
return `${out.join("\n")}\n`;
|
|
85
|
+
}
|
|
86
|
+
let hunkStart = 0;
|
|
87
|
+
while (hunkStart < changed.length) {
|
|
88
|
+
let hunkEnd = hunkStart;
|
|
89
|
+
while (hunkEnd + 1 < changed.length &&
|
|
90
|
+
changed[hunkEnd + 1] - changed[hunkEnd] <= context * 2 + 1) {
|
|
91
|
+
hunkEnd += 1;
|
|
92
|
+
}
|
|
93
|
+
const first = Math.max(0, changed[hunkStart] - context);
|
|
94
|
+
const last = Math.min(ops.length - 1, changed[hunkEnd] + context);
|
|
95
|
+
let fromLine = 0;
|
|
96
|
+
let toLine = 0;
|
|
97
|
+
for (let index = 0; index < first; index += 1) {
|
|
98
|
+
if (ops[index].kind !== "+")
|
|
99
|
+
fromLine += 1;
|
|
100
|
+
if (ops[index].kind !== "-")
|
|
101
|
+
toLine += 1;
|
|
102
|
+
}
|
|
103
|
+
const body = ops.slice(first, last + 1);
|
|
104
|
+
const fromCount = body.filter((op) => op.kind !== "+").length;
|
|
105
|
+
const toCount = body.filter((op) => op.kind !== "-").length;
|
|
106
|
+
out.push(`@@ -${range(fromLine, fromCount)} +${range(toLine, toCount)} @@`);
|
|
107
|
+
out.push(...body.map((op) => `${op.kind}${op.line}`));
|
|
108
|
+
hunkStart = hunkEnd + 1;
|
|
109
|
+
}
|
|
110
|
+
return `${out.join("\n")}\n`;
|
|
111
|
+
}
|
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.1",
|
|
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.1",
|
|
49
49
|
"@podosoft/podokit-template-engine": "^0.4.2"
|
|
50
50
|
}
|
|
51
51
|
}
|