@donotdev/cli 0.0.12 → 0.0.13

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 (67) hide show
  1. package/dependencies-matrix.json +30 -116
  2. package/dist/bin/commands/bump.js +33 -7
  3. package/dist/bin/commands/create-project.js +43 -7
  4. package/dist/bin/commands/deploy.js +7606 -17
  5. package/dist/bin/commands/firebase-setup.d.ts +6 -0
  6. package/dist/bin/commands/firebase-setup.d.ts.map +1 -0
  7. package/dist/bin/commands/firebase-setup.js +7 -0
  8. package/dist/bin/commands/firebase-setup.js.map +1 -0
  9. package/dist/bin/commands/staging.d.ts +11 -0
  10. package/dist/bin/commands/staging.d.ts.map +1 -0
  11. package/dist/bin/commands/staging.js +12 -0
  12. package/dist/bin/commands/staging.js.map +1 -0
  13. package/dist/bin/dndev.js +28 -3
  14. package/dist/bin/donotdev.js +28 -3
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +7671 -39
  18. package/dist/index.js.map +1 -1
  19. package/package.json +1 -1
  20. package/templates/app-demo/src/pages/DetailPage.tsx.example +1 -1
  21. package/templates/app-demo/src/pages/FullPage.tsx.example +3 -3
  22. package/templates/app-demo/src/pages/HomePage.tsx.example +1 -1
  23. package/templates/app-demo/src/pages/components/ComponentRenderer.tsx.example +5 -5
  24. package/templates/app-demo/src/pages/components/DemoLayout.tsx.example +3 -3
  25. package/templates/app-next/.env.example +2 -0
  26. package/templates/app-next/src/pages/HomePage.tsx.example +1 -1
  27. package/templates/app-vite/.env.example +2 -0
  28. package/templates/app-vite/src/pages/HomePage.tsx.example +163 -73
  29. package/templates/functions-firebase/build.mjs.example +26 -10
  30. package/templates/functions-firebase/functions-firebase/build.mjs.example +26 -10
  31. package/templates/functions-firebase/functions.config.js.example +11 -15
  32. package/templates/github-consumer/.github/workflows/ci.yml.example +36 -0
  33. package/templates/root-consumer/.claude/agents/architect.md.example +2 -2
  34. package/templates/root-consumer/.claude/agents/builder.md.example +2 -2
  35. package/templates/root-consumer/.claude/agents/coder.md.example +2 -2
  36. package/templates/root-consumer/.claude/agents/extractor.md.example +2 -3
  37. package/templates/root-consumer/.claude/agents/polisher.md.example +67 -291
  38. package/templates/root-consumer/.claude/agents/prompt-engineer.md.example +4 -4
  39. package/templates/root-consumer/.claude/commands/build.md.example +2 -2
  40. package/templates/root-consumer/.claude/commands/polish.md.example +65 -81
  41. package/templates/root-consumer/.env.example +13 -13
  42. package/templates/root-consumer/.gemini/settings.json.example +9 -0
  43. package/templates/root-consumer/.gitignore.example +3 -1
  44. package/templates/root-consumer/AI.md.example +139 -0
  45. package/templates/root-consumer/CLAUDE.md.example +13 -104
  46. package/templates/root-consumer/README.md.example +81 -255
  47. package/templates/root-consumer/entities/Contact.ts.example +126 -0
  48. package/templates/root-consumer/entities/index.ts.example +6 -3
  49. package/templates/root-consumer/guides/dndev/AGENT_START_HERE.md.example +41 -342
  50. package/templates/root-consumer/guides/dndev/COMPONENTS_ADV.md.example +2 -1
  51. package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +144 -9
  52. package/templates/root-consumer/guides/dndev/INDEX.md.example +9 -0
  53. package/templates/root-consumer/guides/dndev/SETUP_APP_CONFIG.md.example +13 -16
  54. package/templates/root-consumer/guides/dndev/SETUP_BLOG.md.example +263 -0
  55. package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +1 -1
  56. package/templates/root-consumer/guides/dndev/SETUP_FIREBASE.md.example +168 -0
  57. package/templates/root-consumer/guides/dndev/SETUP_FUNCTIONS.md.example +5 -12
  58. package/templates/root-consumer/guides/dndev/SETUP_TESTING.md.example +184 -0
  59. package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +134 -69
  60. package/templates/root-consumer/guides/wai-way/agents/polisher.md.example +66 -44
  61. package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +18 -1
  62. package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +1 -0
  63. package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +2 -1
  64. package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +2 -1
  65. package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +180 -108
  66. package/templates/root-consumer/guides/wai-way/context_map.json.example +8 -7
  67. package/templates/root-consumer/guides/wai-way/page_patterns.md.example +4 -4
