@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.
Files changed (86) hide show
  1. package/README.md +3 -0
  2. package/dist/add.d.ts +27 -1
  3. package/dist/add.js +3 -1
  4. package/dist/index.js +6 -3
  5. package/dist/lib.d.ts +1 -1
  6. package/dist/lockfile.d.ts +4 -1
  7. package/dist/lockfile.js +6 -3
  8. package/dist/module-migrations.d.ts +25 -0
  9. package/dist/module-migrations.js +233 -0
  10. package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +3 -1
  11. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -1
  12. package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +3 -0
  13. package/dist/templates/fullstack-nest-svelte/apps/web/package.json +1 -1
  14. package/dist/templates/fullstack-nest-svelte/apps/web/src/app.html +2 -0
  15. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/input.svelte +3 -1
  16. package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +5 -0
  17. package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +6 -0
  18. package/dist/templates/fullstack-nest-svelte/dot-env.example +2 -0
  19. package/dist/templates/fullstack-nest-svelte/infra/k3s/configmap.yaml +1 -0
  20. package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +6 -4
  21. package/dist/templates/fullstack-nest-svelte/tests/playwright.extensions.ts +44 -0
  22. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image-upload.interceptor.ts +39 -0
  23. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.controller.ts +73 -0
  24. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.module.ts +9 -0
  25. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +43 -0
  26. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +110 -0
  27. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.spec.ts +79 -0
  28. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.ts +74 -0
  29. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +1 -1
  30. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +35 -4
  31. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +155 -2
  32. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +33 -4
  33. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
  34. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +13 -0
  35. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +13 -0
  36. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +78 -0
  37. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/+layout.server.ts +8 -0
  38. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/+layout.svelte +2 -2
  39. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.svelte +18 -10
  40. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +2 -47
  41. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+page.svelte +4 -0
  42. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +11 -6
  43. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +18 -8
  44. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +96 -0
  45. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +15 -9
  46. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +1 -0
  47. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +83 -0
  48. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +13 -10
  49. package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +69 -3
  50. package/dist/templates/modules/admin-dashboard/files/tests/seed.teardown.ts +46 -0
  51. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +64 -1
  52. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +16 -0
  53. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +13 -0
  54. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +3 -6
  55. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +4 -7
  56. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +4 -1
  57. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +34 -28
  58. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +29 -39
  59. package/dist/templates/modules/admin-dashboard/module.manifest.json +74 -11
  60. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +5 -1
  61. package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.spec.ts +42 -0
  62. package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.ts +27 -0
  63. package/dist/templates/modules/auth/module.manifest.json +1 -1
  64. package/dist/templates/todo/Dockerfile.dev +3 -1
  65. package/dist/templates/todo/apps/api/package.json +1 -1
  66. package/dist/templates/todo/apps/web/package.json +1 -1
  67. package/dist/templates/todo/compose.dev.yaml +6 -0
  68. package/dist/templates/todo/tests/playwright.config.ts +6 -4
  69. package/dist/templates/todo/tests/playwright.extensions.ts +44 -0
  70. package/dist/update.d.ts +8 -1
  71. package/dist/update.js +85 -25
  72. package/package.json +1 -1
  73. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/{app-sidebar.svelte → admin-sidebar.svelte} +0 -0
  74. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/+page.svelte +0 -0
  75. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.server.ts +0 -0
  76. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.svelte +0 -0
  77. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.server.ts +0 -0
  78. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.svelte +0 -0
  79. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.server.ts +0 -0
  80. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.svelte +0 -0
  81. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.server.ts +0 -0
  82. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.svelte +0 -0
  83. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.server.ts +0 -0
  84. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.svelte +0 -0
  85. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/general-settings.svelte +0 -0
  86. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.server.ts +0 -0
@@ -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
+ }
package/dist/update.d.ts CHANGED
@@ -5,9 +5,11 @@ import { type Tier } from "./lockfile";
5
5
  * working copy, classifying each file. Applying the plan (writing files, 3-way
6
6
  * merging) is a later step (see ADR-0009).
7
7
  */
8
- export type Action = "update" | "merge" | "conflict" | "add" | "remove" | "skip" | "up-to-date";
8
+ export type Action = "update" | "merge" | "conflict" | "add" | "move" | "remove" | "skip" | "up-to-date";
9
9
  export interface FileChange {
10
10
  path: string;
11
+ /** Previous path when this change is an update migration move. */
12
+ fromPath?: string;
11
13
  tier: Tier;
12
14
  action: Action;
13
15
  /** Why the action was chosen (for the report). */
@@ -33,6 +35,11 @@ export interface ApplyOptions {
33
35
  }
34
36
  export interface ApplyResult {
35
37
  written: string[];
38
+ /** Files relocated by module update migrations. */
39
+ moved: {
40
+ from: string;
41
+ to: string;
42
+ }[];
36
43
  removed: string[];
37
44
  /** Files 3-way merged cleanly. */
38
45
  merged: string[];
package/dist/update.js CHANGED
@@ -12,6 +12,7 @@ const assemble_1 = require("./assemble");
12
12
  const lockfile_1 = require("./lockfile");
13
13
  const inspect_1 = require("./inspect");
14
14
  const add_1 = require("./add");
15
+ const module_migrations_1 = require("./module-migrations");
15
16
  function treeText(tree, path) {
16
17
  const file = tree.get(path);
17
18
  if (!file)
@@ -64,26 +65,50 @@ function planUpdate(projectRoot, templatesDir) {
64
65
  });
65
66
  const managedOverrides = targetManagedOverrides(projectRoot, templatesDir, manifest.modules, manifest.managedOverrides);
66
67
  const ownedGlobs = targetOwnedGlobs(projectRoot, templatesDir, manifest.modules, manifest.ownedGlobs);
67
- const changes = [];
68
+ const pendingMigrations = (0, module_migrations_1.collectPendingMigrations)(projectRoot, templatesDir, manifest.modules);
69
+ const migrationPlan = (0, module_migrations_1.planModuleMigrations)(projectRoot, newTree, lock, pendingMigrations);
70
+ const changes = migrationPlan.moves.map((move) => ({
71
+ path: move.to,
72
+ fromPath: move.from,
73
+ tier: move.tier,
74
+ action: "move",
75
+ note: "module path migration",
76
+ }));
77
+ for (const conflict of migrationPlan.conflicts) {
78
+ changes.push({
79
+ path: "module migration",
80
+ tier: "managed",
81
+ action: "conflict",
82
+ note: conflict,
83
+ });
84
+ }
85
+ const movedSources = new Set(migrationPlan.moves.map((move) => move.from));
86
+ const movedTargets = new Set(migrationPlan.moves.map((move) => move.to));
68
87
  const paths = new Set([...newTree.keys(), ...Object.keys(lock.files)]);
69
88
  for (const path of [...paths].sort()) {
89
+ if (movedSources.has(path) || movedTargets.has(path))
90
+ continue;
70
91
  const locked = lock.files[path];
71
92
  const newText = treeText(newTree, path);
72
93
  const disk = diskContent(projectRoot, path);
73
- // A newer module can explicitly take responsibility for a path that older
74
- // projects classified under a broad owned glob (notably generated skills).
75
- const managedByTarget = managedOverrides.some((glob) => (0, lockfile_1.matchGlob)(path, glob));
76
- const ownedByTarget = ownedGlobs.some((glob) => (0, lockfile_1.matchGlob)(path, glob));
77
- const tier = managedByTarget
78
- ? "managed"
79
- : ownedByTarget
80
- ? "owned"
81
- : locked?.tier ??
82
- (newText !== null
83
- ? (0, lockfile_1.classifyTier)(path, newText, ownedGlobs, managedOverrides)
84
- : "managed");
94
+ // Use the same ownership precedence as lockfile classification. In
95
+ // particular, an exact path recorded by `podo eject` must keep winning over
96
+ // a module's managed override, while managed overrides may still reclaim a
97
+ // path covered only by a broad owned glob.
98
+ const controlledByTarget = managedOverrides.some((glob) => (0, lockfile_1.matchGlob)(path, glob)) ||
99
+ ownedGlobs.some((glob) => (0, lockfile_1.matchGlob)(path, glob));
100
+ const classified = (0, lockfile_1.classifyTier)(path, newText ?? disk ?? "", ownedGlobs, managedOverrides);
101
+ const tier = controlledByTarget ? classified : locked?.tier ?? classified;
85
102
  if (tier === "owned") {
86
- changes.push({ path, tier, action: "skip", note: "owned — never modified" });
103
+ const explicitlyOwned = ownedGlobs.some((glob) => !glob.includes("*") && (0, lockfile_1.matchGlob)(path, glob));
104
+ changes.push({
105
+ path,
106
+ tier,
107
+ action: "skip",
108
+ note: explicitlyOwned && disk === null && newText !== null
109
+ ? "explicitly owned — missing or relocated; not restored"
110
+ : "owned — never modified",
111
+ });
87
112
  continue;
88
113
  }
89
114
  // File removed upstream.
@@ -232,6 +257,19 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
232
257
  });
233
258
  const managedOverrides = targetManagedOverrides(projectRoot, templatesDir, modules, manifest.managedOverrides);
234
259
  const ownedGlobs = targetOwnedGlobs(projectRoot, templatesDir, modules, manifest.ownedGlobs);
260
+ const pendingMigrations = (0, module_migrations_1.collectPendingMigrations)(projectRoot, templatesDir, modules);
261
+ const migrationPlan = (0, module_migrations_1.planModuleMigrations)(projectRoot, newTree, previousLock, pendingMigrations);
262
+ if (migrationPlan.conflicts.length) {
263
+ throw new Error(`Cannot apply module path migration:\n${migrationPlan.conflicts
264
+ .map((conflict) => ` - ${conflict}`)
265
+ .join("\n")}`);
266
+ }
267
+ const migratedOwnedGlobs = [
268
+ ...new Set(ownedGlobs.map((glob) => (0, module_migrations_1.migrateConfiguredPath)(glob, pendingMigrations))),
269
+ ];
270
+ const migratedManagedOverrides = [
271
+ ...new Set(managedOverrides.map((glob) => (0, module_migrations_1.migrateConfiguredPath)(glob, pendingMigrations))),
272
+ ];
235
273
  const needsMergeBase = plan.changes.some((change) => change.action === "conflict");
236
274
  let oldTree = null;
237
275
  if (options.oldTemplatesDir && needsMergeBase) {
@@ -250,7 +288,13 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
250
288
  (0, node_fs_1.rmSync)(previousModules.root, { recursive: true, force: true });
251
289
  }
252
290
  }
253
- const result = { written: [], removed: [], merged: [], conflicts: [] };
291
+ const result = {
292
+ written: [],
293
+ moved: (0, module_migrations_1.applyModuleMigrations)(projectRoot, migrationPlan),
294
+ removed: [],
295
+ merged: [],
296
+ conflicts: [],
297
+ };
254
298
  for (const change of plan.changes) {
255
299
  const newText = treeText(newTree, change.path);
256
300
  if (change.action === "update" || change.action === "add") {
@@ -280,22 +324,37 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
280
324
  // Keep the assembled target as the managed baseline. A merged working file
281
325
  // intentionally remains drifted so a future update performs another 3-way
282
326
  // merge instead of treating the user's lines as clean generated output.
283
- (0, lockfile_1.writeFilesLock)(projectRoot, updatedFilesLock(projectRoot, newTree, previousLock, ownedGlobs, managedOverrides));
327
+ (0, lockfile_1.writeFilesLock)(projectRoot, updatedFilesLock(projectRoot, newTree, previousLock, migratedOwnedGlobs, migratedManagedOverrides));
284
328
  const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
329
+ const appliedMigrations = new Map();
330
+ for (const pending of pendingMigrations) {
331
+ const ids = appliedMigrations.get(pending.module) ?? [];
332
+ ids.push(pending.migration.id);
333
+ appliedMigrations.set(pending.module, ids);
334
+ }
285
335
  const refreshedModules = manifest.modules.map((module) => {
286
336
  const resolved = (0, add_1.resolveModule)(module.packageName ?? module.name, modulesDir, projectRoot);
287
- return resolved?.packageName
288
- ? {
289
- ...module,
290
- packageName: resolved.packageName,
291
- moduleVersion: resolved.moduleVersion,
292
- }
293
- : module;
337
+ const applied = [
338
+ ...new Set([
339
+ ...(module.appliedMigrations ?? []),
340
+ ...(appliedMigrations.get(module.name) ?? []),
341
+ ]),
342
+ ];
343
+ return {
344
+ ...module,
345
+ ...(resolved?.packageName
346
+ ? {
347
+ packageName: resolved.packageName,
348
+ moduleVersion: resolved.moduleVersion,
349
+ }
350
+ : {}),
351
+ ...(applied.length ? { appliedMigrations: applied } : {}),
352
+ };
294
353
  });
295
354
  (0, lockfile_1.writeManifest)(projectRoot, {
296
355
  ...manifest,
297
- ownedGlobs,
298
- managedOverrides,
356
+ ownedGlobs: migratedOwnedGlobs,
357
+ managedOverrides: migratedManagedOverrides,
299
358
  modules: refreshedModules,
300
359
  podokitVersion: (0, lockfile_1.podokitVersion)(),
301
360
  });
@@ -308,6 +367,7 @@ function summarize(plan) {
308
367
  merge: 0,
309
368
  conflict: 0,
310
369
  add: 0,
370
+ move: 0,
311
371
  remove: 0,
312
372
  skip: 0,
313
373
  "up-to-date": 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podosoft/podokit",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "An opinionated but extensible starter toolkit and CLI for full-stack TypeScript apps with NestJS, SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {