create-momentum-app 0.5.2 → 0.5.4

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/index.cjs CHANGED
@@ -34,7 +34,10 @@ var import_fs_extra = __toESM(require("fs-extra"));
34
34
  var import_picocolors = __toESM(require("picocolors"));
35
35
  var TEMPLATE_EXT = ".tmpl";
36
36
  function getTemplatesDir() {
37
- return import_node_path.default.resolve(__dirname, "templates");
37
+ const distPath = import_node_path.default.resolve(__dirname, "templates");
38
+ if (import_fs_extra.default.existsSync(distPath))
39
+ return distPath;
40
+ return import_node_path.default.resolve(__dirname, "..", "templates");
38
41
  }
39
42
  function interpolate(content, vars) {
40
43
  let result = content;
@@ -163,7 +166,8 @@ Directory "${projectName}" already exists.`));
163
166
  process.exit(1);
164
167
  }
165
168
  const templatesDir = getTemplatesDir();
166
- const pkgJson = import_fs_extra.default.readJsonSync(import_node_path.default.resolve(__dirname, "package.json"));
169
+ const pkgJsonPath = import_fs_extra.default.existsSync(import_node_path.default.resolve(__dirname, "package.json")) ? import_node_path.default.resolve(__dirname, "package.json") : import_node_path.default.resolve(__dirname, "..", "package.json");
170
+ const pkgJson = import_fs_extra.default.readJsonSync(pkgJsonPath);
167
171
  const packageVersion = pkgJson.version ?? "0.0.1";
168
172
  const vars = {
169
173
  projectName,
@@ -171,9 +175,9 @@ Directory "${projectName}" already exists.`));
171
175
  databaseType: database,
172
176
  dbImport: database === "postgres" ? "import { postgresAdapter } from '@momentumcms/db-drizzle';" : "import { sqliteAdapter } from '@momentumcms/db-drizzle';",
173
177
  dbAdapter: database === "postgres" ? `postgresAdapter({
174
- connectionString: process.env['DATABASE_URL'] ?? 'postgresql://postgres:postgres@localhost:5432/momentum',
178
+ connectionString: process.env['DATABASE_URL'] ?? 'postgresql://postgres:postgres@localhost:5432/${projectName}',
175
179
  })` : `sqliteAdapter({
176
- filename: process.env['DATABASE_PATH'] ?? './data/momentum.db',
180
+ filename: process.env['DATABASE_PATH'] ?? './data/${projectName}.db',
177
181
  })`,
178
182
  dbPoolSetup: database === "postgres" ? `import type { PostgresAdapterWithRaw } from '@momentumcms/db-drizzle';
179
183
 
@@ -182,7 +186,7 @@ const pool = (dbAdapter as PostgresAdapterWithRaw).getPool();` : "",
182
186
  authDbConfig: database === "postgres" ? "db: { type: 'postgres', pool }," : "db: { type: 'sqlite', database: dbAdapter.getRawDatabase() },",
183
187
  dbPackage: database === "postgres" ? '"pg": "^8.18.0"' : '"better-sqlite3": "^12.6.0"',
184
188
  dbDevPackage: database === "postgres" ? "" : '"@types/better-sqlite3": "^7.6.13",',
185
- envDbVar: database === "postgres" ? "DATABASE_URL=postgresql://postgres:postgres@localhost:5432/momentum" : "DATABASE_PATH=./data/momentum.db",
189
+ envDbVar: database === "postgres" ? `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${projectName}` : `DATABASE_PATH=./data/${projectName}.db`,
186
190
  defaultPort: "4200",
187
191
  externalDependencies: [
188
192
  database === "postgres" ? '"pg", "pg-native"' : '"better-sqlite3"',
@@ -204,7 +208,7 @@ This project uses PostgreSQL via Docker. The database is configured in \`docker-
204
208
  **Connection Details:**
205
209
  - Host: \`localhost\`
206
210
  - Port: \`5432\`
207
- - Database: \`momentum\`
211
+ - Database: \`${projectName}\`
208
212
  - Username: \`postgres\`
209
213
  - Password: \`postgres\`
210
214
 
@@ -226,7 +230,7 @@ docker compose logs -f postgres
226
230
 
227
231
  You can also use an external PostgreSQL instance by updating \`DATABASE_URL\` in \`.env\`.` : `### SQLite
228
232
 
229
- This project uses SQLite with the database file at \`./data/momentum.db\`.
233
+ This project uses SQLite with the database file at \`./data/${projectName}.db\`.
230
234
  The database is automatically created on first run - no setup required.`
231
235
  };
232
236
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-momentum-app",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Create a new Momentum CMS application",
5
5
  "license": "MIT",
6
6
  "author": "Momentum CMS Contributors",
@@ -11,13 +11,13 @@ const dbAdapter = {{dbAdapter}};
11
11
 
12
12
  {{dbPoolSetup}}
13
13
 
14
- const authBaseURL =
15
- process.env['BETTER_AUTH_URL'] || `http://localhost:${process.env['PORT'] || {{defaultPort}}}`;
14
+ const PORT = Number(process.env['PORT']) || {{defaultPort}};
15
+ const BASE_URL = process.env['BETTER_AUTH_URL'] || `http://localhost:${PORT}`;
16
16
 