@@ -0,0 +1,263 @@
1
+ # Setup: Blog
2
+
3
+ **Convention-based markdown blog.** Drop `slug_lang.md` files in `src/content/blog/`, get a fully functional blog with i18n, reading time, tags, RSS, and sitemap.
4
+
5
+ ---
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Create the content directory
10
+
11
+ ```
12
+ src/content/blog/
13
+ ├── my-first-post_en.md
14
+ ├── my-first-post_fr.md
15
+ └── another-post_en.md
16
+ ```
17
+
18
+ ### 2. Write a post with frontmatter
19
+
20
+ ```markdown
21
+ ---
22
+ title: My First Post
23
+ description: A short summary for listings and SEO
24
+ date: 2025-06-01
25
+ tags: tutorial, getting-started
26
+ image: /blog/my-first-post.png
27
+ ---
28
+
29
+ Your markdown content here. Supports:
30
+ - **Bold**, *italic*, ~~strikethrough~~
31
+ - [Internal links](/faq) and [external links](https://example.com)
32
+ - Images: ![alt text](/blog/my-image.png)
33
+ - Code blocks with syntax highlighting
34
+ ```
35
+
36
+ ### 3. Create the data loader
37
+
38
+ ```typescript
39
+ // src/data/blog/index.ts
40
+ import { createBlogLoader } from '@donotdev/templates';
41
+ import type { BlogLoader } from '@donotdev/templates';
42
+
43
+ const markdownFiles = import.meta.glob('../../content/blog/*.md', {
44
+ query: '?raw',
45
+ import: 'default',
46
+ eager: true,
47
+ });
48
+
49
+ export function getBlogLoader(lang: string = 'en'): BlogLoader {
50
+ return createBlogLoader(markdownFiles as Record<string, string>, lang);
51
+ }
52
+ ```
53
+
54
+ ### 4. Create the listing page
55
+
56
+ ```typescript
57
+ // src/pages/BlogPage.tsx
58
+ import { BookOpen } from 'lucide-react';
59
+ import { HeroSection } from '@donotdev/components';
60
+ import { useTranslation, type PageMeta } from '@donotdev/core';
61
+ import { BlogList } from '@donotdev/templates';
62
+ import { PageContainer } from '@donotdev/ui';
63
+ import { getBlogLoader } from '../data/blog';
64
+
65
+ export const NAMESPACE = 'blog';
66
+ export const meta: PageMeta = { namespace: NAMESPACE, icon: <BookOpen /> };
67
+
68
+ export default function BlogPage() {
69
+ const { t, i18n } = useTranslation([NAMESPACE]);
70
+ const posts = getBlogLoader(i18n?.language).getAllPosts();
71
+
72
+ return (
73
+ <PageContainer variant="docs">
74
+ <HeroSection title={t('title')} subtitle={t('subtitle')} />
75
+ <BlogList posts={posts} />
76
+ </PageContainer>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ### 5. Create the post page (dynamic route)
82
+
83
+ ```typescript
84
+ // src/pages/BlogPostPage.tsx
85
+ import { BookOpen } from 'lucide-react';
86
+ import { useTranslation, type PageMeta } from '@donotdev/core';
87
+ import { BlogPostView } from '@donotdev/templates';
88
+ import { PageContainer, useRouteParam } from '@donotdev/ui';
89
+ import { getBlogLoader } from '../data/blog';
90
+
91
+ export const NAMESPACE = 'blog';
92
+ export const meta: PageMeta = {
93
+ namespace: NAMESPACE,
94
+ icon: <BookOpen />,
95
+ route: '/blog/:slug',
96
+ hideFromMenu: true,
97
+ };
98
+
99
+ export default function BlogPostPage() {
100
+ const { i18n } = useTranslation([NAMESPACE]);
101
+ const slug = useRouteParam('slug');
102
+ const post = slug ? getBlogLoader(i18n?.language).getPostBySlug(slug) : null;
103
+
104
+ return (
105
+ <PageContainer variant="docs">
106
+ <BlogPostView post={post} />
107
+ </PageContainer>
108
+ );
109
+ }
110
+ ```
111
+
112
+ ### 6. Add locale files
113
+
114
+ Blog UI labels (`readMore`, `backToBlog`, `publishedOn`, etc.) are provided by the framework via `blog_*.json` in `@donotdev/core`. Override any key by creating the same file in your app.
115
+
116
+ Page-specific strings (title, subtitle) go in your app locale:
117
+
118
+ ```json
119
+ // src/pages/locales/blog_en.json
120
+ {
121
+ "title": "Blog",
122
+ "subtitle": "Your subtitle here"
123
+ }
124
+ ```
125
+
126
+ ---
127
+
128
+ ## File Convention
129
+
130
+ **Pattern:** `slug_lang.md`
131
+
132
+ | File | Slug | Language |
133
+ |------|------|----------|
134
+ | `my-post_en.md` | `my-post` | English |
135
+ | `my-post_fr.md` | `my-post` | French |
136
+ | `my-post_de.md` | `my-post` | German |
137
+ | `solo-post_en.md` | `solo-post` | English (no FR = falls back to EN) |
138
+
139
+ **Rules:**
140
+ - Slug = everything before the last `_xx` suffix
141
+ - Language suffix must be 2-5 characters (ISO codes)
142
+ - English (`_en`) is the fallback language
143
+ - If a post only exists in `_en`, it's shown for all languages
144
+
145
+ ---
146
+
147
+ ## Frontmatter Fields
148
+
149
+ | Field | Required | Description |
150
+ |-------|----------|-------------|
151
+ | `title` | Yes | Post title |
152
+ | `description` | Yes | Summary for listings and SEO |
153
+ | `date` | Yes | Publication date (YYYY-MM-DD) |
154
+ | `tags` | No | Comma-separated tags |
155
+ | `image` | No | Hero image path — must be in `public/blog/` (see Images section) |
156
+
157
+ Custom fields are preserved in `meta[key]`.
158
+
159
+ ---
160
+
161
+ ## Images
162
+
163
+ ### Hero Images (frontmatter `image` field)
164
+
165
+ One image per post. Used in blog listing cards, article page, RSS feed, and OG/social sharing.
166
+
167
+ **Convention:**
168
+ - **Location:** `public/blog/[slug].png` (matches the post slug)
169
+ - **Size:** 1200×630px minimum (standard OG ratio, sharp on retina)
170
+ - **Format:** PNG or JPG
171
+ - **Reference:** `image: /blog/my-post.png` in frontmatter
172
+
173
+ ```
174
+ public/
175
+ └── blog/
176
+ ├── my-first-post.png ← matches my-first-post_en.md
177
+ ├── another-post.png ← matches another-post_en.md
178
+ └── inline-diagram.svg ← used in markdown body
179
+ ```
180
+
181
+ **Where it shows:**
182
+ | Context | Sizing |
183
+ |---------|--------|
184
+ | Blog listing (featured card) | `width: 100%`, `max-height: 360px`, `object-fit: cover` |
185
+ | Blog listing (grid cards) | `width: 100%`, `max-height: 180px`, `object-fit: cover` |
186
+ | Article page | `width: 100%`, `max-height: 480px`, `object-fit: cover` |
187
+ | RSS feed | `<enclosure>` with full URL |
188
+
189
+ ### Inline Images (in markdown body)
190
+
191
+ Images referenced in markdown content also go in `public/blog/`:
192
+
193
+ ```markdown
194
+ ![Architecture diagram](/blog/inline-diagram.svg)
195
+ ```
196
+
197
+ Rendered with `loading="lazy"` and `decoding="async"`.
198
+
199
+ **Co-located images (next to .md files) do NOT work** — `?raw` imports treat markdown as text strings, not processed modules.
200
+
201
+ ---
202
+
203
+ ## Tags & Filtering
204
+
205
+ ```typescript
206
+ const blog = getBlogLoader('en');
207
+ const allTags = blog.getAllTags(); // ['react', 'typescript', ...]
208
+ const filtered = blog.getPostsByTag('react'); // Posts tagged 'react'
209
+ ```
210
+
211
+ ---
212
+
213
+ ## RSS & Sitemap
214
+
215
+ **Auto-generated at build time.** The SEO pipeline (Vite `SEOPlugin` / Next.js `SEOHandler`) automatically:
216
+
217
+ 1. Discovers `src/content/blog/*_en.md` files
218
+ 2. Appends blog post URLs to `sitemap.xml`
219
+ 3. Generates `rss.xml` with all posts
220
+
221
+ No manual scripts needed. Just drop `.md` files and build.
222
+
223
+ ---
224
+
225
+ ## Scaling (50+ Posts)
226
+
227
+ Default uses `eager: true` (all posts bundled into JS). For large blogs, switch to lazy loading:
228
+
229
+ ```typescript
230
+ // Lazy: posts loaded on demand, not bundled
231
+ const markdownFiles = import.meta.glob('../../content/blog/*.md', {
232
+ query: '?raw',
233
+ import: 'default',
234
+ eager: false, // ← Change this
235
+ });
236
+
237
+ // Now each value is () => Promise<string> instead of string
238
+ // You'll need to await them before passing to createBlogLoader
239
+ ```
240
+
241
+ For most apps (< 50 posts), eager loading is optimal.
242
+
243
+ ---
244
+
245
+ ## Auto-Deploy
246
+
247
+ Push a new `.md` file → CI builds → `import.meta.glob` picks it up at build time → deployed. No CMS, no API.
248
+
249
+ ---
250
+
251
+ ## API Reference
252
+
253
+ From `@donotdev/templates`:
254
+
255
+ | Export | Type | Description |
256
+ |--------|------|-------------|
257
+ | `createBlogLoader` | Function | Create loader from glob results + language |
258
+ | `parseFrontmatter` | Function | Parse `---` frontmatter from raw markdown |
259
+ | `BlogList` | Component | Renders post listing with cards |
260
+ | `BlogPostView` | Component | Renders single post with MarkdownViewer |
261
+ | `BlogPost` | Type | Post with slug, meta, content, readingTime, tags |
262
+ | `BlogMeta` | Type | Frontmatter metadata |
263
+ | `BlogLoader` | Type | Loader interface |
@@ -366,7 +366,7 @@ export default function ProductDetailPage() {
366
366
  return (
367
367
  <PageContainer>
368
368
  <Section>
369
- <Stack direction="row" gap="medium" align="center">
369
+ <Stack direction="row" align="center">
370
370
  <h1>{data.name}</h1>
371
371
  <Button
372
372
  variant={isFavorite(id) ? 'primary' : 'outline'}
@@ -0,0 +1,168 @@
1
+ # Setup: Firebase
2
+
3
+ **From zero to deployed in 5 steps.**
4
+
5
+ ---
6
+
7
+ ## Step 1: Run Firebase Setup
8
+
9
+ ```bash
10
+ dndev firebase:setup
11
+ ```
12
+
13
+ This command:
14
+ - Checks Firebase CLI is installed and you're logged in
15
+ - Lists your Firebase projects (or creates a new one)
16
+ - Creates a web app if the project doesn't have one
17
+ - Gets the SDK config and writes it to your app's `.env`
18
+ - Updates `.firebaserc` with the project ID
19
+ - Updates `firebase.json` placeholders
20
+
21
+ **After running, it will prompt you for 2 manual steps (below).**
22
+
23
+ ---
24
+
25
+ ## Step 2: Download Service Account Key
26
+
27
+ The CLI gives you a direct link. In short:
28
+
29
+ 1. Firebase Console → Project Settings → Service Accounts
30
+ 2. Click "Generate new private key"
31
+ 3. Save the file as `service-account-key.json` in your app root
32
+ 4. This file is `.gitignored` — never commit it
33
+
34
+ **Why?** The service account key authenticates `dndev deploy` and Cloud Functions to your Firebase project. Without it, deployment fails.
35
+
36
+ ---
37
+
38
+ ## Step 3: Enable Firebase Services
39
+
40
+ Go to the Firebase Console and enable:
41
+
42
+ **Authentication** (required):
43
+ - Get Started → Enable Email/Password
44
+ - Add OAuth providers if needed (Google, GitHub, etc.)
45
+
46
+ **Cloud Firestore** (required):
47
+ - Create Database → Start in test mode → Select region (e.g., europe-west1)
48
+ - Test mode rules expire after 30 days — Phase 4 generates proper rules
49
+
50
+ **Storage** (optional — only if your app uploads files):
51
+ - Get Started → Select region
52
+
53
+ ---
54
+
55
+ ## Step 4: Test Locally
56
+
57
+ ```bash
58
+ dndev emu start
59
+ ```
60
+
61
+ Select services: Auth + Firestore + Functions. This starts Firebase emulators so you can develop without touching production.
62
+
63
+ **Verify:** Open the app (`bun dev`), sign up, create some data. Everything runs locally.
64
+
65
+ ---
66
+
67
+ ## Step 5: Deploy
68
+
69
+ ```bash
70
+ dndev deploy
71
+ ```
72
+
73
+ This handles everything:
74
+ - Builds the app
75
+ - Deploys hosting (your frontend)
76
+ - Deploys Cloud Functions (your backend)
77
+ - Configures Cloud Run IAM automatically (fixes CORS preflight on 2nd gen functions)
78
+ - Deploys Firestore rules (if `firestore.rules` exists)
79
+
80
+ **After deploy:** Your app is live at `https://<project-id>.web.app`.
81
+
82
+ ---
83
+
84
+ ## Environment Variables
85
+
86
+ **Vite loads `.env` from the app directory only. NOT from the repo root.**
87
+
88
+ | File | What Goes Here | Loaded By |
89
+ |------|---------------|-----------|
90
+ | `apps/<app>/.env` | Firebase config, license key, Stripe publishable key | Vite (at dev + build) |
91
+ | `apps/<app>/.env.local` | Local overrides (gitignored) | Vite (overrides .env) |
92
+ | `apps/<app>/.env.production` | Production overrides | Vite (at build --mode production) |
93
+ | `apps/<app>/.env.staging` | Staging config | Vite (via `dndev staging`) |
94
+ | `functions/.env` | Server secrets: STRIPE_SECRET_KEY, OAuth secrets | Cloud Functions runtime |
95
+ | Root `.env` | **Not read by Vite.** Reference only. | Nothing |
96
+
97
+ **`dndev firebase:setup` writes Firebase vars to `apps/<app>/.env` automatically.**
98
+
99
+ **Custom domains:** Framework uses `APP_URL` hostname as `authDomain` in production (not Firebase's `projectId.firebaseapp.com`). Copy/paste `FIREBASE_AUTH_DOMAIN` from Firebase Console in both `.env.local` and `.env.production` — framework handles the rest.
100
+
101
+ ---
102
+
103
+ ## Staging Environment (Optional)
104
+
105
+ 1. Create a second Firebase project (e.g., `my-app-staging`)
106
+ 2. Run `dndev firebase:setup` again and select the staging project
107
+ 3. Add to `.firebaserc`: `{ "projects": { "staging": "my-app-staging" } }`
108
+ 4. Create `service-account-key.staging.json` (same steps as production)
109
+ 5. Deploy: `dndev staging`
110
+
111
+ ---
112
+
113
+ ## Secrets (Stripe, OAuth, etc.)
114
+
115
+ Server-side secrets go in `functions/.env`, not the app `.env`:
116
+
117
+ ```bash
118
+ # functions/.env
119
+ STRIPE_SECRET_KEY=sk_live_...
120
+ STRIPE_WEBHOOK_SECRET=whsec_...
121
+ GITHUB_CLIENT_SECRET=...
122
+ ```
123
+
124
+ Push to Firebase Secret Manager:
125
+
126
+ ```bash
127
+ dndev sync-secrets
128
+ ```
129
+
130
+ Secrets are auto-loaded by Cloud Functions at runtime. Never put server secrets in `VITE_*` variables — those are exposed to the browser.
131
+
132
+ ---
133
+
134
+ ## Cloud Run IAM (Technical Detail)
135
+
136
+ Firebase 2nd gen Cloud Functions run on Cloud Run. By default, Cloud Run blocks unauthenticated OPTIONS requests (CORS preflight). This breaks browser calls to your functions.
137
+
138
+ `dndev deploy` automatically runs `gcloud run services update --no-invoker-iam-check` on all deployed functions. Your functions still validate Firebase Auth in code — this only allows the CORS preflight to pass.
139
+
140
+ If you deploy manually with `firebase deploy`, you'll need to run this yourself. Use `dndev deploy` instead.
141
+
142
+ ---
143
+
144
+ ## Troubleshooting
145
+
146
+ **"Service account key not found"**
147
+ → Download from Firebase Console → Service Accounts → Generate new private key
148
+ → Save as `service-account-key.json` in your app root
149
+
150
+ **"Permission denied" / 401 on deploy**
151
+ → Check service account key is valid JSON with `project_id`, `private_key`, `client_email`
152
+ → Re-download if corrupted
153
+
154
+ **"CORS error" on function calls**
155
+ → Use `dndev deploy` instead of `firebase deploy` (handles IAM automatically)
156
+ → Or run: `gcloud run services update <function-name> --region=<region> --no-invoker-iam-check --project=<project-id>`
157
+
158
+ **".env values not loading"**
159
+ → Check the `.env` file is in your **app directory** (`apps/<app>/.env`), not the repo root
160
+ → All Vite vars must start with `VITE_`
161
+
162
+ **"License key not working"**
163
+ → Must be in `apps/<app>/.env` as `VITE_DONOTDEV_LICENSE_KEY=dndev_...`
164
+ → Must start with `dndev_` and be at least 20 characters
165
+
166
+ ---
167
+
168
+ **`dndev firebase:setup` → download service account key → enable Auth + Firestore → `dndev emu start` → `dndev deploy`. That's it.**
@@ -66,22 +66,15 @@ export const myFunction = onCall(FUNCTION_CONFIG, async (request) => {
66
66
 
67
67
  ## Post-Deployment: Cloud Run IAM
68
68
 
69
- **After deploying, you may see `403 Forbidden` on CORS preflight. Fix:**
69
+ **`dndev deploy` handles this automatically.** It runs `gcloud run services update --no-invoker-iam-check` on all deployed functions after each deploy.
70
70
 
71
- ```bash
72
- # PowerShell
73
- gcloud run services update create-products --region=europe-west1 --no-invoker-iam-check --project=myproject
74
- ```
71
+ If you deploy manually with `firebase deploy` (not recommended), you'll see `403 Forbidden` on CORS preflight. Fix by using `dndev deploy` instead, or run manually:
75
72
 
76
- **For all CRUD functions:**
77
- ```powershell
78
- $services = @('create-products', 'get-products', 'list-products', 'update-products', 'delete-products');
79
- foreach ($service in $services) {
80
- gcloud run services update $service --region=europe-west1 --no-invoker-iam-check --project=myproject
81
- }
73
+ ```bash
74
+ gcloud run services update <function-name> --region=<region> --no-invoker-iam-check --project=<project-id>
82
75
  ```
83
76
 
84
- **Why?** Cloud Run blocks unauthenticated OPTIONS (CORS preflight) by default. Your function still validates Firebase Auth - this only allows the preflight to pass.
77
+ **Why?** Cloud Run blocks unauthenticated OPTIONS (CORS preflight) by default. Your function still validates Firebase Auth in code — this only allows the preflight to pass.
85
78
 
86
79
  ---
87
80
 
@@ -0,0 +1,184 @@
1
+ # Testing Guide
2
+
3
+ ## Setup
4
+
5
+ ```bash
6
+ bun add -D vitest @testing-library/react @testing-library/jest-dom jsdom
7
+ ```
8
+
9
+ ### vitest.config.ts
10
+
11
+ ```ts
12
+ import { defineConfig } from 'vitest/config';
13
+ import react from '@vitejs/plugin-react';
14
+
15
+ export default defineConfig({
16
+ plugins: [react()],
17
+ test: {
18
+ environment: 'jsdom',
19
+ setupFiles: ['./tests/setup.ts'],
20
+ globals: true,
21
+ include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
22
+ },
23
+ });
24
+ ```
25
+
26
+ ### tests/setup.ts
27
+
28
+ ```ts
29
+ import '@testing-library/jest-dom';
30
+ ```
31
+
32
+ ### package.json scripts
33
+
34
+ ```json
35
+ {
36
+ "scripts": {
37
+ "test": "vitest run",
38
+ "test:watch": "vitest"
39
+ }
40
+ }
41
+ ```
42
+
43
+ ---
44
+
45
+ ## What To Test
46
+
47
+ ### 1. Entity Tests
48
+
49
+ For each entity defined in `entities/`, test:
50
+
51
+ **CRUD operations:**
52
+ ```ts
53
+ // tests/entities/Task.test.ts
54
+ import { describe, it, expect } from 'vitest';
55
+ import { TaskEntity } from '../../entities';
56
+
57
+ describe('TaskEntity', () => {
58
+ it('has correct fields', () => {
59
+ const fields = TaskEntity.fields;
60
+ expect(fields).toHaveProperty('title');
61
+ expect(fields).toHaveProperty('status');
62
+ expect(fields.title.required).toBe(true);
63
+ });
64
+
65
+ it('has correct access rules', () => {
66
+ const access = TaskEntity.access;
67
+ expect(access.create).toContain('authenticated');
68
+ expect(access.read).toContain('owner');
69
+ expect(access.delete).toContain('admin');
70
+ });
71
+ });
72
+ ```
73
+
74
+ **What to verify per entity:**
75
+ - All required fields defined
76
+ - Field types match spec
77
+ - Access rules match spec (create/read/update/delete per role)
78
+ - State transitions valid (if entity has states)
79
+ - Default values set correctly
80
+
81
+ ### 2. Page Render Tests
82
+
83
+ For each page in `src/pages/`, test that it mounts:
84
+
85
+ ```tsx
86
+ // tests/pages/DashboardPage.test.tsx
87
+ import { describe, it, expect } from 'vitest';
88
+ import { render, screen } from '@testing-library/react';
89
+ import DashboardPage from '../../src/pages/DashboardPage';
90
+
91
+ describe('DashboardPage', () => {
92
+ it('renders without crashing', () => {
93
+ render(<DashboardPage />);
94
+ expect(document.body).toBeTruthy();
95
+ });
96
+
97
+ it('has correct PageMeta', () => {
98
+ expect(DashboardPage.meta).toBeDefined();
99
+ expect(DashboardPage.meta.title).toBeTruthy();
100
+ expect(DashboardPage.meta.auth).toBe(true);
101
+ });
102
+ });
103
+ ```
104
+
105
+ **What to verify per page:**
106
+ - Page renders without error
107
+ - PageMeta is defined (title, auth requirement, admin flag)
108
+ - Route protection matches spec (public/auth/admin)
109
+
110
+ ### 3. Access Control Tests
111
+
112
+ Derive from entity permissions in the spec:
113
+
114
+ ```ts
115
+ // tests/access/access-rules.test.ts
116
+ import { describe, it, expect } from 'vitest';
117
+ import { entities } from '../../entities';
118
+
119
+ describe('Access Control', () => {
120
+ it('admin entities require admin access', () => {
121
+ // Entities marked admin-only in spec
122
+ const adminEntities = ['AuditLog', 'SystemConfig'];
123
+ for (const name of adminEntities) {
124
+ const entity = entities[name];
125
+ expect(entity.access.create).toContain('admin');
126
+ }
127
+ });
128
+
129
+ it('user-owned entities have owner access', () => {
130
+ // Entities where users own their own data
131
+ const userEntities = ['Task', 'Profile'];
132
+ for (const name of userEntities) {
133
+ const entity = entities[name];
134
+ expect(entity.access.update).toContain('owner');
135
+ }
136
+ });
137
+ });
138
+ ```
139
+
140
+ ### 4. Firestore Rules Tests
141
+
142
+ If using Firebase, generate rules from entity access definitions:
143
+
144
+ ```
145
+ // firestore.rules — generated from entities
146
+ rules_version = '2';
147
+ service cloud.firestore {
148
+ match /databases/{database}/documents {
149
+
150
+ // TaskEntity: create=authenticated, read=owner, update=owner, delete=admin
151
+ match /tasks/{taskId} {
152
+ allow create: if request.auth != null;
153
+ allow read, update: if request.auth != null && resource.data.userId == request.auth.uid;
154
+ allow delete: if request.auth.token.admin == true;
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Test Generation Checklist
163
+
164
+ The Polisher agent should generate tests for everything in the spec:
165
+
166
+ | Source | Test Type | File |
167
+ |--------|-----------|------|
168
+ | Each entity | Field validation + access rules | `tests/entities/[Entity].test.ts` |
169
+ | Each page | Render + PageMeta | `tests/pages/[Page].test.tsx` |
170
+ | Spec permissions | Access control matrix | `tests/access/access-rules.test.ts` |
171
+ | Spec auth | Auth provider config | `tests/auth/auth.test.ts` |
172
+ | Entity access | Firestore rules | `firestore.rules` |
173
+
174
+ ---
175
+
176
+ ## Running Tests
177
+
178
+ ```bash
179
+ bun test # Run all tests once
180
+ bun test:watch # Watch mode
181
+ bunx vitest --ui # Visual UI
182
+ ```
183
+
184
+ Tests should pass before calling `complete_phase({ files: [...test files...] })`.