start-vibing-stacks 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3 -13
- package/package.json +1 -1
- package/stacks/_shared/skills/playwright-automation/SKILL.md +90 -0
- package/stacks/_shared/skills/test-coverage/SKILL.md +67 -0
- package/stacks/frontend/react/skills/react-patterns/SKILL.md +113 -0
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +94 -0
- package/stacks/nodejs/skills/typescript-strict/SKILL.md +90 -0
- package/stacks/nodejs/stack.json +145 -31
- package/stacks/php/stack.json +3 -28
package/dist/index.js
CHANGED
|
@@ -146,18 +146,8 @@ async function main() {
|
|
|
146
146
|
})),
|
|
147
147
|
},
|
|
148
148
|
]);
|
|
149
|
-
// ─── Step 6: Deploy
|
|
150
|
-
const
|
|
151
|
-
{
|
|
152
|
-
type: 'list',
|
|
153
|
-
name: 'deploy',
|
|
154
|
-
message: 'Deployment target?',
|
|
155
|
-
choices: stackConfig.deployTargets.map((d) => ({
|
|
156
|
-
name: `${d.icon} ${d.name}`,
|
|
157
|
-
value: d.id,
|
|
158
|
-
})),
|
|
159
|
-
},
|
|
160
|
-
]);
|
|
149
|
+
// ─── Step 6: Deploy — GitHub (default) ──────────────────────────────────
|
|
150
|
+
const deploy = 'github';
|
|
161
151
|
// ─── Step 6b: MCP Servers ────────────────────────────────────────────
|
|
162
152
|
const selectedMcps = FLAGS.noMcp ? [] : await selectMcpServers(stackId, database);
|
|
163
153
|
// ─── Step 7: Show Summary & Confirm ────────────────────────────────────
|
|
@@ -180,7 +170,7 @@ async function main() {
|
|
|
180
170
|
'Framework': framework,
|
|
181
171
|
'Database': database,
|
|
182
172
|
'Frontend': frontend,
|
|
183
|
-
'Deploy':
|
|
173
|
+
'Deploy': '🐙 GitHub (git push)',
|
|
184
174
|
'Agents': '6 universal',
|
|
185
175
|
'Skills': `${stackConfig.skills.length} stack + shared`,
|
|
186
176
|
'Hooks': 'stop-validator + prompt-inject',
|
package/package.json
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Playwright Automation — E2E Testing
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing E2E tests or browser automation.**
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
tests/e2e/
|
|
9
|
+
├── fixtures/ # Auth, DB cleanup, custom fixtures
|
|
10
|
+
├── pages/ # Page Object Model
|
|
11
|
+
├── flows/ # User journey tests
|
|
12
|
+
├── api/ # API-only tests
|
|
13
|
+
└── playwright.config.ts
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Page Object Model
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// tests/e2e/pages/base.page.ts
|
|
20
|
+
import { type Page, type Locator, expect } from '@playwright/test';
|
|
21
|
+
|
|
22
|
+
export abstract class BasePage {
|
|
23
|
+
protected readonly page: Page;
|
|
24
|
+
constructor(page: Page) { this.page = page; }
|
|
25
|
+
|
|
26
|
+
get loadingSpinner(): Locator { return this.page.getByTestId('loading-spinner'); }
|
|
27
|
+
get errorMessage(): Locator { return this.page.getByTestId('error-message'); }
|
|
28
|
+
|
|
29
|
+
async waitForLoad(): Promise<void> {
|
|
30
|
+
await this.loadingSpinner.waitFor({ state: 'hidden' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async goto(path: string): Promise<void> {
|
|
34
|
+
await this.page.goto(path);
|
|
35
|
+
await this.waitForLoad();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Auth Fixture
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
export function generateTestUser(): TestUser {
|
|
44
|
+
const ts = Date.now();
|
|
45
|
+
const rand = Math.random().toString(36).substring(7);
|
|
46
|
+
return {
|
|
47
|
+
name: `Test User ${ts}`,
|
|
48
|
+
email: `testuser_${ts}_${rand}@test.com`,
|
|
49
|
+
password: 'TestPassword123!',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Multi-Viewport Testing
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const viewports = [
|
|
58
|
+
{ name: 'mobile', width: 375, height: 667 },
|
|
59
|
+
{ name: 'tablet', width: 768, height: 1024 },
|
|
60
|
+
{ name: 'desktop', width: 1280, height: 800 },
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
for (const viewport of viewports) {
|
|
64
|
+
test.describe(`Responsive - ${viewport.name}`, () => {
|
|
65
|
+
test.use({ viewport: { width: viewport.width, height: viewport.height } });
|
|
66
|
+
test('navigation adapts', async ({ page }) => { /* ... */ });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Required data-testid
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<input data-testid="email-input" />
|
|
75
|
+
<input data-testid="password-input" />
|
|
76
|
+
<button data-testid="submit-button" />
|
|
77
|
+
<div data-testid="error-message" />
|
|
78
|
+
<div data-testid="success-message" />
|
|
79
|
+
<div data-testid="loading-spinner" />
|
|
80
|
+
<nav data-testid="sidebar" />
|
|
81
|
+
<button data-testid="hamburger-menu" />
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## FORBIDDEN
|
|
85
|
+
|
|
86
|
+
1. **Hardcoded test data** — generate unique data with timestamps
|
|
87
|
+
2. **`.skip()` or `.only()`** — never in committed code
|
|
88
|
+
3. **No cleanup** — always track + clean created data
|
|
89
|
+
4. **Mocked auth** — use real authentication flows
|
|
90
|
+
5. **Single viewport** — test mobile + tablet + desktop
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Test Coverage — Testing Management
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke AFTER implementing any feature. Do NOT skip.**
|
|
4
|
+
|
|
5
|
+
## Critical Rules
|
|
6
|
+
|
|
7
|
+
1. **CLEANUP ALL TEST DATA** — fixture-based tracking
|
|
8
|
+
2. **VERIFY IN DATABASE** — check DB state after UI actions
|
|
9
|
+
3. **TEST ALL VIEWPORTS** — desktop, tablet, mobile minimum
|
|
10
|
+
4. **REAL AUTH ONLY** — never mock authentication
|
|
11
|
+
5. **UNIQUE DATA** — timestamps in emails/names
|
|
12
|
+
6. **NO `.skip()`** — never in committed code
|
|
13
|
+
|
|
14
|
+
## Files That NEED Tests
|
|
15
|
+
|
|
16
|
+
| Type | Test Expected | Required |
|
|
17
|
+
|------|--------------|----------|
|
|
18
|
+
| API Route | Unit + E2E | **YES** |
|
|
19
|
+
| Model/Entity | Unit | **YES** |
|
|
20
|
+
| Page/View | E2E flow | **YES** |
|
|
21
|
+
| Component (interactive) | E2E | YES |
|
|
22
|
+
| Hook/Service | Unit | YES |
|
|
23
|
+
| Utility (exported) | Unit | YES |
|
|
24
|
+
|
|
25
|
+
## Required E2E Flows
|
|
26
|
+
|
|
27
|
+
- [ ] Registration — create user, verify in DB
|
|
28
|
+
- [ ] Login/Logout — auth state changes
|
|
29
|
+
- [ ] CRUD Create — item created, visible, in DB
|
|
30
|
+
- [ ] CRUD Read — item displayed correctly
|
|
31
|
+
- [ ] CRUD Update — changes reflected in DB
|
|
32
|
+
- [ ] CRUD Delete — removed from DB
|
|
33
|
+
- [ ] Permissions — forbidden requests blocked
|
|
34
|
+
- [ ] Responsive — works on all viewports
|
|
35
|
+
|
|
36
|
+
## Stack-Specific Commands
|
|
37
|
+
|
|
38
|
+
### PHP (PHPUnit + Pest)
|
|
39
|
+
```bash
|
|
40
|
+
./vendor/bin/phpunit
|
|
41
|
+
./vendor/bin/pest --coverage --min=70
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Node.js (Vitest + Playwright)
|
|
45
|
+
```bash
|
|
46
|
+
npx vitest run --coverage
|
|
47
|
+
npx playwright test
|
|
48
|
+
npx playwright test --ui
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Before Commit Checklist
|
|
52
|
+
|
|
53
|
+
- [ ] All new features have tests?
|
|
54
|
+
- [ ] Tests use fixtures for cleanup?
|
|
55
|
+
- [ ] Database state verified after UI actions?
|
|
56
|
+
- [ ] Tests run on all viewports?
|
|
57
|
+
- [ ] Coverage threshold met (≥70%)?
|
|
58
|
+
- [ ] No `.skip()` in tests?
|
|
59
|
+
- [ ] All tests passing?
|
|
60
|
+
|
|
61
|
+
## FORBIDDEN
|
|
62
|
+
|
|
63
|
+
1. **Skipping tests** — `test.skip()`, `.only()`
|
|
64
|
+
2. **Mocking auth** — use real authentication
|
|
65
|
+
3. **Fixed test users** — `test@test.com` → generate unique
|
|
66
|
+
4. **No cleanup** — orphaned test data
|
|
67
|
+
5. **No DB validation** — trusting UI without checking DB
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# React Patterns — Modern Component Architecture
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing React components, hooks, or state management.**
|
|
4
|
+
|
|
5
|
+
## Component Patterns
|
|
6
|
+
|
|
7
|
+
### Compound Components
|
|
8
|
+
```tsx
|
|
9
|
+
const TabsContext = createContext<TabsContextValue | null>(null);
|
|
10
|
+
|
|
11
|
+
function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
|
|
12
|
+
const [activeTab, setActiveTab] = useState(defaultTab);
|
|
13
|
+
return (
|
|
14
|
+
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
|
15
|
+
<div className="tabs">{children}</div>
|
|
16
|
+
</TabsContext.Provider>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
// + TabList, Tab, TabPanel components using useContext(TabsContext)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Generic List Component
|
|
23
|
+
```tsx
|
|
24
|
+
interface ListProps<T> {
|
|
25
|
+
items: T[];
|
|
26
|
+
renderItem: (item: T) => ReactNode;
|
|
27
|
+
keyExtractor: (item: T) => string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
|
|
31
|
+
return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Custom Hooks
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// useLocalStorage
|
|
39
|
+
function useLocalStorage<T>(key: string, initial: T) {
|
|
40
|
+
const [value, setValue] = useState<T>(() => {
|
|
41
|
+
if (typeof window === 'undefined') return initial;
|
|
42
|
+
try { return JSON.parse(window.localStorage.getItem(key) ?? '') } catch { return initial }
|
|
43
|
+
});
|
|
44
|
+
const set = useCallback((v: T | ((val: T) => T)) => {
|
|
45
|
+
setValue(prev => {
|
|
46
|
+
const next = v instanceof Function ? v(prev) : v;
|
|
47
|
+
window.localStorage.setItem(key, JSON.stringify(next));
|
|
48
|
+
return next;
|
|
49
|
+
});
|
|
50
|
+
}, [key]);
|
|
51
|
+
return [value, set] as const;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// useDebounce
|
|
55
|
+
function useDebounce<T>(value: T, delay: number): T {
|
|
56
|
+
const [debounced, setDebounced] = useState(value);
|
|
57
|
+
useEffect(() => { const t = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(t); }, [value, delay]);
|
|
58
|
+
return debounced;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## State Management
|
|
63
|
+
|
|
64
|
+
### useReducer for Complex State
|
|
65
|
+
```tsx
|
|
66
|
+
type Action =
|
|
67
|
+
| { type: 'FETCH_START' }
|
|
68
|
+
| { type: 'FETCH_SUCCESS'; payload: Item[] }
|
|
69
|
+
| { type: 'FETCH_ERROR'; payload: string };
|
|
70
|
+
|
|
71
|
+
function reducer(state: State, action: Action): State {
|
|
72
|
+
switch (action.type) {
|
|
73
|
+
case 'FETCH_START': return { ...state, loading: true, error: null };
|
|
74
|
+
case 'FETCH_SUCCESS': return { ...state, loading: false, items: action.payload };
|
|
75
|
+
case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Context (avoid prop drilling)
|
|
81
|
+
```tsx
|
|
82
|
+
const AppContext = createContext<AppContextValue | undefined>(undefined);
|
|
83
|
+
export function useApp() {
|
|
84
|
+
const ctx = useContext(AppContext);
|
|
85
|
+
if (!ctx) throw new Error('useApp must be used within AppProvider');
|
|
86
|
+
return ctx;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Performance
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// memo — prevent re-renders
|
|
94
|
+
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) { ... });
|
|
95
|
+
|
|
96
|
+
// useMemo — expensive calculations
|
|
97
|
+
const processed = useMemo(() => data.map(d => expensiveCalc(d)), [data]);
|
|
98
|
+
|
|
99
|
+
// useCallback — stable refs
|
|
100
|
+
const handleClick = useCallback(() => setCount(c => c + 1), []);
|
|
101
|
+
|
|
102
|
+
// Code splitting
|
|
103
|
+
const Heavy = lazy(() => import('./HeavyComponent'));
|
|
104
|
+
<Suspense fallback={<Loading />}><Heavy /></Suspense>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## FORBIDDEN
|
|
108
|
+
|
|
109
|
+
1. **Class components** — function components only
|
|
110
|
+
2. **Prop drilling** — use context or composition
|
|
111
|
+
3. **Inline objects/functions in JSX** — causes re-renders
|
|
112
|
+
4. **useEffect for derived state** — use useMemo
|
|
113
|
+
5. **Mutating state directly** — always setState/dispatch
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Tailwind Patterns — Utility-First CSS
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing Tailwind CSS classes.**
|
|
4
|
+
|
|
5
|
+
## Mobile-First Breakpoints
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
sm: 640px → md: 768px → lg: 1024px → xl: 1280px → 2xl: 1536px
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<div className="w-full md:w-1/2 lg:w-1/3 xl:w-1/4">
|
|
13
|
+
{/* Mobile: full → Tablet: half → Desktop: third → Large: quarter */}
|
|
14
|
+
</div>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Layout Patterns
|
|
18
|
+
|
|
19
|
+
### Flexbox
|
|
20
|
+
```tsx
|
|
21
|
+
// Center
|
|
22
|
+
<div className="flex items-center justify-center h-screen" />
|
|
23
|
+
|
|
24
|
+
// Responsive direction
|
|
25
|
+
<div className="flex flex-col md:flex-row gap-4" />
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Grid
|
|
29
|
+
```tsx
|
|
30
|
+
// Responsive grid
|
|
31
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" />
|
|
32
|
+
|
|
33
|
+
// Auto-fit
|
|
34
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-4" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Dark Mode
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
|
41
|
+
<p className="text-gray-600 dark:text-gray-300">Adapts to theme</p>
|
|
42
|
+
</div>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Component Patterns
|
|
46
|
+
|
|
47
|
+
### Card
|
|
48
|
+
```tsx
|
|
49
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Button
|
|
53
|
+
```tsx
|
|
54
|
+
<button className="bg-primary text-primary-foreground px-4 py-2 rounded-md font-medium hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Input
|
|
58
|
+
```tsx
|
|
59
|
+
<input className="w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md text-gray-900 dark:text-white placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Animations
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<Loader className="h-5 w-5 animate-spin" /> // Spinner
|
|
66
|
+
<div className="h-4 w-24 bg-gray-200 animate-pulse" /> // Skeleton
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Custom (tailwind.config.js)
|
|
70
|
+
```js
|
|
71
|
+
animation: {
|
|
72
|
+
'fade-in': 'fadeIn 0.3s ease-in-out',
|
|
73
|
+
'slide-up': 'slideUp 0.3s ease-out',
|
|
74
|
+
},
|
|
75
|
+
keyframes: {
|
|
76
|
+
fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } },
|
|
77
|
+
slideUp: { '0%': { transform: 'translateY(10px)', opacity: '0' }, '100%': { transform: 'translateY(0)', opacity: '1' } },
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Show/Hide Responsive
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<div className="hidden lg:block">Desktop only</div>
|
|
85
|
+
<div className="block lg:hidden">Mobile only</div>
|
|
86
|
+
<span className="sr-only">Screen reader only</span>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## FORBIDDEN
|
|
90
|
+
|
|
91
|
+
1. **`!important`** — fix specificity properly
|
|
92
|
+
2. **Arbitrary values when utilities exist** — use the scale
|
|
93
|
+
3. **Mixing raw CSS with Tailwind** — stick to utilities
|
|
94
|
+
4. **Inconsistent spacing** — follow the 4px scale (p-1=4px, p-2=8px, p-4=16px)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# TypeScript Strict — Type Safety Patterns
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing .ts/.tsx files.**
|
|
4
|
+
|
|
5
|
+
## Required tsconfig.json
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"compilerOptions": {
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"noImplicitAny": true,
|
|
13
|
+
"strictNullChecks": true,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"exactOptionalPropertyTypes": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Index Access
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// WRONG
|
|
25
|
+
const port = process.env.PORT;
|
|
26
|
+
|
|
27
|
+
// CORRECT — bracket notation
|
|
28
|
+
const port = process.env['PORT'];
|
|
29
|
+
const host = process.env['HOST'] ?? 'localhost';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Null Handling
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// Optional chaining + nullish coalescing
|
|
36
|
+
const avatar = user.profile?.avatar ?? '/default.png';
|
|
37
|
+
|
|
38
|
+
// WRONG — || treats 0, '', false as falsy
|
|
39
|
+
const count = input || 10;
|
|
40
|
+
// CORRECT — ?? only null/undefined
|
|
41
|
+
const count = input ?? 10;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Type Guards
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
function isUser(value: unknown): value is User {
|
|
48
|
+
return typeof value === 'object' && value !== null && 'id' in value && 'email' in value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Assertion function
|
|
52
|
+
function assertDefined<T>(value: T | undefined | null, msg: string): asserts value is T {
|
|
53
|
+
if (value == null) throw new Error(msg);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Const Assertions
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Exact types (not widened)
|
|
61
|
+
const endpoints = {
|
|
62
|
+
users: '/api/users',
|
|
63
|
+
posts: '/api/posts',
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
// Discriminated unions
|
|
67
|
+
type Result<T> =
|
|
68
|
+
| { success: true; data: T }
|
|
69
|
+
| { success: false; error: string };
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Generic Patterns
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Constrained
|
|
76
|
+
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
|
|
77
|
+
return items.find(item => item.id === id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// RequireFields utility
|
|
81
|
+
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## FORBIDDEN
|
|
85
|
+
|
|
86
|
+
1. **`any`** — use `unknown` instead
|
|
87
|
+
2. **Non-null assertion without certainty** — check first
|
|
88
|
+
3. **Type assertion to hide errors** — fix the actual issue
|
|
89
|
+
4. **`@ts-ignore`** — use `@ts-expect-error` with comment
|
|
90
|
+
5. **Ignoring strict errors** — always fix properly
|
package/stacks/nodejs/stack.json
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "nodejs",
|
|
3
3
|
"name": "Node.js / TypeScript",
|
|
4
|
-
"icon": "
|
|
4
|
+
"icon": "\ud83d\udce6",
|
|
5
5
|
"runtime": "Bun / Node.js 20+",
|
|
6
6
|
"minVersion": "20.0.0",
|
|
7
7
|
"packageManager": "bun|npm|pnpm",
|
|
8
|
-
"extensions": [
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
"extensions": [
|
|
9
|
+
".ts",
|
|
10
|
+
".tsx",
|
|
11
|
+
".js",
|
|
12
|
+
".jsx",
|
|
13
|
+
".mjs",
|
|
14
|
+
".cjs"
|
|
15
|
+
],
|
|
16
|
+
"testExtensions": [
|
|
17
|
+
"*.test.ts",
|
|
18
|
+
"*.spec.ts",
|
|
19
|
+
"*.test.tsx"
|
|
20
|
+
],
|
|
21
|
+
"detectFiles": [
|
|
22
|
+
"package.json",
|
|
23
|
+
"tsconfig.json",
|
|
24
|
+
"bun.lockb",
|
|
25
|
+
"next.config.js"
|
|
26
|
+
],
|
|
11
27
|
"commands": {
|
|
12
28
|
"test": "bun run test",
|
|
13
29
|
"lint": "bun run lint",
|
|
@@ -17,41 +33,139 @@
|
|
|
17
33
|
"typecheck": "bun run typecheck"
|
|
18
34
|
},
|
|
19
35
|
"qualityGates": [
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
{
|
|
37
|
+
"name": "TypeCheck",
|
|
38
|
+
"command": "bun run typecheck",
|
|
39
|
+
"required": true,
|
|
40
|
+
"order": 1
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "Lint",
|
|
44
|
+
"command": "bun run lint",
|
|
45
|
+
"required": true,
|
|
46
|
+
"order": 2
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "Tests",
|
|
50
|
+
"command": "bun run test",
|
|
51
|
+
"required": true,
|
|
52
|
+
"order": 3
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "Build",
|
|
56
|
+
"command": "bun run build",
|
|
57
|
+
"required": true,
|
|
58
|
+
"order": 4
|
|
59
|
+
}
|
|
24
60
|
],
|
|
25
61
|
"frameworks": [
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
62
|
+
{
|
|
63
|
+
"id": "nextjs",
|
|
64
|
+
"name": "Next.js (App Router)",
|
|
65
|
+
"icon": "\u25b2",
|
|
66
|
+
"detectFiles": [
|
|
67
|
+
"next.config.js",
|
|
68
|
+
"next.config.ts",
|
|
69
|
+
"next.config.mjs"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "nuxt",
|
|
74
|
+
"name": "Nuxt",
|
|
75
|
+
"icon": "\ud83d\udc9a",
|
|
76
|
+
"detectFiles": [
|
|
77
|
+
"nuxt.config.ts"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "astro",
|
|
82
|
+
"name": "Astro",
|
|
83
|
+
"icon": "\ud83d\ude80",
|
|
84
|
+
"detectFiles": [
|
|
85
|
+
"astro.config.mjs"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "express",
|
|
90
|
+
"name": "Express",
|
|
91
|
+
"icon": "\u26a1"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "fastify",
|
|
95
|
+
"name": "Fastify",
|
|
96
|
+
"icon": "\ud83c\udfce\ufe0f"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "vanilla",
|
|
100
|
+
"name": "Vanilla Node.js",
|
|
101
|
+
"icon": "\ud83d\udcc4"
|
|
102
|
+
}
|
|
32
103
|
],
|
|
33
104
|
"databases": [
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
105
|
+
{
|
|
106
|
+
"id": "mongodb",
|
|
107
|
+
"name": "MongoDB",
|
|
108
|
+
"icon": "\ud83c\udf43"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "postgresql",
|
|
112
|
+
"name": "PostgreSQL",
|
|
113
|
+
"icon": "\ud83d\udc18"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "mysql",
|
|
117
|
+
"name": "MySQL / MariaDB",
|
|
118
|
+
"icon": "\ud83d\udc2c"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "sqlite",
|
|
122
|
+
"name": "SQLite (Turso / libSQL)",
|
|
123
|
+
"icon": "\ud83d\udcc1"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "redis",
|
|
127
|
+
"name": "Redis (Upstash)",
|
|
128
|
+
"icon": "\ud83d\udd34"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": "none",
|
|
132
|
+
"name": "None",
|
|
133
|
+
"icon": "\u274c"
|
|
134
|
+
}
|
|
40
135
|
],
|
|
41
136
|
"frontendOptions": [
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
137
|
+
{
|
|
138
|
+
"id": "react-tailwind",
|
|
139
|
+
"name": "React 19+ / TailwindCSS 4+",
|
|
140
|
+
"icon": "\u269b\ufe0f"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "vue",
|
|
144
|
+
"name": "Vue.js / Nuxt",
|
|
145
|
+
"icon": "\ud83d\udc9a"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "svelte",
|
|
149
|
+
"name": "Svelte / SvelteKit",
|
|
150
|
+
"icon": "\ud83d\udd25"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "shadcn",
|
|
154
|
+
"name": "shadcn/ui + Tailwind",
|
|
155
|
+
"icon": "\ud83c\udfa8"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"id": "none",
|
|
159
|
+
"name": "API only \u2014 no frontend",
|
|
160
|
+
"icon": "\u274c"
|
|
161
|
+
}
|
|
47
162
|
],
|
|
48
163
|
"deployTargets": [
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{ "id": "netlify", "name": "Netlify", "icon": "🌐" }
|
|
164
|
+
{
|
|
165
|
+
"id": "github",
|
|
166
|
+
"name": "GitHub (git push)",
|
|
167
|
+
"icon": "\ud83d\udc19"
|
|
168
|
+
}
|
|
55
169
|
],
|
|
56
170
|
"skills": [
|
|
57
171
|
"typescript-strict",
|
package/stacks/php/stack.json
CHANGED
|
@@ -150,34 +150,9 @@
|
|
|
150
150
|
],
|
|
151
151
|
"deployTargets": [
|
|
152
152
|
{
|
|
153
|
-
"id": "
|
|
154
|
-
"name": "
|
|
155
|
-
"icon": "\
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
"id": "docker",
|
|
159
|
-
"name": "Docker",
|
|
160
|
-
"icon": "\ud83d\udc33"
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
"id": "vps-ssh",
|
|
164
|
-
"name": "VPS (SSH)",
|
|
165
|
-
"icon": "\u2601\ufe0f"
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
"id": "cpanel",
|
|
169
|
-
"name": "cPanel",
|
|
170
|
-
"icon": "\ud83d\udce6"
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
"id": "forge",
|
|
174
|
-
"name": "Laravel Forge",
|
|
175
|
-
"icon": "\ud83d\udd28"
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
"id": "vapor",
|
|
179
|
-
"name": "Laravel Vapor (AWS)",
|
|
180
|
-
"icon": "\u2601\ufe0f"
|
|
153
|
+
"id": "github",
|
|
154
|
+
"name": "GitHub (git push)",
|
|
155
|
+
"icon": "\ud83d\udc19"
|
|
181
156
|
}
|
|
182
157
|
],
|
|
183
158
|
"skills": [
|