@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.
- package/dependencies-matrix.json +30 -116
- package/dist/bin/commands/bump.js +33 -7
- package/dist/bin/commands/create-project.js +43 -7
- package/dist/bin/commands/deploy.js +7606 -17
- package/dist/bin/commands/firebase-setup.d.ts +6 -0
- package/dist/bin/commands/firebase-setup.d.ts.map +1 -0
- package/dist/bin/commands/firebase-setup.js +7 -0
- package/dist/bin/commands/firebase-setup.js.map +1 -0
- package/dist/bin/commands/staging.d.ts +11 -0
- package/dist/bin/commands/staging.d.ts.map +1 -0
- package/dist/bin/commands/staging.js +12 -0
- package/dist/bin/commands/staging.js.map +1 -0
- package/dist/bin/dndev.js +28 -3
- package/dist/bin/donotdev.js +28 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7671 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/app-demo/src/pages/DetailPage.tsx.example +1 -1
- package/templates/app-demo/src/pages/FullPage.tsx.example +3 -3
- package/templates/app-demo/src/pages/HomePage.tsx.example +1 -1
- package/templates/app-demo/src/pages/components/ComponentRenderer.tsx.example +5 -5
- package/templates/app-demo/src/pages/components/DemoLayout.tsx.example +3 -3
- package/templates/app-next/.env.example +2 -0
- package/templates/app-next/src/pages/HomePage.tsx.example +1 -1
- package/templates/app-vite/.env.example +2 -0
- package/templates/app-vite/src/pages/HomePage.tsx.example +163 -73
- package/templates/functions-firebase/build.mjs.example +26 -10
- package/templates/functions-firebase/functions-firebase/build.mjs.example +26 -10
- package/templates/functions-firebase/functions.config.js.example +11 -15
- package/templates/github-consumer/.github/workflows/ci.yml.example +36 -0
- package/templates/root-consumer/.claude/agents/architect.md.example +2 -2
- package/templates/root-consumer/.claude/agents/builder.md.example +2 -2
- package/templates/root-consumer/.claude/agents/coder.md.example +2 -2
- package/templates/root-consumer/.claude/agents/extractor.md.example +2 -3
- package/templates/root-consumer/.claude/agents/polisher.md.example +67 -291
- package/templates/root-consumer/.claude/agents/prompt-engineer.md.example +4 -4
- package/templates/root-consumer/.claude/commands/build.md.example +2 -2
- package/templates/root-consumer/.claude/commands/polish.md.example +65 -81
- package/templates/root-consumer/.env.example +13 -13
- package/templates/root-consumer/.gemini/settings.json.example +9 -0
- package/templates/root-consumer/.gitignore.example +3 -1
- package/templates/root-consumer/AI.md.example +139 -0
- package/templates/root-consumer/CLAUDE.md.example +13 -104
- package/templates/root-consumer/README.md.example +81 -255
- package/templates/root-consumer/entities/Contact.ts.example +126 -0
- package/templates/root-consumer/entities/index.ts.example +6 -3
- package/templates/root-consumer/guides/dndev/AGENT_START_HERE.md.example +41 -342
- package/templates/root-consumer/guides/dndev/COMPONENTS_ADV.md.example +2 -1
- package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +144 -9
- package/templates/root-consumer/guides/dndev/INDEX.md.example +9 -0
- package/templates/root-consumer/guides/dndev/SETUP_APP_CONFIG.md.example +13 -16
- package/templates/root-consumer/guides/dndev/SETUP_BLOG.md.example +263 -0
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +1 -1
- package/templates/root-consumer/guides/dndev/SETUP_FIREBASE.md.example +168 -0
- package/templates/root-consumer/guides/dndev/SETUP_FUNCTIONS.md.example +5 -12
- package/templates/root-consumer/guides/dndev/SETUP_TESTING.md.example +184 -0
- package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +134 -69
- package/templates/root-consumer/guides/wai-way/agents/polisher.md.example +66 -44
- package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +18 -1
- package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +1 -0
- package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +2 -1
- package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +2 -1
- package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +180 -108
- package/templates/root-consumer/guides/wai-way/context_map.json.example +8 -7
- 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: 
|
|
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
|
+

|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
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...] })`.
|