@podosoft/podokit 0.13.0 → 0.15.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 +3 -0
- package/dist/add.d.ts +27 -1
- package/dist/add.js +3 -1
- package/dist/index.js +6 -3
- package/dist/lib.d.ts +1 -1
- package/dist/lockfile.d.ts +4 -1
- package/dist/lockfile.js +6 -3
- package/dist/module-migrations.d.ts +25 -0
- package/dist/module-migrations.js +233 -0
- package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +3 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.html +2 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/input.svelte +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +5 -0
- package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +6 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.example +2 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/configmap.yaml +1 -0
- package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +6 -4
- package/dist/templates/fullstack-nest-svelte/tests/playwright.extensions.ts +44 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image-upload.interceptor.ts +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.controller.ts +73 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.module.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +43 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +110 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.spec.ts +79 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +35 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +155 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +33 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +13 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +13 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +78 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/+layout.server.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/+layout.svelte +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.svelte +18 -10
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +2 -47
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+page.svelte +4 -0
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +11 -6
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +18 -8
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +96 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +15 -9
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +1 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +83 -0
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +13 -10
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +69 -3
- package/dist/templates/modules/admin-dashboard/files/tests/seed.teardown.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +64 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +16 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +13 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +3 -6
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +4 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +4 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +34 -28
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +29 -39
- package/dist/templates/modules/admin-dashboard/module.manifest.json +74 -11
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +5 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.spec.ts +42 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.ts +27 -0
- package/dist/templates/modules/auth/module.manifest.json +1 -1
- package/dist/templates/todo/Dockerfile.dev +3 -1
- package/dist/templates/todo/apps/api/package.json +1 -1
- package/dist/templates/todo/apps/web/package.json +1 -1
- package/dist/templates/todo/compose.dev.yaml +6 -0
- package/dist/templates/todo/tests/playwright.config.ts +6 -4
- package/dist/templates/todo/tests/playwright.extensions.ts +44 -0
- package/dist/update.d.ts +8 -1
- package/dist/update.js +85 -25
- package/package.json +1 -1
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/{app-sidebar.svelte → admin-sidebar.svelte} +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/general-settings.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.server.ts +0 -0
package/README.md
CHANGED
|
@@ -101,6 +101,9 @@ podo update --apply # apply it: clean updates are written, your edits are 3-way
|
|
|
101
101
|
|
|
102
102
|
Files you own (routes, your components, shadcn UI) are never touched. See the
|
|
103
103
|
[updating guide](https://github.com/podosoft-dev/podokit/blob/main/docs/updating.md).
|
|
104
|
+
An exact path handed over with `podo eject` remains owned even if a module also
|
|
105
|
+
declares it managed; if you later move or delete that file, update reports the
|
|
106
|
+
missing owned path instead of recreating it.
|
|
104
107
|
|
|
105
108
|
## What you get (`fullstack-nest-svelte`)
|
|
106
109
|
|
package/dist/add.d.ts
CHANGED
|
@@ -7,7 +7,31 @@ interface Injection {
|
|
|
7
7
|
}
|
|
8
8
|
/** Highest `module.manifest.json` schema version this CLI understands. A package
|
|
9
9
|
* module declaring a higher version is rejected rather than mis-applied. */
|
|
10
|
-
export declare const SUPPORTED_MANIFEST_VERSION =
|
|
10
|
+
export declare const SUPPORTED_MANIFEST_VERSION = 2;
|
|
11
|
+
export interface ModulePathMove {
|
|
12
|
+
/** Project-relative file or directory path from an older module layout. */
|
|
13
|
+
from: string;
|
|
14
|
+
/** Project-relative destination. Existing files are never overwritten. */
|
|
15
|
+
to: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ModuleTextReplacement {
|
|
18
|
+
/** Destination path after all migration moves have been applied. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Exact text expected in the existing generated file. */
|
|
21
|
+
from: string;
|
|
22
|
+
/** Replacement text written while preserving every other local edit. */
|
|
23
|
+
to: string;
|
|
24
|
+
/** Required number of matches. A mismatch aborts the migration atomically. */
|
|
25
|
+
expected: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ModuleMigration {
|
|
28
|
+
/** Stable module-local identifier recorded after a successful apply. */
|
|
29
|
+
id: string;
|
|
30
|
+
/** File or directory moves performed as one atomic migration. */
|
|
31
|
+
moves: ModulePathMove[];
|
|
32
|
+
/** Optional exact rewrites needed after a move, such as renamed imports. */
|
|
33
|
+
replacements?: ModuleTextReplacement[];
|
|
34
|
+
}
|
|
11
35
|
export interface ModuleManifest {
|
|
12
36
|
/** Manifest schema version. Absent (bundled modules) means "same as the CLI". */
|
|
13
37
|
manifestVersion?: number;
|
|
@@ -35,6 +59,8 @@ export interface ModuleManifest {
|
|
|
35
59
|
/** Module files that remain update-managed even when a broad default glob
|
|
36
60
|
* (for example `.claude/**`) would otherwise classify them as owned. */
|
|
37
61
|
managedOverrides?: string[];
|
|
62
|
+
/** Idempotent path migrations applied by `podo update --apply`. */
|
|
63
|
+
migrations?: ModuleMigration[];
|
|
38
64
|
}
|
|
39
65
|
export interface PackageOverlay {
|
|
40
66
|
dependencies?: Record<string, string>;
|
package/dist/add.js
CHANGED
|
@@ -14,7 +14,7 @@ const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
|
14
14
|
const lockfile_1 = require("./lockfile");
|
|
15
15
|
/** Highest `module.manifest.json` schema version this CLI understands. A package
|
|
16
16
|
* module declaring a higher version is rejected rather than mis-applied. */
|
|
17
|
-
exports.SUPPORTED_MANIFEST_VERSION =
|
|
17
|
+
exports.SUPPORTED_MANIFEST_VERSION = 2;
|
|
18
18
|
/** Return the legacy targetApp package fields plus any additional app overlays. */
|
|
19
19
|
function modulePackageOverlays(manifest) {
|
|
20
20
|
const overlays = new Map();
|
|
@@ -281,10 +281,12 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
283
|
function moduleRecord(module) {
|
|
284
|
+
const migrations = readModuleManifest(module.dir).migrations ?? [];
|
|
284
285
|
return {
|
|
285
286
|
name: module.name,
|
|
286
287
|
packageName: module.packageName,
|
|
287
288
|
moduleVersion: module.moduleVersion,
|
|
289
|
+
appliedMigrations: migrations.map((migration) => migration.id),
|
|
288
290
|
};
|
|
289
291
|
}
|
|
290
292
|
/** Copy a module overlay without clobbering app-owned presentation files. */
|
package/dist/index.js
CHANGED
|
@@ -288,7 +288,8 @@ async function main(argv) {
|
|
|
288
288
|
try {
|
|
289
289
|
if (args.apply) {
|
|
290
290
|
const result = (0, update_1.applyUpdate)(process.cwd(), templatesDir, { oldTemplatesDir: args.from });
|
|
291
|
-
process.stdout.write(`Applied: ${result.written.length} written, ${result.
|
|
291
|
+
process.stdout.write(`Applied: ${result.written.length} written, ${result.moved.length} moved, ` +
|
|
292
|
+
`${result.removed.length} removed, ` +
|
|
292
293
|
`${result.merged.length} merged, ${result.conflicts.length} conflict.\n`);
|
|
293
294
|
if (result.conflicts.length) {
|
|
294
295
|
process.stdout.write("\nResolve the following, then commit:\n" +
|
|
@@ -306,9 +307,11 @@ async function main(argv) {
|
|
|
306
307
|
}
|
|
307
308
|
else {
|
|
308
309
|
for (const c of shown) {
|
|
309
|
-
|
|
310
|
+
const path = c.fromPath ? `${c.fromPath} -> ${c.path}` : c.path;
|
|
311
|
+
process.stdout.write(` ${c.action.padEnd(9)} ${path} (${c.note})\n`);
|
|
310
312
|
}
|
|
311
|
-
process.stdout.write(`\n${counts.update} update, ${counts.add} add, ${counts.
|
|
313
|
+
process.stdout.write(`\n${counts.update} update, ${counts.add} add, ${counts.move} move, ` +
|
|
314
|
+
`${counts.remove} remove, ${counts.conflict} conflict. ` +
|
|
312
315
|
`Dry-run — nothing was written. Re-run with --apply to write (use --from <dir> for a 3-way merge).\n`);
|
|
313
316
|
}
|
|
314
317
|
}
|
package/dist/lib.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { CreateOptions, CreateResult } from "./create";
|
|
|
3
3
|
export { TEMPLATES, DEFAULT_TEMPLATE, TEMPLATE_NAMES, isKnownTemplate } from "./templates";
|
|
4
4
|
export type { TemplateInfo } from "./templates";
|
|
5
5
|
export { listModules, addModule } from "./add";
|
|
6
|
-
export type { ModuleManifest, AddOptions, AddResult } from "./add";
|
|
6
|
+
export type { ModuleManifest, ModuleMigration, ModulePathMove, ModuleTextReplacement, AddOptions, AddResult, } from "./add";
|
|
7
7
|
export { status, diff, doctor, SUPPORTED_FRAMEWORKS } from "./inspect";
|
|
8
8
|
export type { StatusReport, DoctorFinding } from "./inspect";
|
|
9
9
|
export { planUpdate, summarize } from "./update";
|
package/dist/lockfile.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `podo update` can rebuild the same inputs and detect user edits. See
|
|
4
4
|
* ADR-0009 (update mechanism) and ADR-0010 (ownership model).
|
|
5
5
|
*/
|
|
6
|
-
export declare const LOCK_SCHEMA_VERSION =
|
|
6
|
+
export declare const LOCK_SCHEMA_VERSION = 2;
|
|
7
7
|
/** File ownership tier. See ADR-0010. */
|
|
8
8
|
export type Tier = "managed" | "assembled" | "owned";
|
|
9
9
|
export interface ManifestModule {
|
|
@@ -16,11 +16,14 @@ export interface ManifestModule {
|
|
|
16
16
|
packageName?: string;
|
|
17
17
|
/** Installed external-module version last applied to this project. */
|
|
18
18
|
moduleVersion?: string;
|
|
19
|
+
/** Module-local update migrations already applied to this project. */
|
|
20
|
+
appliedMigrations?: string[];
|
|
19
21
|
}
|
|
20
22
|
export interface ManifestModuleInput {
|
|
21
23
|
name: string;
|
|
22
24
|
packageName?: string;
|
|
23
25
|
moduleVersion?: string;
|
|
26
|
+
appliedMigrations?: string[];
|
|
24
27
|
}
|
|
25
28
|
export interface PodokitManifest {
|
|
26
29
|
schemaVersion: number;
|
package/dist/lockfile.js
CHANGED
|
@@ -24,7 +24,7 @@ const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
|
24
24
|
* `podo update` can rebuild the same inputs and detect user edits. See
|
|
25
25
|
* ADR-0009 (update mechanism) and ADR-0010 (ownership model).
|
|
26
26
|
*/
|
|
27
|
-
exports.LOCK_SCHEMA_VERSION =
|
|
27
|
+
exports.LOCK_SCHEMA_VERSION = 2;
|
|
28
28
|
const PODOKIT_DIR = ".podokit";
|
|
29
29
|
const MANIFEST_FILE = "manifest.json";
|
|
30
30
|
const FILES_LOCK_FILE = "files.lock";
|
|
@@ -66,6 +66,8 @@ exports.DEFAULT_OWNED_GLOBS = [
|
|
|
66
66
|
".env.docker",
|
|
67
67
|
"infra/traefik/**",
|
|
68
68
|
".podokit/dev.json",
|
|
69
|
+
// Optional application-owned Playwright device/browser projects.
|
|
70
|
+
"tests/playwright.projects.cjs",
|
|
69
71
|
];
|
|
70
72
|
const TEXT_EXT = /\.(json|md|ts|tsx|js|mjs|cjs|css|html|yml|yaml|txt|env|svelte|gitignore|example)$/i;
|
|
71
73
|
function podokitDir(projectRoot) {
|
|
@@ -83,11 +85,11 @@ function readManifest(projectRoot) {
|
|
|
83
85
|
}
|
|
84
86
|
function writeManifest(projectRoot, manifest) {
|
|
85
87
|
(0, node_fs_1.mkdirSync)(podokitDir(projectRoot), { recursive: true });
|
|
86
|
-
(0, node_fs_1.writeFileSync)(manifestPath(projectRoot), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
88
|
+
(0, node_fs_1.writeFileSync)(manifestPath(projectRoot), `${JSON.stringify({ ...manifest, schemaVersion: exports.LOCK_SCHEMA_VERSION }, null, 2)}\n`);
|
|
87
89
|
}
|
|
88
90
|
function writeFilesLock(projectRoot, lock) {
|
|
89
91
|
(0, node_fs_1.mkdirSync)(podokitDir(projectRoot), { recursive: true });
|
|
90
|
-
(0, node_fs_1.writeFileSync)(filesLockPath(projectRoot), `${JSON.stringify(lock, null, 2)}\n`);
|
|
92
|
+
(0, node_fs_1.writeFileSync)(filesLockPath(projectRoot), `${JSON.stringify({ ...lock, schemaVersion: exports.LOCK_SCHEMA_VERSION }, null, 2)}\n`);
|
|
91
93
|
}
|
|
92
94
|
/** Read the PodoKit CLI's own version to stamp into the manifest. */
|
|
93
95
|
function podokitVersion() {
|
|
@@ -242,6 +244,7 @@ function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnaps
|
|
|
242
244
|
addedWith: stampedWith,
|
|
243
245
|
packageName: module.packageName,
|
|
244
246
|
moduleVersion: module.moduleVersion,
|
|
247
|
+
appliedMigrations: module.appliedMigrations,
|
|
245
248
|
});
|
|
246
249
|
known.add(module.name);
|
|
247
250
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { VfsTree } from "@podosoft/podokit-template-engine";
|
|
2
|
+
import { type ModuleMigration } from "./add";
|
|
3
|
+
import type { FilesLock, ManifestModule, Tier } from "./lockfile";
|
|
4
|
+
export interface PendingModuleMigration {
|
|
5
|
+
module: string;
|
|
6
|
+
migration: ModuleMigration;
|
|
7
|
+
}
|
|
8
|
+
export interface PlannedPathMove {
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
tier: Tier;
|
|
12
|
+
content: Buffer;
|
|
13
|
+
}
|
|
14
|
+
export interface ModuleMigrationPlan {
|
|
15
|
+
pending: PendingModuleMigration[];
|
|
16
|
+
moves: PlannedPathMove[];
|
|
17
|
+
conflicts: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function collectPendingMigrations(projectRoot: string, templatesDir: string, modules: ManifestModule[]): PendingModuleMigration[];
|
|
20
|
+
export declare function planModuleMigrations(projectRoot: string, targetTree: VfsTree, lock: FilesLock, pending: PendingModuleMigration[]): ModuleMigrationPlan;
|
|
21
|
+
export declare function applyModuleMigrations(projectRoot: string, plan: ModuleMigrationPlan): {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
}[];
|
|
25
|
+
export declare function migrateConfiguredPath(path: string, pending: PendingModuleMigration[]): string;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectPendingMigrations = collectPendingMigrations;
|
|
4
|
+
exports.planModuleMigrations = planModuleMigrations;
|
|
5
|
+
exports.applyModuleMigrations = applyModuleMigrations;
|
|
6
|
+
exports.migrateConfiguredPath = migrateConfiguredPath;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const add_1 = require("./add");
|
|
11
|
+
function treeBuffer(tree, path) {
|
|
12
|
+
const file = tree.get(path);
|
|
13
|
+
if (!file)
|
|
14
|
+
return null;
|
|
15
|
+
return Buffer.isBuffer(file.content) ? file.content : Buffer.from(file.content);
|
|
16
|
+
}
|
|
17
|
+
function assertProjectPath(path, label) {
|
|
18
|
+
if (!path ||
|
|
19
|
+
path.startsWith("/") ||
|
|
20
|
+
path.endsWith("/") ||
|
|
21
|
+
node_path_1.posix.normalize(path) !== path ||
|
|
22
|
+
path.split("/").includes("..")) {
|
|
23
|
+
throw new Error(`${label} must be a normalized project-relative path: ${path}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function filesUnder(projectRoot, path) {
|
|
27
|
+
const absolute = (0, node_path_1.join)(projectRoot, path);
|
|
28
|
+
if (!(0, node_fs_1.existsSync)(absolute))
|
|
29
|
+
return [];
|
|
30
|
+
if (!(0, node_fs_1.statSync)(absolute).isDirectory())
|
|
31
|
+
return [path];
|
|
32
|
+
const files = [];
|
|
33
|
+
const visit = (relative) => {
|
|
34
|
+
for (const entry of (0, node_fs_1.readdirSync)((0, node_path_1.join)(projectRoot, relative), { withFileTypes: true })) {
|
|
35
|
+
const child = `${relative}/${entry.name}`;
|
|
36
|
+
if (entry.isDirectory())
|
|
37
|
+
visit(child);
|
|
38
|
+
else
|
|
39
|
+
files.push(child);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
visit(path);
|
|
43
|
+
return files.sort();
|
|
44
|
+
}
|
|
45
|
+
function destinationFor(path, from, to) {
|
|
46
|
+
return path === from ? to : `${to}${path.slice(from.length)}`;
|
|
47
|
+
}
|
|
48
|
+
function occurrences(content, needle) {
|
|
49
|
+
if (!needle)
|
|
50
|
+
return 0;
|
|
51
|
+
let count = 0;
|
|
52
|
+
let offset = 0;
|
|
53
|
+
while (true) {
|
|
54
|
+
const index = content.indexOf(needle, offset);
|
|
55
|
+
if (index === -1)
|
|
56
|
+
return count;
|
|
57
|
+
count += 1;
|
|
58
|
+
offset = index + needle.length;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function transformedContent(path, content, pending, conflicts) {
|
|
62
|
+
let text = null;
|
|
63
|
+
for (const { module, migration } of pending) {
|
|
64
|
+
for (const replacement of migration.replacements ?? []) {
|
|
65
|
+
if (replacement.path !== path)
|
|
66
|
+
continue;
|
|
67
|
+
text ??= content.toString("utf8");
|
|
68
|
+
const currentCount = occurrences(text, replacement.from);
|
|
69
|
+
if (currentCount === replacement.expected) {
|
|
70
|
+
text = text.split(replacement.from).join(replacement.to);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const replacementCount = occurrences(text, replacement.to);
|
|
74
|
+
if (currentCount === 0 && replacementCount === replacement.expected)
|
|
75
|
+
continue;
|
|
76
|
+
conflicts.push(`${module}:${migration.id} expected ${replacement.expected} occurrence(s) in ` +
|
|
77
|
+
`${replacement.path}, found ${currentCount}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return text === null ? content : Buffer.from(text);
|
|
81
|
+
}
|
|
82
|
+
function matchesPathOrChild(path, prefix) {
|
|
83
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
84
|
+
}
|
|
85
|
+
function collectPendingMigrations(projectRoot, templatesDir, modules) {
|
|
86
|
+
const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
|
|
87
|
+
const pending = [];
|
|
88
|
+
for (const module of modules) {
|
|
89
|
+
const resolved = (0, add_1.resolveModule)(module.packageName ?? module.name, modulesDir, projectRoot);
|
|
90
|
+
if (!resolved)
|
|
91
|
+
continue;
|
|
92
|
+
const manifest = (0, add_1.readModuleManifest)(resolved.dir);
|
|
93
|
+
const applied = new Set(module.appliedMigrations ?? []);
|
|
94
|
+
for (const migration of manifest.migrations ?? []) {
|
|
95
|
+
if (applied.has(migration.id))
|
|
96
|
+
continue;
|
|
97
|
+
if (!migration.id || migration.moves.length === 0) {
|
|
98
|
+
throw new Error(`Module "${module.name}" declares an invalid migration.`);
|
|
99
|
+
}
|
|
100
|
+
for (const move of migration.moves) {
|
|
101
|
+
assertProjectPath(move.from, `${module.name}:${migration.id} move.from`);
|
|
102
|
+
assertProjectPath(move.to, `${module.name}:${migration.id} move.to`);
|
|
103
|
+
if (move.from === move.to) {
|
|
104
|
+
throw new Error(`${module.name}:${migration.id} cannot move a path onto itself.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const replacement of migration.replacements ?? []) {
|
|
108
|
+
assertProjectPath(replacement.path, `${module.name}:${migration.id} replacement.path`);
|
|
109
|
+
if (!replacement.from || replacement.expected < 1) {
|
|
110
|
+
throw new Error(`${module.name}:${migration.id} declares an invalid text replacement.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
pending.push({ module: module.name, migration });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return pending;
|
|
117
|
+
}
|
|
118
|
+
function planModuleMigrations(projectRoot, targetTree, lock, pending) {
|
|
119
|
+
const conflicts = [];
|
|
120
|
+
const moveSources = new Set();
|
|
121
|
+
const moveTargets = new Set();
|
|
122
|
+
const candidates = [];
|
|
123
|
+
for (const { module, migration } of pending) {
|
|
124
|
+
for (const move of migration.moves) {
|
|
125
|
+
const staleTargetPaths = [...targetTree.keys()].filter((path) => matchesPathOrChild(path, move.from));
|
|
126
|
+
if (staleTargetPaths.length) {
|
|
127
|
+
conflicts.push(`${module}:${migration.id} target modules still provide legacy path ${move.from}; ` +
|
|
128
|
+
`upgrade external modules before updating`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
for (const source of filesUnder(projectRoot, move.from)) {
|
|
132
|
+
const target = destinationFor(source, move.from, move.to);
|
|
133
|
+
if (moveSources.has(source)) {
|
|
134
|
+
conflicts.push(`${module}:${migration.id} moves ${source} more than once`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (moveTargets.has(target)) {
|
|
138
|
+
conflicts.push(`${module}:${migration.id} has duplicate destination ${target}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
moveSources.add(source);
|
|
142
|
+
moveTargets.add(target);
|
|
143
|
+
candidates.push({
|
|
144
|
+
from: source,
|
|
145
|
+
to: target,
|
|
146
|
+
tier: lock.files[source]?.tier ?? "owned",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const candidate of candidates) {
|
|
152
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, candidate.to)) && !moveSources.has(candidate.to)) {
|
|
153
|
+
conflicts.push(`cannot move ${candidate.from}: destination already exists at ${candidate.to}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const moves = candidates.map((candidate) => {
|
|
157
|
+
const sourceContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(projectRoot, candidate.from));
|
|
158
|
+
const locked = lock.files[candidate.from];
|
|
159
|
+
const targetContent = treeBuffer(targetTree, candidate.to);
|
|
160
|
+
const content = locked && locked.outHash === hashBuffer(sourceContent) && targetContent
|
|
161
|
+
? targetContent
|
|
162
|
+
: transformedContent(candidate.to, sourceContent, pending, conflicts);
|
|
163
|
+
return { ...candidate, content };
|
|
164
|
+
});
|
|
165
|
+
for (const { module, migration } of pending) {
|
|
166
|
+
for (const replacement of migration.replacements ?? []) {
|
|
167
|
+
if (moves.some((move) => move.to === replacement.path))
|
|
168
|
+
continue;
|
|
169
|
+
const target = (0, node_path_1.join)(projectRoot, replacement.path);
|
|
170
|
+
if (!(0, node_fs_1.existsSync)(target))
|
|
171
|
+
continue;
|
|
172
|
+
transformedContent(replacement.path, (0, node_fs_1.readFileSync)(target), [{ module, migration }], conflicts);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { pending, moves, conflicts: [...new Set(conflicts)] };
|
|
176
|
+
}
|
|
177
|
+
function hashBuffer(content) {
|
|
178
|
+
return `sha256:${(0, node_crypto_1.createHash)("sha256").update(content).digest("hex")}`;
|
|
179
|
+
}
|
|
180
|
+
function pruneEmptyDirectories(projectRoot, relative) {
|
|
181
|
+
let current = relative;
|
|
182
|
+
while (current && current !== "." && current !== "/") {
|
|
183
|
+
try {
|
|
184
|
+
(0, node_fs_1.rmdirSync)((0, node_path_1.join)(projectRoot, current));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
current = (0, node_path_1.dirname)(current);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function applyModuleMigrations(projectRoot, plan) {
|
|
193
|
+
if (plan.conflicts.length)
|
|
194
|
+
return [];
|
|
195
|
+
const moved = [];
|
|
196
|
+
const replacementByPath = new Map();
|
|
197
|
+
for (const move of plan.moves)
|
|
198
|
+
replacementByPath.set(move.to, move.content);
|
|
199
|
+
for (const { module, migration } of plan.pending) {
|
|
200
|
+
for (const replacement of migration.replacements ?? []) {
|
|
201
|
+
if (replacementByPath.has(replacement.path))
|
|
202
|
+
continue;
|
|
203
|
+
const target = (0, node_path_1.join)(projectRoot, replacement.path);
|
|
204
|
+
if (!(0, node_fs_1.existsSync)(target))
|
|
205
|
+
continue;
|
|
206
|
+
replacementByPath.set(replacement.path, transformedContent(replacement.path, (0, node_fs_1.readFileSync)(target), [{ module, migration }], []));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const move of plan.moves) {
|
|
210
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)((0, node_path_1.join)(projectRoot, move.to)), { recursive: true });
|
|
211
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(projectRoot, move.to), replacementByPath.get(move.to) ?? move.content);
|
|
212
|
+
(0, node_fs_1.rmSync)((0, node_path_1.join)(projectRoot, move.from));
|
|
213
|
+
pruneEmptyDirectories(projectRoot, (0, node_path_1.dirname)(move.from));
|
|
214
|
+
moved.push({ from: move.from, to: move.to });
|
|
215
|
+
}
|
|
216
|
+
for (const [path, content] of replacementByPath) {
|
|
217
|
+
if (plan.moves.some((move) => move.to === path))
|
|
218
|
+
continue;
|
|
219
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(projectRoot, path), content);
|
|
220
|
+
}
|
|
221
|
+
return moved;
|
|
222
|
+
}
|
|
223
|
+
function migrateConfiguredPath(path, pending) {
|
|
224
|
+
let migrated = path;
|
|
225
|
+
for (const { migration } of pending) {
|
|
226
|
+
for (const move of migration.moves) {
|
|
227
|
+
if (matchesPathOrChild(migrated, move.from)) {
|
|
228
|
+
migrated = destinationFor(migrated, move.from, move.to);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return migrated;
|
|
233
|
+
}
|
|
@@ -17,7 +17,9 @@ WORKDIR /app
|
|
|
17
17
|
COPY package.json ./
|
|
18
18
|
COPY apps/api/package.json ./apps/api/package.json
|
|
19
19
|
COPY apps/web/package.json ./apps/web/package.json
|
|
20
|
-
|
|
20
|
+
ARG PODOKIT_NPM_REGISTRY=""
|
|
21
|
+
RUN if [ -n "$PODOKIT_NPM_REGISTRY" ]; then npm config set registry "$PODOKIT_NPM_REGISTRY"; fi \
|
|
22
|
+
&& npm install
|
|
21
23
|
|
|
22
24
|
# Baked source (kept in sync during development by `docker compose watch`).
|
|
23
25
|
COPY . .
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"migration:generate": "npm run typeorm -- migration:generate"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@podosoft/podokit-contracts": "^0.
|
|
17
|
+
"@podosoft/podokit-contracts": "^0.4.0",
|
|
18
18
|
"@nestjs/common": "^11.1.27",
|
|
19
19
|
"@nestjs/config": "^4.0.4",
|
|
20
20
|
"@nestjs/core": "^11.1.27",
|
|
@@ -16,6 +16,9 @@ RUN npm run build --workspace=apps/web
|
|
|
16
16
|
FROM node:22-alpine AS runtime
|
|
17
17
|
WORKDIR /app
|
|
18
18
|
ENV NODE_ENV=production
|
|
19
|
+
# Keep the adapter-node proxy above the 2 MiB application upload policy so the
|
|
20
|
+
# API can return stable validation codes after accounting for multipart framing.
|
|
21
|
+
ENV BODY_SIZE_LIMIT=3M
|
|
19
22
|
RUN addgroup -S app && adduser -S app -G app
|
|
20
23
|
COPY --from=build /app/node_modules ./node_modules
|
|
21
24
|
COPY --from=deps /app/apps/web ./apps/web
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
<html lang="en" class="h-full">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
|
+
<!-- Add viewport-fit=cover only when the application-owned layout also
|
|
6
|
+
handles env(safe-area-inset-*) padding for edge-to-edge content. -->
|
|
5
7
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
8
|
<title>{{projectName}}</title>
|
|
7
9
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/input.svelte
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
files = $bindable(),
|
|
17
17
|
class: className,
|
|
18
18
|
"data-slot": dataSlot = "input",
|
|
19
|
+
onchange,
|
|
19
20
|
...restProps
|
|
20
21
|
}: Props = $props();
|
|
21
22
|
</script>
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
)}
|
|
31
32
|
type="file"
|
|
32
33
|
bind:files
|
|
33
|
-
|
|
34
|
+
{onchange}
|
|
34
35
|
{...restProps}
|
|
35
36
|
/>
|
|
36
37
|
{:else}
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
)}
|
|
44
45
|
{type}
|
|
45
46
|
bind:value
|
|
47
|
+
{onchange}
|
|
46
48
|
{...restProps}
|
|
47
49
|
/>
|
|
48
50
|
{/if}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import * as Card from "$lib/components/ui/card";
|
|
4
4
|
import { api } from "$lib/api";
|
|
5
5
|
import { ApiError } from "@podosoft/podokit-api-client";
|
|
6
|
+
// podokit:begin:landing-imports
|
|
7
|
+
// podokit:end:landing-imports
|
|
6
8
|
|
|
7
9
|
type Health = { status: string } | { error: string };
|
|
8
10
|
|
|
@@ -17,6 +19,9 @@
|
|
|
17
19
|
}
|
|
18
20
|
</script>
|
|
19
21
|
|
|
22
|
+
<!-- podokit:begin:landing-actions -->
|
|
23
|
+
<!-- podokit:end:landing-actions -->
|
|
24
|
+
|
|
20
25
|
<main class="mx-auto flex min-h-full max-w-2xl flex-col gap-6 p-8">
|
|
21
26
|
<div>
|
|
22
27
|
<h1 class="text-3xl font-bold">{{projectName}}</h1>
|
|
@@ -41,6 +41,8 @@ services:
|
|
|
41
41
|
build:
|
|
42
42
|
context: .
|
|
43
43
|
dockerfile: Dockerfile.dev
|
|
44
|
+
args:
|
|
45
|
+
PODOKIT_NPM_REGISTRY: ${PODOKIT_NPM_REGISTRY:-}
|
|
44
46
|
command: npm run dev -w {{projectName}}-web -- --host 0.0.0.0
|
|
45
47
|
working_dir: /app
|
|
46
48
|
env_file: .env.docker
|
|
@@ -64,6 +66,8 @@ services:
|
|
|
64
66
|
build:
|
|
65
67
|
context: .
|
|
66
68
|
dockerfile: Dockerfile.dev
|
|
69
|
+
args:
|
|
70
|
+
PODOKIT_NPM_REGISTRY: ${PODOKIT_NPM_REGISTRY:-}
|
|
67
71
|
init: true
|
|
68
72
|
command: npm run dev -w {{projectName}}-api
|
|
69
73
|
working_dir: /app
|
|
@@ -87,6 +91,8 @@ services:
|
|
|
87
91
|
build:
|
|
88
92
|
context: .
|
|
89
93
|
dockerfile: Dockerfile.dev
|
|
94
|
+
args:
|
|
95
|
+
PODOKIT_NPM_REGISTRY: ${PODOKIT_NPM_REGISTRY:-}
|
|
90
96
|
init: true
|
|
91
97
|
command: npm run dev:worker -w {{projectName}}-api
|
|
92
98
|
working_dir: /app
|
|
@@ -13,6 +13,8 @@ POSTGRES_DB=podokit
|
|
|
13
13
|
# Web -> API boundary (server-side only; not exposed to the browser)
|
|
14
14
|
BACKEND_INTERNAL_URL=http://localhost:5002
|
|
15
15
|
CORS_ORIGIN=http://localhost:5001
|
|
16
|
+
# adapter-node request limit; leave room for a 2 MiB multipart upload
|
|
17
|
+
BODY_SIZE_LIMIT=3M
|
|
16
18
|
|
|
17
19
|
# ── Optional / development ───────────────────────────────────────────
|
|
18
20
|
# Redis — only needed by cache/queue modules (redis, bullmq, rate-limit,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { defineConfig, devices } from "@playwright/test";
|
|
2
|
+
import { loadPlaywrightProjects } from "./playwright.extensions";
|
|
2
3
|
|
|
3
4
|
// The suite runs against a live stack: web on E2E_BASE_URL (proxying /api/* to
|
|
4
5
|
// the API). Start it first (see docs/testing.md) — locally via the dev harness,
|
|
5
6
|
// in CI via scripts/e2e-ci.mjs. Two projects: `api` (request-only, backend e2e)
|
|
6
7
|
// and `ui` (chromium, pages). Files are routed by suffix (*.api/*.ui.spec.ts).
|
|
7
8
|
const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
9
|
+
const coreProjects = [
|
|
10
|
+
{ name: "api", testMatch: /.*\.api\.spec\.ts/ },
|
|
11
|
+
{ name: "ui", testMatch: /.*\.ui\.spec\.ts/, use: { ...devices["Desktop Chrome"] } },
|
|
12
|
+
];
|
|
8
13
|
|
|
9
14
|
export default defineConfig({
|
|
10
15
|
testDir: ".",
|
|
@@ -18,8 +23,5 @@ export default defineConfig({
|
|
|
18
23
|
trace: "on-first-retry",
|
|
19
24
|
screenshot: "only-on-failure",
|
|
20
25
|
},
|
|
21
|
-
projects: [
|
|
22
|
-
{ name: "api", testMatch: /.*\.api\.spec\.ts/ },
|
|
23
|
-
{ name: "ui", testMatch: /.*\.ui\.spec\.ts/, use: { ...devices["Desktop Chrome"] } },
|
|
24
|
-
],
|
|
26
|
+
projects: [...coreProjects, ...loadPlaywrightProjects(coreProjects)],
|
|
25
27
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { TestConfig } from "@playwright/test";
|
|
5
|
+
|
|
6
|
+
type Projects = NonNullable<TestConfig["projects"]>;
|
|
7
|
+
|
|
8
|
+
const requireFromTests = createRequire(__filename);
|
|
9
|
+
|
|
10
|
+
function projectName(project: unknown): string | null {
|
|
11
|
+
if (typeof project !== "object" || project === null || !("name" in project)) return null;
|
|
12
|
+
return typeof project.name === "string" && project.name.length > 0 ? project.name : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Merge application-owned Playwright projects into PodoKit's managed config.
|
|
17
|
+
* The optional CommonJS file stays owned so applications can add devices and
|
|
18
|
+
* browsers without creating a 3-way merge hotspot in playwright.config.ts.
|
|
19
|
+
*/
|
|
20
|
+
export function loadPlaywrightProjects(coreProjects: Projects): Projects {
|
|
21
|
+
const extensionPath = join(__dirname, "playwright.projects.cjs");
|
|
22
|
+
if (!existsSync(extensionPath)) return [];
|
|
23
|
+
|
|
24
|
+
const loaded = requireFromTests(extensionPath) as unknown;
|
|
25
|
+
if (!Array.isArray(loaded)) {
|
|
26
|
+
throw new Error("tests/playwright.projects.cjs must export an array of Playwright projects.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const names = new Set(
|
|
30
|
+
coreProjects.map((project) => projectName(project)).filter((name): name is string => name !== null),
|
|
31
|
+
);
|
|
32
|
+
for (const project of loaded) {
|
|
33
|
+
const name = projectName(project);
|
|
34
|
+
if (!name) {
|
|
35
|
+
throw new Error("Every project in tests/playwright.projects.cjs must have a non-empty name.");
|
|
36
|
+
}
|
|
37
|
+
if (names.has(name)) {
|
|
38
|
+
throw new Error(`Duplicate Playwright project name: ${name}`);
|
|
39
|
+
}
|
|
40
|
+
names.add(name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return loaded as Projects;
|
|
44
|
+
}
|