17
17
  export const authPlugin = momentumAuth({
18
18
  {{authDbConfig}}
19
- baseURL: authBaseURL,
20
- trustedOrigins: [authBaseURL],
19
+ baseURL: BASE_URL,
20
+ trustedOrigins: [BASE_URL],
21
21
  email: {
22
22
  appName: '{{projectName}}',
23
23
  },
@@ -28,7 +28,7 @@ export const authPlugin = momentumAuth({
28
28
  */
29
29
  export const seo = seoPlugin({
30
30
  collections: ['posts'],
31
- siteUrl: `http://localhost:${process.env['PORT'] || {{defaultPort}}}`,
31
+ siteUrl: BASE_URL,
32
32
  analysis: true,
33
33
  sitemap: true,
34
34
  robots: true,
@@ -51,7 +51,7 @@ const config = defineMomentumConfig({
51
51
  },
52
52
  },
53
53
  server: {
54
- port: Number(process.env['PORT']) || {{defaultPort}},
54
+ port: PORT,
55
55
  cors: {
56
56
  origin: process.env['CORS_ORIGIN'] || '*',
57
57
  methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
@@ -13,16 +13,16 @@ const dbAdapter = {{dbAdapter}};
13
13
 
14
14
  {{dbPoolSetup}}
15
15
 
16
- const authBaseURL =
17
- process.env['BETTER_AUTH_URL'] || `http://localhost:${process.env['PORT'] || {{defaultPort}}}`;
16
+ const PORT = Number(process.env['PORT']) || {{defaultPort}};
17
+ const BASE_URL = process.env['BETTER_AUTH_URL'] || `http://localhost:${PORT}`;
18
18
 
19
19
  /**
20
20
  * Auth plugin - manages Better Auth integration, user tables, and middleware.
21
21
  */
22
22
  export const authPlugin = momentumAuth({
23
23
  {{authDbConfig}}
24
- baseURL: authBaseURL,
25
- trustedOrigins: [authBaseURL],
24
+ baseURL: BASE_URL,
25
+ trustedOrigins: [BASE_URL],
26
26
  });
27
27
 
28
28
  /**
@@ -30,7 +30,7 @@ export const authPlugin = momentumAuth({
30
30
  */
31
31
  export const seo = seoPlugin({
32
32
  collections: ['posts'],
33
- siteUrl: `http://localhost:${process.env['PORT'] || {{defaultPort}}}`,
33
+ siteUrl: BASE_URL,
34
34
  analysis: true,
35
35
  sitemap: true,
36
36
  robots: true,
@@ -56,7 +56,7 @@ const config = defineMomentumConfig({
56
56
  },
57
57
  },
58
58
  server: {
59
- port: Number(process.env['PORT']) || {{defaultPort}},
59
+ port: PORT,
60
60
  cors: {
61
61
  origin: '*',
62
62
  methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
@@ -13,16 +13,16 @@ const dbAdapter = {{dbAdapter}};
13
13
 
14
14
  {{dbPoolSetup}}
15
15
 
16
- const authBaseURL =
17
- process.env['BETTER_AUTH_URL'] || `http://localhost:${process.env['PORT'] || {{defaultPort}}}`;
16
+ const PORT = Number(process.env['PORT']) || {{defaultPort}};
17
+ const BASE_URL = process.env['BETTER_AUTH_URL'] || `http://localhost:${PORT}`;
18
18
 
19
19
  /**
20
20
  * Auth plugin - manages Better Auth integration, user tables, and middleware.
21
21
  */
22
22
  export const authPlugin = momentumAuth({
23
23
  {{authDbConfig}}
24
- baseURL: authBaseURL,
25
- trustedOrigins: [authBaseURL],
24
+ baseURL: BASE_URL,
25
+ trustedOrigins: [BASE_URL],
26
26
  });
27
27
 
28
28
  /**
@@ -30,7 +30,7 @@ export const authPlugin = momentumAuth({
30
30
  */
31
31
  export const seo = seoPlugin({
32
32
  collections: ['posts'],
33
- siteUrl: `http://localhost:${process.env['PORT'] || {{defaultPort}}}`,
33
+ siteUrl: BASE_URL,
34
34
  analysis: true,
35
35
  sitemap: true,
36
36
  robots: true,
@@ -56,7 +56,7 @@ const config = defineMomentumConfig({
56
56
  },
57
57
  },
58
58
  server: {
59
- port: Number(process.env['PORT']) || {{defaultPort}},
59
+ port: PORT,
60
60
  cors: {
61
61
  origin: '*',
62
62
  methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
@@ -17,11 +17,17 @@ Momentum CMS is a headless CMS built with Angular. You define collections in Typ
17
17
 
18
18
  ## Available Skills
19
19
 
20
- | Skill | Usage | Description |
21
- | -------------------- | -------------------------- | ---------------------------------------------------------------- |
22
- | `/collection <name>` | `/collection products` | Generate a new collection with fields, access control, and hooks |
23
- | `/momentum-api <op>` | `/momentum-api crud posts` | Guide for using `injectMomentumAPI()` in Angular components |
24
- | `/add-plugin <name>` | `/add-plugin analytics` | Add and configure a Momentum CMS plugin |
20
+ | Skill | Usage | Description |
21
+ | ------------------------- | ------------------------------ | ---------------------------------------------------------------- |
22
+ | `/collection <name>` | `/collection products` | Generate a new collection with fields, access control, and hooks |
23
+ | `/momentum-api <op>` | `/momentum-api crud posts` | Guide for using `injectMomentumAPI()` in Angular components |
24
+ | `/add-plugin <name>` | `/add-plugin analytics` | Add and configure a Momentum CMS plugin |
25
+ | `/admin-config <what>` | `/admin-config field-renderer` | Wire admin routes, plugin imports, and custom field renderers |
26
+ | `/admin-customize <what>` | `/admin-customize slot` | Swappable pages, layout slots, per-collection overrides |
27
+ | `/api-route <name>` | `/api-route health` | Generate API route handlers for Express or Analog.js |
28
+ | `/component <name>` | `/component post-card` | Generate an Angular component with signals and OnPush |
29
+ | `/e2e-test <feature>` | `/e2e-test posts` | Write Playwright E2E tests (dashboard-first, no blind tests) |
30
+ | `/migrations <op>` | `/migrations generate` | Run migrations, generate schemas, and manage code generation |
25
31
 
26
32
  ## Common Workflows
27
33
 
@@ -10,7 +10,7 @@ Add and configure a plugin in this Momentum CMS project.
10
10
 
11
11
  ## Arguments
12
12
 
13
- - `$ARGUMENTS` - Plugin name: "analytics", "otel", "event-bus", or an npm package name
13
+ - `$ARGUMENTS` - Plugin name: "seo", "analytics", "otel", "event-bus", or an npm package name
14
14
 
15
15
  ## Steps
16
16
 
@@ -23,6 +23,43 @@ Add and configure a plugin in this Momentum CMS project.
23
23
 
24
24
  ## Official Plugins
25
25
 
26
+ ### SEO (`@momentumcms/plugins-seo`)
27
+
28
+ Adds SEO fields to collections, sitemap.xml, robots.txt, meta tag API, and an admin dashboard. **Included by default** in scaffolded projects.
29
+
30
+ ```bash
31
+ npm install @momentumcms/plugins-seo
32
+ ```
33
+
34
+ ```typescript
35
+ // In src/momentum.config.ts
36
+ import { seoPlugin } from '@momentumcms/plugins-seo';
37
+
38
+ const seo = seoPlugin({
39
+ collections: ['posts'], // Collection slugs to inject SEO fields into (or '*' for all)
40
+ siteUrl: BASE_URL, // Base URL for sitemaps and canonical URLs
41
+ analysis: true, // Enable SEO scoring analysis on save
42
+ sitemap: true, // Enable /sitemap.xml endpoint
43
+ robots: true, // Enable /robots.txt endpoint
44
+ metaApi: true, // Enable /api/seo/meta/:collection/:id endpoint
45
+ adminDashboard: true, // Enable SEO admin pages (dashboard, sitemap, robots)
46
+ });
47
+
48
+ const config = defineMomentumConfig({
49
+ // ...existing config
50
+ plugins: [authPlugin, seo],
51
+ });
52
+ ```
53
+
54
+ **What it adds:**
55
+
56
+ - SEO fields (title, description, keywords, Open Graph, Twitter cards) injected into specified collections via a tabbed "SEO" section
57
+ - SEO analysis scoring (title length, description, keyword density) with per-document scores
58
+ - Sitemap.xml generation from published documents
59
+ - Robots.txt generation with configurable rules
60
+ - Meta tag API endpoint for headless frontends
61
+ - Admin dashboard with SEO overview, sitemap settings, and robots.txt editor
62
+
26
63
  ### Analytics (`@momentumcms/plugins-analytics`)
27
64
 
28
65
  Adds event tracking, content performance dashboard, and block-level analytics.
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: admin-config
3
+ description: Wire admin routes, configure browser-safe plugin imports, and create custom field renderers. Use when setting up momentumAdminRoutes, browserImports, or FieldRendererRegistry.
4
+ argument-hint: <routes|field-renderer|plugin-imports>
5
+ ---
6
+
7
+ # Admin Config & Field Renderers
8
+
9
+ Reference for wiring admin routes, plugin browser imports, and custom field renderers.
10
+
11
+ ## Arguments
12
+
13
+ - `$ARGUMENTS` - What to configure: `routes`, `field-renderer`, or `plugin-imports`
14
+
15
+ ## Admin Config Generator
16
+
17
+ The admin config generator reads `momentum.config.ts` (server-side, Node) and outputs a browser-safe TypeScript file with proper imports. This eliminates manual wiring of collections, auth collections, and plugin routes in app routing.
18
+
19
+ ### Usage in app routes
20
+
21
+ ```typescript
22
+ import { momentumAdminRoutes } from '@momentumcms/admin';
23
+ import { adminConfig } from '../generated/momentum.config';
24
+
25
+ export const appRoutes: Route[] = [
26
+ ...momentumAdminRoutes(adminConfig),
27
+ // app-specific routes...
28
+ ];
29
+ ```
30
+
31
+ ### Regenerate after config changes
32
+
33
+ ```bash
34
+ npm run generate # Regenerates types + admin config
35
+ ```
36
+
37
+ ### Plugin browserImports
38
+
39
+ Plugins declare browser-safe imports via `browserImports` on `MomentumPlugin`:
40
+
41
+ ```typescript
42
+ browserImports: {
43
+ collections: { path: '@momentumcms/auth/collections', exportName: 'BASE_AUTH_COLLECTIONS' },
44
+ adminRoutes: { path: '@momentumcms/plugins-seo/admin-routes', exportName: 'seoAdminRoutes' },
45
+ modifyCollections: { path: '@momentumcms/plugins-analytics/block-fields', exportName: 'injectBlockAnalyticsFields' },
46
+ }
47
+ ```
48
+
49
+ ### Component loaders in generated config
50
+
51
+ The generator emits `admin.components` (global) and per-collection `admin.components` as lazy-loading functions with rewritten import paths. This means `momentum.config.ts` is the single source of truth for page overrides and layout slots.
52
+
53
+ For swappable pages and layout slots, see `/admin-customize`.
54
+
55
+ ## Custom Field Renderers
56
+
57
+ Field renderers are lazily loaded via `FieldRendererRegistry`. Built-in renderers are registered with `provideMomentumFieldRenderers()`.
58
+
59
+ ### App setup (required)
60
+
61
+ ```typescript
62
+ import { provideMomentumFieldRenderers } from '@momentumcms/admin';
63
+
64
+ export const appConfig: ApplicationConfig = {
65
+ providers: [provideMomentumFieldRenderers()],
66
+ };
67
+ ```
68
+
69
+ ### Adding a custom field type
70
+
71
+ ```typescript
72
+ import { provideFieldRenderer } from '@momentumcms/admin';
73
+
74
+ export const appConfig: ApplicationConfig = {
75
+ providers: [
76
+ provideMomentumFieldRenderers(),
77
+ provideFieldRenderer('color', () =>
78
+ import('./renderers/color-field.component').then((m) => m.ColorFieldRenderer),
79
+ ),
80
+ ],
81
+ };
82
+ ```
@@ -0,0 +1,185 @@
1
+ ---
2
+ name: admin-customize
3
+ description: Customize the admin UI with swappable pages and layout slots. Use when replacing built-in pages (dashboard, list, edit, view), injecting content into layout slots (header, footer, sidebar, before/after), or registering per-collection overrides.
4
+ argument-hint: <page-override|slot|per-collection>
5
+ ---
6
+
7
+ # Admin Customization — Swappable Pages & Layout Slots
8
+
9
+ Guide for customizing the Momentum CMS admin UI via page replacements and layout slot injection.
10
+
11
+ ## Arguments
12
+
13
+ - `$ARGUMENTS` - What to customize: `page-override`, `slot`, or `per-collection`
14
+
15
+ ## Two Registration Methods
16
+
17
+ ### 1. Config-level (momentum.config.ts)
18
+
19
+ Registered in the server config. The code generator emits these as loader functions in the browser-safe generated config. This is the recommended approach — single source of truth.
20
+
21
+ After changes, run: `npm run generate`
22
+
23
+ ### 2. Provider-level (app.config.ts)
24
+
25
+ Registered via Angular DI providers. Takes effect immediately, no generation needed. Useful for app-specific customizations or login page slots.
26
+
27
+ ## Swappable Pages (Full Replacement)
28
+
29
+ ### Available Page Keys
30
+
31
+ | Key | Built-in Page |
32
+ | ----------------- | --------------- |
33
+ | `dashboard` | Dashboard |
34
+ | `login` | Login |
35
+ | `media` | Media Library |
36
+ | `collection-list` | Collection List |
37
+ | `collection-edit` | Collection Edit |
38
+ | `collection-view` | Collection View |
39
+ | `global-edit` | Global Edit |
40
+
41
+ ### Config-level (momentum.config.ts)
42
+
43
+ ```typescript
44
+ admin: {
45
+ components: {
46
+ dashboard: () =>
47
+ import('./app/custom-dashboard.component').then((m) => m.CustomDashboard),
48
+ },
49
+ },
50
+ ```
51
+
52
+ ### Provider-level (app.config.ts)
53
+
54
+ ```typescript
55
+ import { provideAdminComponent } from '@momentumcms/admin';
56
+
57
+ providers: [
58
+ provideAdminComponent('dashboard', () =>
59
+ import('./custom-dashboard.component').then((m) => m.CustomDashboard),
60
+ ),
61
+ ],
62
+ ```
63
+
64
+ ### Per-Collection Page Overrides
65
+
66
+ Override pages for a specific collection only:
67
+
68
+ #### Config-level (on collection admin.components)
69
+
70
+ ```typescript
71
+ admin: {
72
+ components: {
73
+ list: () => import('./custom-articles-list.component').then((m) => m.CustomArticlesList),
74
+ },
75
+ },
76
+ ```
77
+
78
+ #### Provider-level
79
+
80
+ ```typescript
81
+ provideAdminComponent('collections/articles/list', () =>
82
+ import('./custom-articles-list.component').then((m) => m.CustomArticlesList),
83
+ );
84
+ ```
85
+
86
+ ### Resolution Chain
87
+
88
+ 1. Per-collection override (`collections/{slug}/{type}`)
89
+ 2. Global override (`collection-list`)
90
+ 3. Built-in default
91
+
92
+ ## Layout Slots (Additive Injection)
93
+
94
+ Slots inject content around existing pages. Multiple components can register for the same slot.
95
+
96
+ ### Available Slots
97
+
98
+ | Slot Key | Position |
99
+ | -------------------------------------- | ------------------------------- |
100
+ | `shell:header` | Top of main content area |
101
+ | `shell:footer` | Bottom of main content area |
102
+ | `shell:nav-start` | After Dashboard link in sidebar |
103
+ | `shell:nav-end` | After plugin routes in sidebar |
104
+ | `dashboard:before/after` | Around dashboard content |
105
+ | `collection-list:before/after` | Around list tables |
106
+ | `collection-edit:before/after/sidebar` | Around edit forms |
107
+ | `collection-view:before/after` | Around view pages |
108
+ | `login:before/after` | Around login form |
109
+
110
+ ### Config-level (momentum.config.ts — admin.components)
111
+
112
+ | Config Key | Slot Position |
113
+ | ------------------ | ------------------ |
114
+ | `beforeNavigation` | `shell:nav-start` |
115
+ | `afterNavigation` | `shell:nav-end` |
116
+ | `header` | `shell:header` |
117
+ | `footer` | `shell:footer` |
118
+ | `beforeDashboard` | `dashboard:before` |
119
+ | `afterDashboard` | `dashboard:after` |
120
+ | `beforeLogin` | `login:before` |
121
+ | `afterLogin` | `login:after` |
122
+
123
+ ```typescript
124
+ admin: {
125
+ components: {
126
+ beforeDashboard: () =>
127
+ import('./app/announcement-banner.component').then((m) => m.AnnouncementBanner),
128
+ footer: () =>
129
+ import('./app/custom-footer.component').then((m) => m.CustomFooter),
130
+ },
131
+ },
132
+ ```
133
+
134
+ ### Per-collection config keys
135
+
136
+ `beforeList`, `afterList`, `beforeEdit`, `afterEdit`, `editSidebar`, `beforeView`, `afterView`
137
+
138
+ ### Provider-level (app.config.ts)
139
+
140
+ ```typescript
141
+ import { provideAdminSlot } from '@momentumcms/admin';
142
+
143
+ providers: [
144
+ provideAdminSlot('shell:header', () =>
145
+ import('./env-banner.component').then((m) => m.EnvBanner),
146
+ ),
147
+ // Per-collection:
148
+ provideAdminSlot('collection-list:before:articles', () =>
149
+ import('./articles-filter.component').then((m) => m.ArticlesFilter),
150
+ ),
151
+ ],
152
+ ```
153
+
154
+ ## Component Template
155
+
156
+ ```typescript
157
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
158
+ import type { CollectionConfig } from '@momentumcms/core';
159
+
160
+ @Component({
161
+ selector: 'app-custom-slot',
162
+ host: { class: 'block' },
163
+ template: `
164
+ <div class="p-4 bg-mcms-muted rounded-lg">
165
+ @if (collection(); as col) {
166
+ <p>Collection: {{ col.slug }}</p>
167
+ }
168
+ </div>
169
+ `,
170
+ changeDetection: ChangeDetectionStrategy.OnPush,
171
+ })
172
+ export class CustomSlotComponent {
173
+ readonly collection = input<CollectionConfig>();
174
+ readonly entityId = input<string>();
175
+ }
176
+ ```
177
+
178
+ ## Exports from @momentumcms/admin
179
+
180
+ ```typescript
181
+ import {
182
+ provideAdminComponent, // Register page override via DI
183
+ provideAdminSlot, // Register slot component via DI
184
+ } from '@momentumcms/admin';
185
+ ```
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: api-route
3
+ description: Generate API route handlers for Express, NestJS, or Analog.js
4
+ argument-hint: <route-name>
5
+ ---
6
+
7
+ # Generate API Route
8
+
9
+ Create API route handlers for your Momentum CMS project.
10
+
11
+ ## Arguments
12
+
13
+ - `$ARGUMENTS` - Route name (e.g., "health", "custom-endpoint")
14
+
15
+ ## For Express / NestJS (Angular SSR)
16
+
17
+ Both the Express and NestJS server adapters expose an Express instance. Custom routes use the same Express Router pattern.
18
+
19
+ Create handler in `src/api/<route-name>.ts`:
20
+
21
+ ```typescript
22
+ import { Router, Request, Response } from 'express';
23
+ import type { CollectionConfig } from '@momentumcms/core';
24
+
25
+ export function create<PascalName>Routes(collections: CollectionConfig[]): Router {
26
+ const router = Router();
27
+
28
+ router.get('/<route-name>', async (req: Request, res: Response) => {
29
+ try {
30
+ // Implementation
31
+ res.json({ success: true });
32
+ } catch (error) {
33
+ res.status(500).json({ error: 'Internal server error' });
34
+ }
35
+ });
36
+
37
+ return router;
38
+ }
39
+ ```
40
+
41
+ Register in `src/server.ts`:
42
+
43
+ ```typescript
44
+ import { create<PascalName>Routes } from './api/<route-name>';
45
+
46
+ // Express: register on the Express app directly
47
+ app.use('/api', create<PascalName>Routes(collections));
48
+
49
+ // NestJS: register via afterApiMiddleware in createMomentumNestServer()
50
+ afterApiMiddleware: (app) => {
51
+ app.use('/api', create<PascalName>Routes(collections));
52
+ // ...existing static files and Angular SSR handlers
53
+ },
54
+ ```
55
+
56
+ ## For Analog.js
57
+
58
+ Create file-based route in `src/server/routes/api/<route-name>.get.ts`:
59
+
60
+ ```typescript
61
+ import { defineEventHandler, getQuery } from 'h3';
62
+
63
+ export default defineEventHandler(async (event) => {
64
+ const query = getQuery(event);
65
+
66
+ try {
67
+ // Implementation
68
+ return { success: true };
69
+ } catch (error) {
70
+ throw createError({
71
+ statusCode: 500,
72
+ statusMessage: 'Internal server error',
73
+ });
74
+ }
75
+ });
76
+ ```
77
+
78
+ ## HTTP Method Suffixes (Analog.js)
79
+
80
+ - `index.get.ts` - GET request
81
+ - `index.post.ts` - POST request
82
+ - `[id].get.ts` - GET with dynamic param
83
+ - `[id].patch.ts` - PATCH with dynamic param
84
+ - `[id].delete.ts` - DELETE with dynamic param
85
+ - `[...].ts` - Catch-all route
86
+
87
+ ## h3 Utilities
88
+
89
+ ```typescript
90
+ import {
91
+ defineEventHandler,
92
+ getQuery,
93
+ readBody,
94
+ getRouterParam,
95
+ createError,
96
+ setResponseStatus,
97
+ } from 'h3';
98
+ ```
@@ -70,8 +70,8 @@ collections: [Posts, <PascalName>],
70
70
  4. Remind user to run schema generation:
71
71
 
72
72
  ```bash
73
- npx drizzle-kit generate
74
- npx drizzle-kit push
73
+ npm run migrate:generate
74
+ npm run migrate:run
75
75
  ```
76
76
 
77
77
  ## Field Types Available
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: component
3
+ description: Generate an Angular component with signals, OnPush, and host-based styling following Momentum CMS conventions.
4
+ argument-hint: <component-name>
5
+ ---
6
+
7
+ # Generate Angular Component
8
+
9
+ Create an Angular component following Momentum CMS conventions.
10
+
11
+ ## Arguments
12
+
13
+ - `$ARGUMENTS` - Component name in kebab-case (e.g., "data-table", "post-card")
14
+
15
+ ## Steps
16
+
17
+ 1. Create component file at `src/app/components/<component>.ts` (or `src/app/<feature>/<component>.ts`)
18
+
19
+ 2. Use this template:
20
+
21
+ ```typescript
22
+ import { ChangeDetectionStrategy, Component, computed, input, output, signal, inject } from '@angular/core';
23
+
24
+ @Component({
25
+ selector: 'app-<component-name>',
26
+ host: { class: 'block' },
27
+ template: `
28
+ <!-- Template here -->
29
+ `,
30
+ changeDetection: ChangeDetectionStrategy.OnPush,
31
+ })
32
+ export class <PascalName>Component {
33
+ // Signal inputs (use proper types, never `any`)
34
+ readonly data = input.required<DataType>();
35
+
36
+ // Optional inputs with defaults
37
+ readonly disabled = input(false);
38
+
39
+ // Signal outputs
40
+ readonly valueChange = output<ValueType>();
41
+
42
+ // Internal state
43
+ private readonly _loading = signal(false);
44
+
45
+ // Computed values
46
+ readonly loading = this._loading.asReadonly();
47
+ readonly hasData = computed(() => !!this.data());
48
+ }
49
+ ```
50
+
51
+ ## Key Conventions
52
+
53
+ - **NO `standalone: true`** — default in Angular 21, redundant
54
+ - **NO `any` types** — use proper interfaces
55
+ - **NO `CommonModule` import** — unnecessary in Angular 21
56
+ - Always use `ChangeDetectionStrategy.OnPush`
57
+ - Use `input()` and `input.required()` for inputs
58
+ - Use `output()` for outputs
59
+ - Use `signal()` for internal state, `computed()` for derived values
60
+ - Use `inject()` for dependency injection
61
+ - Use `@for`/`@if`/`@switch` control flow
62
+
63
+ ## UI Component Patterns
64
+
65
+ ### No Wrapping Divs
66
+
67
+ Angular components create a host element. Style the host directly — do NOT add wrapper divs:
68
+
69
+ ```typescript
70
+ @Component({
71
+ selector: 'app-button',
72
+ host: { class: 'inline-flex items-center gap-2' },
73
+ template: `<ng-content />`,
74
+ })
75
+ ```
76
+
77
+ NOT:
78
+
79
+ ```typescript
80
+ // BAD: unnecessary wrapper div
81
+ template: `<div class="inline-flex items-center gap-2"><ng-content /></div>`,
82
+ ```
83
+
84
+ ### Theme Service
85
+
86
+ Use `McmsThemeService` for dark mode support:
87
+
88
+ ```typescript
89
+ import { McmsThemeService } from '@momentumcms/admin';
90
+
91
+ @Component({...})
92
+ export class MyComponent {
93
+ private readonly theme = inject(McmsThemeService);
94
+
95
+ toggleDarkMode(): void {
96
+ this.theme.toggleTheme();
97
+ }
98
+
99
+ readonly isDark = this.theme.isDark; // computed signal
100
+ readonly currentTheme = this.theme.theme; // 'light' | 'dark' | 'system'
101
+ }
102
+ ```
103
+
104
+ ## Tailwind Setup
105
+
106
+ The project uses the `@momentumcms/admin` Tailwind preset and CSS variables for theming.
107
+
108
+ ### tailwind.config.js
109
+
110
+ ```javascript
111
+ const adminPreset = require('@momentumcms/admin/tailwind.preset');
112
+
113
+ module.exports = {
114
+ presets: [adminPreset],
115
+ content: [
116
+ './src/**/*.{html,ts}',
117
+ './node_modules/@momentumcms/admin/**/*.{html,ts,mjs}',
118
+ './node_modules/@momentumcms/ui/**/*.{html,ts,mjs}',
119
+ ],
120
+ };
121
+ ```
122
+
123
+ ### styles.css
124
+
125
+ CSS variables are defined in `src/styles.css` with `:root` (light) and `.dark` variants. All use HSL values referenced via `hsl(var(--mcms-<name>))`. Key tokens:
126
+
127
+ - `--mcms-background/foreground` — page background and text
128
+ - `--mcms-primary/primary-foreground` — primary actions (blue)
129
+ - `--mcms-card/card-foreground` — card surfaces
130
+ - `--mcms-muted/muted-foreground` — subtle backgrounds
131
+ - `--mcms-destructive` — danger/delete actions
132
+ - `--mcms-sidebar` — sidebar-specific colors
133
+ - `--mcms-border`, `--mcms-input`, `--mcms-ring` — form elements
134
+ - `--mcms-radius` — border radius base
135
+
136
+ Use Tailwind utility classes that reference these tokens (e.g., `bg-background`, `text-foreground`, `border-border`).
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: e2e-test
3
+ description: Write and validate Playwright E2E tests for Momentum CMS features. UI tests start from /admin dashboard and navigate via sidebar — never go directly to deep URLs.
4
+ argument-hint: <feature-or-collection-name>
5
+ ---
6
+
7
+ # E2E Test Writing Skill
8
+
9
+ Write Playwright E2E tests for Momentum CMS features. UI tests simulate real user journeys — they **always start from the admin dashboard** and navigate through the actual UI.
10
+
11
+ ## RULE 1: DASHBOARD IS THE STARTING POINT
12
+
13
+ **Every admin UI test starts at `/admin` (the dashboard) and navigates to the feature via the sidebar or dashboard cards.** This validates that the feature is actually visible and reachable.
14
+
15
+ ### The Navigation Chain
16
+
17
+ ```
18
+ /admin (dashboard) → sidebar click → list view → create/view/edit
19
+ ```
20
+
21
+ **NEVER do this:**
22
+
23
+ ```typescript
24
+ // BAD: Skipping to a deep URL
25
+ await page.goto('/admin/collections/posts/new');
26
+ ```
27
+
28
+ **ALWAYS do this:**
29
+
30
+ ```typescript
31
+ // GOOD: Start from dashboard
32
+ await page.goto('/admin');
33
+ await page.waitForLoadState('domcontentloaded');
34
+
35
+ const sidebar = page.getByLabel('Main navigation');
36
+ await sidebar.getByRole('link', { name: 'Posts' }).click();
37
+ await expect(page).toHaveURL(/\/admin\/collections\/posts$/);
38
+
39
+ await page.getByRole('button', { name: /Create Post/i }).click();
40
+ ```
41
+
42
+ ## RULE 2: NEVER WRITE BLIND TESTS
43
+
44
+ **You MUST see the actual UI before writing any assertions.** Write a probe test that intentionally fails to see the page structure:
45
+
46
+ ```typescript
47
+ test('probe: inspect dashboard', async ({ page }) => {
48
+ await page.goto('/admin');
49
+ await page.waitForLoadState('domcontentloaded');
50
+ await expect(page.getByText('XYZZY_WILL_NOT_MATCH')).toBeVisible();
51
+ });
52
+ ```
53
+
54
+ ## Arguments
55
+
56
+ - `$ARGUMENTS` - Feature/collection to test (e.g., "posts", "settings", "seo dashboard")
57
+
58
+ ## Test File Location
59
+
60
+ Create test files at:
61
+
62
+ ```
63
+ tests/<feature>.spec.ts
64
+ ```
65
+
66
+ ## Test Setup
67
+
68
+ ```typescript
69
+ import { test, expect } from '@playwright/test';
70
+
71
+ const ADMIN_EMAIL = 'admin@test.com';
72
+ const ADMIN_PASSWORD = 'password123';
73
+
74
+ test.beforeEach(async ({ page, request }) => {
75
+ // Sign in
76
+ await request.post('/api/auth/sign-in/email', {
77
+ headers: { 'Content-Type': 'application/json' },
78
+ data: { email: ADMIN_EMAIL, password: ADMIN_PASSWORD },
79
+ });
80
+ });
81
+ ```
82
+
83
+ ## Admin UI Test Patterns
84
+
85
+ ### Full Navigation Flow
86
+
87
+ ```typescript
88
+ test.describe('Feature Admin UI', () => {
89
+ test('navigate from dashboard to list via sidebar', async ({ page }) => {
90
+ await page.goto('/admin');
91
+ await page.waitForLoadState('domcontentloaded');
92
+
93
+ // Navigate via sidebar
94
+ const sidebar = page.getByLabel('Main navigation');
95
+ await sidebar.getByRole('link', { name: 'Posts' }).click();
96
+ await expect(page).toHaveURL(/\/admin\/collections\/posts$/);
97
+ await expect(page.getByRole('heading', { name: 'Posts' })).toBeVisible();
98
+ });
99
+
100
+ test('create via UI from dashboard', async ({ page }) => {
101
+ await page.goto('/admin');
102
+ await page.waitForLoadState('domcontentloaded');
103
+
104
+ const sidebar = page.getByLabel('Main navigation');
105
+ await sidebar.getByRole('link', { name: 'Posts' }).click();
106
+
107
+ await page.getByRole('button', { name: /Create Post/i }).click();
108
+ await expect(page.getByRole('button', { name: 'Create', exact: true })).toBeVisible();
109
+
110
+ await page.locator('input#field-title').fill('Test Post');
111
+ await page.getByRole('button', { name: 'Create', exact: true }).click();
112
+
113
+ await expect(page).toHaveURL(/\/admin\/collections\/posts\/[^/]+$/);
114
+ });
115
+ });
116
+ ```
117
+
118
+ ### Key UI Selectors
119
+
120
+ | Element | Selector |
121
+ | --------------- | ------------------------------------------------------------ |
122
+ | Sidebar nav | `getByLabel('Main navigation')` |
123
+ | Dashboard group | `getByRole('region', { name: 'GroupName' })` |
124
+ | Text inputs | `locator('input#field-{fieldName}')` |
125
+ | Create button | `getByRole('button', { name: 'Create', exact: true })` |
126
+ | Save button | `getByRole('button', { name: 'Save Changes' })` (NOT "Save") |
127
+ | Edit URL | `/{id}/edit` (view page is `/{id}`) |
128
+
129
+ ## API Test Patterns
130
+
131
+ ```typescript
132
+ test.describe('Feature - API', () => {
133
+ test('CRUD operations', async ({ request }) => {
134
+ const create = await request.post('/api/posts', {
135
+ headers: { 'Content-Type': 'application/json' },
136
+ data: { title: 'API Test Post' },
137
+ });
138
+ expect(create.status()).toBe(201);
139
+
140
+ const { doc } = (await create.json()) as { doc: { id: string } };
141
+
142
+ const get = await request.get(`/api/posts/${doc.id}`);
143
+ expect(get.ok()).toBe(true);
144
+
145
+ // Clean up
146
+ await request.delete(`/api/posts/${doc.id}`);
147
+ });
148
+ });
149
+ ```
150
+
151
+ ## Banned Patterns
152
+
153
+ 1. **NO `.catch(() => false/null/{})` on Playwright calls**
154
+ 2. **NO `waitForTimeout(N)`** — use `expect(locator).toBeVisible({ timeout: N })`
155
+ 3. **NO ambiguous assertions** — use exact status codes and values
156
+ 4. **NO direct URL navigation for UI tests** — always start from `/admin`
157
+
158
+ ## Running Tests
159
+
160
+ ```bash
161
+ npx playwright test # Run all tests
162
+ npx playwright test tests/<feature>.spec.ts # Run specific spec
163
+ npx playwright test --grep "test name" # Run by name
164
+ ```
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: migrations
3
+ description: Run migrations, generate schemas, and manage code generation for Momentum CMS.
4
+ argument-hint: <generate|run|status|rollback|codegen>
5
+ ---
6
+
7
+ # Migrations & Code Generation
8
+
9
+ Reference for database migrations and type/config generation.
10
+
11
+ ## Arguments
12
+
13
+ - `$ARGUMENTS` - Operation: `generate`, `run`, `status`, `rollback`, or `codegen`
14
+
15
+ ## Migration CLI
16
+
17
+ ```bash
18
+ npm run migrate:generate # Diff schema, create migration file
19
+ npm run migrate:run # Apply pending migrations
20
+ npm run migrate:status # Show applied vs pending
21
+ npm run migrate:rollback # Rollback latest batch
22
+ ```
23
+
24
+ ## Drizzle Kit
25
+
26
+ ```bash
27
+ npx drizzle-kit generate # Create SQL migrations from schema diff
28
+ npx drizzle-kit migrate # Apply migrations
29
+ npx drizzle-kit push # Direct push (dev only, skips migration files)
30
+ ```
31
+
32
+ ## Code Generation
33
+
34
+ The generator reads `momentum.config.ts` (server-side, Node) and outputs:
35
+
36
+ 1. **TypeScript types** — interfaces for all collections, blocks, where clauses
37
+ 2. **Browser-safe admin config** — inlined collections with server-only props stripped
38
+
39
+ ```bash
40
+ npm run generate # One-shot generation (types + admin config)
41
+ ```
42
+
43
+ Output files (do not edit manually):
44
+
45
+ - `src/generated/momentum.types.ts` — TypeScript interfaces
46
+ - `src/generated/momentum.config.ts` — Browser-safe admin config
47
+
48
+ ## Workflow
49
+
50
+ 1. Make collection changes in `src/collections/`
51
+ 2. Run `npm run generate` to regenerate types + admin config
52
+ 3. Run `npm run migrate:generate` to create a migration file
53
+ 4. Review the generated migration SQL
54
+ 5. Run `npm run migrate:run` to apply
55
+ 6. Restart the dev server
@@ -62,6 +62,18 @@ const updated = await this.api.collection<Post>('posts').update('123', { title:
62
62
  const result = await this.api.collection<Post>('posts').delete('123');
63
63
  ```
64
64
 
65
+ ### With Generated Types
66
+
67
+ 1. Generate types: `npm run generate`
68
+ 2. Import and use:
69
+
70
+ ```typescript
71
+ import type { Post, User } from '../generated/momentum.types';
72
+
73
+ const posts = await this.api.collection<Post>('posts').find();
74
+ const users = await this.api.collection<User>('users').find();
75
+ ```
76
+
65
77
  ## Find Options
66
78
 
67
79
  ```typescript
@@ -78,6 +90,7 @@ interface FindOptions {
78
90
  ```typescript
79
91
  import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
80
92
  import { injectMomentumAPI } from '@momentumcms/admin';
93
+ import type { Post } from '../generated/momentum.types';
81
94
 
82
95
  @Component({
83
96
  selector: 'app-posts',
@@ -88,17 +101,23 @@ import { injectMomentumAPI } from '@momentumcms/admin';
88
101
  @for (post of posts(); track post.id) {
89
102
  <article>
90
103
  <h2>{{ post.title }}</h2>
104
+ <p>{{ post.content }}</p>
91
105
  <button (click)="deletePost(post.id)">Delete</button>
92
106
  </article>
93
107
  }
94
108
  }
109
+
110
+ <form (submit)="createPost($event)">
111
+ <input #titleInput placeholder="Title" />
112
+ <button type="submit">Create Post</button>
113
+ </form>
95
114
  `,
96
115
  changeDetection: ChangeDetectionStrategy.OnPush,
97
116
  })
98
117
  export class PostsComponent {
99
118
  private readonly api = injectMomentumAPI();
100
119
 
101
- readonly posts = signal<any[]>([]);
120
+ readonly posts = signal<Post[]>([]);
102
121
  readonly loading = signal(true);
103
122
 
104
123
  constructor() {
@@ -108,7 +127,7 @@ export class PostsComponent {
108
127
  async loadPosts(): Promise<void> {
109
128
  this.loading.set(true);
110
129
  try {
111
- const result = await this.api.collection('posts').find({
130
+ const result = await this.api.collection<Post>('posts').find({
112
131
  limit: 20,
113
132
  sort: '-createdAt',
114
133
  });
@@ -118,8 +137,21 @@ export class PostsComponent {
118
137
  }
119
138
  }
120
139
 
140
+ async createPost(event: Event): Promise<void> {
141
+ event.preventDefault();
142
+ const form = event.target as HTMLFormElement;
143
+ const input = form.querySelector('input') as HTMLInputElement;
144
+
145
+ const post = await this.api.collection<Post>('posts').create({
146
+ title: input.value,
147
+ });
148
+
149
+ this.posts.update((posts) => [post, ...posts]);
150
+ input.value = '';
151
+ }
152
+
121
153
  async deletePost(id: string): Promise<void> {
122
- await this.api.collection('posts').delete(id);
154
+ await this.api.collection<Post>('posts').delete(id);
123
155
  this.posts.update((posts) => posts.filter((p) => p.id !== id));
124
156
  }
125
157
  }
@@ -131,28 +163,76 @@ export class PostsComponent {
131
163
  - **Browser**: HTTP calls to `/api/*`
132
164
  - **Same interface** - code works identically on both platforms
133
165
 
166
+ ## Error Handling
167
+
168
+ ```typescript
169
+ import {
170
+ CollectionNotFoundError,
171
+ DocumentNotFoundError,
172
+ AccessDeniedError,
173
+ ValidationError,
174
+ } from '@momentumcms/server-core';
175
+
176
+ try {
177
+ await this.api.collection('posts').create({ title: '' });
178
+ } catch (error) {
179
+ if (error instanceof ValidationError) {
180
+ console.error('Validation failed:', error.errors);
181
+ }
182
+ }
183
+ ```
184
+
185
+ ## Type Generation
186
+
187
+ Generate types from your collections:
188
+
189
+ ```bash
190
+ npm run generate
191
+ ```
192
+
193
+ Output files (do not edit manually):
194
+
195
+ - `src/generated/momentum.types.ts` — TypeScript interfaces
196
+ - `src/generated/momentum.config.ts` — Browser-safe admin config
197
+
134
198
  ## TransferState (SSR Hydration)
135
199
 
136
- TransferState is **enabled by default** for read operations. Data fetched during SSR is cached and reused on browser hydration.
200
+ TransferState is **enabled by default** for all read operations (`find`, `findById`, `findSignal`, `findByIdSignal`). Data fetched during SSR is automatically cached and reused on browser hydration, eliminating duplicate HTTP calls.
201
+
202
+ ### Default Behavior (TransferState enabled)
137
203
 
138
204
  ```typescript
139
- // Default: TransferState enabled (no duplicate fetch on hydration)
205
+ // SSR: Fetches and caches | Browser: Reads from cache (no HTTP)
140
206
  const posts = await this.api.collection<Post>('posts').find({ limit: 10 });
207
+ const post = await this.api.collection<Post>('posts').findById(id);
208
+ ```
209
+
210
+ ### Opt-out
141
211
 
142
- // Opt-out: always fetch fresh data
143
- const posts = await this.api.collection<Post>('posts').find({ limit: 10, transfer: false });
212
+ Use `transfer: false` to disable TransferState for a specific call:
213
+
214
+ ```typescript
215
+ const posts = await this.api.collection<Post>('posts').find({
216
+ limit: 10,
217
+ transfer: false,
218
+ });
144
219
  ```
145
220
 
146
- Requires `provideClientHydration()` in app config.
221
+ ### Signal Methods
147
222
 
148
- ## Error Handling
223
+ ```typescript
224
+ // Signals also use TransferState by default
225
+ readonly posts = this.api.collection<Post>('posts').findSignal({ limit: 10 });
226
+ readonly post = this.api.collection<Post>('posts').findByIdSignal(id);
227
+ ```
228
+
229
+ ### Requirements
230
+
231
+ Ensure `provideClientHydration()` is in your app config:
149
232
 
150
233
  ```typescript
151
- try {
152
- await this.api.collection('posts').create({ title: '' });
153
- } catch (error) {
154
- if (error instanceof ValidationError) {
155
- console.error('Validation failed:', error.errors);
156
- }
157
- }
234
+ // app.config.ts
235
+ export const appConfig: ApplicationConfig = {
236
+ providers: [provideClientHydration()],
237
+ };
158
238
  ```
@@ -12,7 +12,7 @@ services:
12
12
  environment:
13
13
  POSTGRES_USER: postgres
14
14
  POSTGRES_PASSWORD: postgres
15
- POSTGRES_DB: momentum
15
+ POSTGRES_DB: {{projectName}}
16
16
  volumes:
17
17
  - {{projectName}}_postgres_data:/var/lib/postgresql/data
18
18
  healthcheck: