create-momentum-app 0.5.2 → 0.5.3
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 +11 -7
- package/package.json +1 -1
- package/templates/analog/src/momentum.config.ts.tmpl +6 -6
- package/templates/angular/src/momentum.config.ts.tmpl +6 -6
- package/templates/nestjs/src/momentum.config.ts.tmpl +6 -6
- package/templates/shared/.claude/agents.md +10 -5
- package/templates/shared/.claude/skills/add-plugin/SKILL.md +38 -1
- package/templates/shared/.claude/skills/admin-config/SKILL.md +76 -0
- package/templates/shared/.claude/skills/api-route/SKILL.md +98 -0
- package/templates/shared/.claude/skills/collection/SKILL.md +2 -2
- package/templates/shared/.claude/skills/component/SKILL.md +136 -0
- package/templates/shared/.claude/skills/e2e-test/SKILL.md +164 -0
- package/templates/shared/.claude/skills/migrations/SKILL.md +55 -0
- package/templates/shared/.claude/skills/momentum-api/SKILL.md +96 -16
- package/templates/shared/docker-compose.yml.tmpl +1 -1
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
|
-
|
|
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
|
|
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
|
|
178
|
+
connectionString: process.env['DATABASE_URL'] ?? 'postgresql://postgres:postgres@localhost:5432/${projectName}',
|
|
175
179
|
})` : `sqliteAdapter({
|
|
176
|
-
filename: process.env['DATABASE_PATH'] ?? './data
|
|
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" ?
|
|
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: \`
|
|
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
|
|
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
|
@@ -11,13 +11,13 @@ const dbAdapter = {{dbAdapter}};
|
|
|
11
11
|
|
|
12
12
|
{{dbPoolSetup}}
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
|
|
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:
|
|
20
|
-
trustedOrigins: [
|
|
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:
|
|
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:
|
|
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
|
|
17
|
-
|
|
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:
|
|
25
|
-
trustedOrigins: [
|
|
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:
|
|
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:
|
|
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
|
|
17
|
-
|
|
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:
|
|
25
|
-
trustedOrigins: [
|
|
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:
|
|
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:
|
|
59
|
+
port: PORT,
|
|
60
60
|
cors: {
|
|
61
61
|
origin: '*',
|
|
62
62
|
methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
|
|
@@ -17,11 +17,16 @@ Momentum CMS is a headless CMS built with Angular. You define collections in Typ
|
|
|
17
17
|
|
|
18
18
|
## Available Skills
|
|
19
19
|
|
|
20
|
-
| Skill
|
|
21
|
-
|
|
|
22
|
-
| `/collection <name>`
|
|
23
|
-
| `/momentum-api <op>`
|
|
24
|
-
| `/add-plugin <name>`
|
|
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
|
+
| `/api-route <name>` | `/api-route health` | Generate API route handlers for Express or Analog.js |
|
|
27
|
+
| `/component <name>` | `/component post-card` | Generate an Angular component with signals and OnPush |
|
|
28
|
+
| `/e2e-test <feature>` | `/e2e-test posts` | Write Playwright E2E tests (dashboard-first, no blind tests) |
|
|
29
|
+
| `/migrations <op>` | `/migrations generate` | Run migrations, generate schemas, and manage code generation |
|
|
25
30
|
|
|
26
31
|
## Common Workflows
|
|
27
32
|
|
|
@@ -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,76 @@
|
|
|
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
|
+
## Custom Field Renderers
|
|
50
|
+
|
|
51
|
+
Field renderers are lazily loaded via `FieldRendererRegistry`. Built-in renderers are registered with `provideMomentumFieldRenderers()`.
|
|
52
|
+
|
|
53
|
+
### App setup (required)
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { provideMomentumFieldRenderers } from '@momentumcms/admin';
|
|
57
|
+
|
|
58
|
+
export const appConfig: ApplicationConfig = {
|
|
59
|
+
providers: [provideMomentumFieldRenderers()],
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Adding a custom field type
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { provideFieldRenderer } from '@momentumcms/admin';
|
|
67
|
+
|
|
68
|
+
export const appConfig: ApplicationConfig = {
|
|
69
|
+
providers: [
|
|
70
|
+
provideMomentumFieldRenderers(),
|
|
71
|
+
provideFieldRenderer('color', () =>
|
|
72
|
+
import('./renderers/color-field.component').then((m) => m.ColorFieldRenderer),
|
|
73
|
+
),
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
```
|
|
@@ -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
|
+
```
|
|
@@ -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<
|
|
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
|
-
//
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
221
|
+
### Signal Methods
|
|
147
222
|
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
console.error('Validation failed:', error.errors);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
234
|
+
// app.config.ts
|
|
235
|
+
export const appConfig: ApplicationConfig = {
|
|
236
|
+
providers: [provideClientHydration()],
|
|
237
|
+
};
|
|
158
238
|
```
|