opencastle 0.9.2 → 0.10.1
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/README.md +12 -69
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +13 -7
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +2 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.d.ts +17 -0
- package/dist/cli/init.test.d.ts.map +1 -0
- package/dist/cli/init.test.js +881 -0
- package/dist/cli/init.test.js.map +1 -0
- package/dist/cli/mcp.d.ts +9 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +56 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/run/adapters/copilot.d.ts +10 -2
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +83 -56
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run.js +2 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/stack-config-update.test.d.ts +2 -0
- package/dist/cli/stack-config-update.test.d.ts.map +1 -0
- package/dist/cli/stack-config-update.test.js +185 -0
- package/dist/cli/stack-config-update.test.js.map +1 -0
- package/dist/cli/stack-config.d.ts +27 -0
- package/dist/cli/stack-config.d.ts.map +1 -1
- package/dist/cli/stack-config.js +80 -27
- package/dist/cli/stack-config.js.map +1 -1
- package/dist/cli/types.d.ts +1 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +184 -17
- package/dist/cli/update.js.map +1 -1
- package/dist/orchestrator/plugins/astro/config.d.ts +3 -0
- package/dist/orchestrator/plugins/astro/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/astro/config.js +27 -0
- package/dist/orchestrator/plugins/astro/config.js.map +1 -0
- package/dist/orchestrator/plugins/chrome-devtools/config.js +2 -2
- package/dist/orchestrator/plugins/chrome-devtools/config.js.map +1 -1
- package/dist/orchestrator/plugins/contentful/config.js +1 -1
- package/dist/orchestrator/plugins/contentful/config.js.map +1 -1
- package/dist/orchestrator/plugins/convex/config.js +1 -1
- package/dist/orchestrator/plugins/convex/config.js.map +1 -1
- package/dist/orchestrator/plugins/cypress/config.d.ts +3 -0
- package/dist/orchestrator/plugins/cypress/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/cypress/config.js +15 -0
- package/dist/orchestrator/plugins/cypress/config.js.map +1 -0
- package/dist/orchestrator/plugins/figma/config.d.ts +3 -0
- package/dist/orchestrator/plugins/figma/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/figma/config.js +33 -0
- package/dist/orchestrator/plugins/figma/config.js.map +1 -0
- package/dist/orchestrator/plugins/index.d.ts.map +1 -1
- package/dist/orchestrator/plugins/index.js +20 -0
- package/dist/orchestrator/plugins/index.js.map +1 -1
- package/dist/orchestrator/plugins/jira/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/jira/config.js +2 -3
- package/dist/orchestrator/plugins/jira/config.js.map +1 -1
- package/dist/orchestrator/plugins/linear/config.js +2 -2
- package/dist/orchestrator/plugins/linear/config.js.map +1 -1
- package/dist/orchestrator/plugins/netlify/config.d.ts +3 -0
- package/dist/orchestrator/plugins/netlify/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/netlify/config.js +30 -0
- package/dist/orchestrator/plugins/netlify/config.js.map +1 -0
- package/dist/orchestrator/plugins/nextjs/config.d.ts +3 -0
- package/dist/orchestrator/plugins/nextjs/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/nextjs/config.js +35 -0
- package/dist/orchestrator/plugins/nextjs/config.js.map +1 -0
- package/dist/orchestrator/plugins/nx/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/nx/config.js +2 -3
- package/dist/orchestrator/plugins/nx/config.js.map +1 -1
- package/dist/orchestrator/plugins/playwright/config.d.ts +3 -0
- package/dist/orchestrator/plugins/playwright/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/playwright/config.js +25 -0
- package/dist/orchestrator/plugins/playwright/config.js.map +1 -0
- package/dist/orchestrator/plugins/prisma/config.d.ts +3 -0
- package/dist/orchestrator/plugins/prisma/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/prisma/config.js +25 -0
- package/dist/orchestrator/plugins/prisma/config.js.map +1 -0
- package/dist/orchestrator/plugins/resend/config.d.ts +3 -0
- package/dist/orchestrator/plugins/resend/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/resend/config.js +46 -0
- package/dist/orchestrator/plugins/resend/config.js.map +1 -0
- package/dist/orchestrator/plugins/sanity/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/sanity/config.js +1 -2
- package/dist/orchestrator/plugins/sanity/config.js.map +1 -1
- package/dist/orchestrator/plugins/slack/config.js +1 -1
- package/dist/orchestrator/plugins/slack/config.js.map +1 -1
- package/dist/orchestrator/plugins/strapi/config.js +1 -1
- package/dist/orchestrator/plugins/strapi/config.js.map +1 -1
- package/dist/orchestrator/plugins/supabase/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/supabase/config.js +1 -2
- package/dist/orchestrator/plugins/supabase/config.js.map +1 -1
- package/dist/orchestrator/plugins/teams/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/teams/config.js +1 -2
- package/dist/orchestrator/plugins/teams/config.js.map +1 -1
- package/dist/orchestrator/plugins/turborepo/config.d.ts +3 -0
- package/dist/orchestrator/plugins/turborepo/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/turborepo/config.js +15 -0
- package/dist/orchestrator/plugins/turborepo/config.js.map +1 -0
- package/dist/orchestrator/plugins/types.d.ts +7 -7
- package/dist/orchestrator/plugins/types.d.ts.map +1 -1
- package/dist/orchestrator/plugins/vercel/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/vercel/config.js +2 -3
- package/dist/orchestrator/plugins/vercel/config.js.map +1 -1
- package/dist/orchestrator/plugins/vitest/config.d.ts +3 -0
- package/dist/orchestrator/plugins/vitest/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/vitest/config.js +15 -0
- package/dist/orchestrator/plugins/vitest/config.js.map +1 -0
- package/package.json +2 -1
- package/src/cli/doctor.ts +14 -7
- package/src/cli/init.test.ts +1141 -0
- package/src/cli/init.ts +2 -1
- package/src/cli/mcp.ts +77 -1
- package/src/cli/run/adapters/copilot.ts +86 -58
- package/src/cli/run.ts +2 -2
- package/src/cli/stack-config-update.test.ts +210 -0
- package/src/cli/stack-config.ts +110 -37
- package/src/cli/types.ts +1 -1
- package/src/cli/update.ts +230 -23
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/agents/api-designer.agent.md +1 -11
- package/src/orchestrator/agents/architect.agent.md +1 -9
- package/src/orchestrator/agents/content-engineer.agent.md +1 -5
- package/src/orchestrator/agents/copywriter.agent.md +1 -9
- package/src/orchestrator/agents/data-expert.agent.md +2 -6
- package/src/orchestrator/agents/database-engineer.agent.md +1 -6
- package/src/orchestrator/agents/developer.agent.md +2 -12
- package/src/orchestrator/agents/devops-expert.agent.md +1 -5
- package/src/orchestrator/agents/documentation-writer.agent.md +1 -4
- package/src/orchestrator/agents/performance-expert.agent.md +1 -5
- package/src/orchestrator/agents/release-manager.agent.md +1 -11
- package/src/orchestrator/agents/researcher.agent.md +1 -4
- package/src/orchestrator/agents/security-expert.agent.md +2 -7
- package/src/orchestrator/agents/seo-specialist.agent.md +1 -10
- package/src/orchestrator/agents/testing-expert.agent.md +2 -11
- package/src/orchestrator/agents/ui-ux-expert.agent.md +3 -10
- package/src/orchestrator/customizations/README.md +2 -1
- package/src/orchestrator/customizations/agents/skill-matrix.json +106 -0
- package/src/orchestrator/customizations/agents/skill-matrix.md +58 -121
- package/src/orchestrator/instructions/general.instructions.md +1 -1
- package/src/orchestrator/plugins/astro/SKILL.md +288 -0
- package/src/orchestrator/plugins/astro/config.ts +28 -0
- package/src/orchestrator/plugins/chrome-devtools/config.ts +2 -2
- package/src/orchestrator/plugins/contentful/config.ts +1 -1
- package/src/orchestrator/plugins/convex/config.ts +1 -1
- package/src/orchestrator/plugins/cypress/SKILL.md +145 -0
- package/src/orchestrator/plugins/cypress/config.ts +16 -0
- package/src/orchestrator/plugins/figma/SKILL.md +85 -0
- package/src/orchestrator/plugins/figma/config.ts +34 -0
- package/src/orchestrator/plugins/index.ts +20 -0
- package/src/orchestrator/plugins/jira/config.ts +2 -3
- package/src/orchestrator/plugins/linear/config.ts +2 -2
- package/src/orchestrator/plugins/netlify/SKILL.md +134 -0
- package/src/orchestrator/plugins/netlify/config.ts +31 -0
- package/src/orchestrator/plugins/nextjs/SKILL.md +376 -0
- package/src/orchestrator/plugins/nextjs/config.ts +36 -0
- package/src/orchestrator/plugins/nx/config.ts +2 -3
- package/src/orchestrator/plugins/playwright/SKILL.md +191 -0
- package/src/orchestrator/plugins/playwright/config.ts +26 -0
- package/src/orchestrator/plugins/prisma/SKILL.md +137 -0
- package/src/orchestrator/plugins/prisma/config.ts +26 -0
- package/src/orchestrator/plugins/resend/SKILL.md +187 -0
- package/src/orchestrator/plugins/resend/config.ts +47 -0
- package/src/orchestrator/plugins/sanity/config.ts +1 -2
- package/src/orchestrator/plugins/slack/config.ts +1 -1
- package/src/orchestrator/plugins/strapi/config.ts +1 -1
- package/src/orchestrator/plugins/supabase/config.ts +1 -2
- package/src/orchestrator/plugins/teams/config.ts +1 -2
- package/src/orchestrator/plugins/turborepo/SKILL.md +121 -0
- package/src/orchestrator/plugins/turborepo/config.ts +16 -0
- package/src/orchestrator/plugins/types.ts +7 -7
- package/src/orchestrator/plugins/vercel/SKILL.md +99 -0
- package/src/orchestrator/plugins/vercel/config.ts +2 -3
- package/src/orchestrator/plugins/vitest/SKILL.md +166 -0
- package/src/orchestrator/plugins/vitest/config.ts +16 -0
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +6 -4
- package/src/orchestrator/prompts/create-skill.prompt.md +6 -7
- package/src/orchestrator/prompts/generate-task-spec.prompt.md +1 -1
- package/src/orchestrator/skills/agent-hooks/SKILL.md +2 -2
- package/src/orchestrator/skills/memory-merger/SKILL.md +1 -1
- package/src/orchestrator/skills/nextjs-patterns/SKILL.md +0 -200
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextjs-framework
|
|
3
|
+
description: "Next.js framework best practices covering App Router, server/client components, data fetching, caching, rendering strategies, middleware, configuration, and deployment. Use when creating or modifying Next.js pages, layouts, route handlers, Server Actions, or project configuration."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .github/customizations/ directory instead. -->
|
|
7
|
+
|
|
8
|
+
# Next.js Framework
|
|
9
|
+
|
|
10
|
+
## Project Structure
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
├── app/ # App Router (file-based routing)
|
|
14
|
+
│ ├── layout.tsx # Root layout (required)
|
|
15
|
+
│ ├── page.tsx # Home route → /
|
|
16
|
+
│ ├── loading.tsx # Loading UI (Suspense boundary)
|
|
17
|
+
│ ├── error.tsx # Error boundary (Client Component)
|
|
18
|
+
│ ├── not-found.tsx # 404 page
|
|
19
|
+
│ ├── global-error.tsx # Root error boundary
|
|
20
|
+
│ ├── (marketing)/ # Route group (no URL segment)
|
|
21
|
+
│ │ ├── about/page.tsx # → /about
|
|
22
|
+
│ │ └── blog/page.tsx # → /blog
|
|
23
|
+
│ ├── dashboard/
|
|
24
|
+
│ │ ├── layout.tsx # Nested layout
|
|
25
|
+
│ │ ├── page.tsx # → /dashboard
|
|
26
|
+
│ │ └── [id]/page.tsx # → /dashboard/:id
|
|
27
|
+
│ └── api/
|
|
28
|
+
│ └── users/route.ts # API route handler
|
|
29
|
+
├── components/ # Shared React components
|
|
30
|
+
├── lib/ # Utilities, helpers, server logic
|
|
31
|
+
├── public/ # Static assets (served as-is)
|
|
32
|
+
├── next.config.ts # Next.js configuration
|
|
33
|
+
├── middleware.ts # Edge middleware
|
|
34
|
+
└── .env.local # Environment variables (not committed)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **Route Groups** `(name)` — organize routes without affecting the URL.
|
|
38
|
+
- **Private Folders** `_internal` — opt out of routing entirely.
|
|
39
|
+
- **Parallel Routes** `@modal` — render multiple pages in the same layout.
|
|
40
|
+
- **Intercepting Routes** `(.)photo` — intercept navigation to show modals.
|
|
41
|
+
|
|
42
|
+
## Rendering Strategies
|
|
43
|
+
|
|
44
|
+
Next.js supports multiple rendering strategies per route:
|
|
45
|
+
|
|
46
|
+
| Strategy | When | How |
|
|
47
|
+
|----------|------|-----|
|
|
48
|
+
| **Static (SSG)** | Build time | Default for pages with no dynamic data |
|
|
49
|
+
| **Incremental Static Regeneration (ISR)** | Build + revalidation | `fetch` with `next: { revalidate: N }` or route segment config |
|
|
50
|
+
| **Server-Side Rendering (SSR)** | Every request | `export const dynamic = 'force-dynamic'` or dynamic functions (`cookies()`, `headers()`) |
|
|
51
|
+
| **Client-Side Rendering (CSR)** | Browser | `'use client'` components with `useEffect`/SWR |
|
|
52
|
+
| **Streaming** | Progressive | `<Suspense>` boundaries + `loading.tsx` |
|
|
53
|
+
| **Partial Prerendering (PPR)** | Build + streaming | Static shell with dynamic holes via `<Suspense>` |
|
|
54
|
+
|
|
55
|
+
### Route Segment Config
|
|
56
|
+
|
|
57
|
+
Control per-route rendering behavior:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// app/dashboard/page.tsx
|
|
61
|
+
export const dynamic = 'force-dynamic'; // SSR every request
|
|
62
|
+
export const revalidate = 60; // ISR: revalidate every 60s
|
|
63
|
+
export const fetchCache = 'default-cache'; // Cache fetch requests
|
|
64
|
+
export const runtime = 'nodejs'; // 'nodejs' | 'edge'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Server and Client Components
|
|
68
|
+
|
|
69
|
+
**Default: Server Components** — data fetching, heavy logic, non-interactive UI.
|
|
70
|
+
|
|
71
|
+
**Client Components** — add `'use client'` at top. Use for interactivity, state, browser APIs.
|
|
72
|
+
|
|
73
|
+
### Decision Table
|
|
74
|
+
|
|
75
|
+
| Need | Component Type | Why |
|
|
76
|
+
|------|---------------|-----|
|
|
77
|
+
| Fetch data at request time | Server | Direct DB/API access, no client waterfall |
|
|
78
|
+
| Read cookies/headers | Server | Available only on the server |
|
|
79
|
+
| Interactive UI (clicks, inputs) | Client | Requires event handlers |
|
|
80
|
+
| Use `useState` / `useEffect` | Client | React hooks need client runtime |
|
|
81
|
+
| Access browser APIs (localStorage, geolocation) | Client | Not available on server |
|
|
82
|
+
| Render static/non-interactive content | Server | Smaller bundle, faster paint |
|
|
83
|
+
| Show loading spinners for async children | Server (with `<Suspense>`) | Streams HTML progressively |
|
|
84
|
+
|
|
85
|
+
### Critical Rule
|
|
86
|
+
|
|
87
|
+
**Never use `next/dynamic` with `{ ssr: false }` inside a Server Component.** Move client-only logic into a dedicated `'use client'` component, then import it normally.
|
|
88
|
+
|
|
89
|
+
## Data Fetching
|
|
90
|
+
|
|
91
|
+
### Server-Side Fetching
|
|
92
|
+
|
|
93
|
+
Fetch directly in `async` Server Components. Next.js deduplicates identical `fetch` calls.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
export default async function ProjectsPage() {
|
|
97
|
+
const projects = await fetch('https://api.example.com/projects', {
|
|
98
|
+
next: { revalidate: 60 },
|
|
99
|
+
}).then((res) => res.json());
|
|
100
|
+
return <ul>{projects.map((p: { id: string; name: string }) => <li key={p.id}>{p.name}</li>)}</ul>;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Server Actions (Mutations)
|
|
105
|
+
|
|
106
|
+
Define with `'use server'`. Call from Client Components via `action` or `startTransition`.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// lib/actions.ts
|
|
110
|
+
'use server';
|
|
111
|
+
import { revalidatePath } from 'next/cache';
|
|
112
|
+
export async function createItem(formData: FormData) {
|
|
113
|
+
const name = formData.get('name') as string;
|
|
114
|
+
await db.items.create({ data: { name } });
|
|
115
|
+
revalidatePath('/items');
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// components/CreateItemForm.tsx
|
|
121
|
+
'use client';
|
|
122
|
+
import { createItem } from '@/lib/actions';
|
|
123
|
+
export default function CreateItemForm() {
|
|
124
|
+
return <form action={createItem}><input name="name" required /><button type="submit">Add</button></form>;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Parallel Data Fetching
|
|
129
|
+
|
|
130
|
+
Initiate independent fetches simultaneously — never `await` sequentially.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
export default async function DashboardPage() {
|
|
134
|
+
const [metrics, activity] = await Promise.all([getMetrics(), getRecentActivity()]);
|
|
135
|
+
return <><MetricsPanel data={metrics} /><ActivityFeed items={activity} /></>;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Caching and Revalidation
|
|
140
|
+
|
|
141
|
+
| Mechanism | Scope | How to Use |
|
|
142
|
+
|-----------|-------|------------|
|
|
143
|
+
| **Request Memoization** | Per-request | Automatic deduplication of identical `fetch` calls |
|
|
144
|
+
| **Data Cache** | Cross-request | `fetch` results cached by default; opt out with `cache: 'no-store'` |
|
|
145
|
+
| **Full Route Cache** | Build time | Static routes cached as HTML + RSC payload |
|
|
146
|
+
| **Router Cache** | Client-side | Prefetched and visited routes cached in browser |
|
|
147
|
+
|
|
148
|
+
### Revalidation
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
// Time-based — revalidate every 60 seconds
|
|
152
|
+
fetch(url, { next: { revalidate: 60 } });
|
|
153
|
+
|
|
154
|
+
// On-demand — revalidate by path or tag
|
|
155
|
+
import { revalidatePath, revalidateTag } from 'next/cache';
|
|
156
|
+
revalidatePath('/blog');
|
|
157
|
+
revalidateTag('posts');
|
|
158
|
+
|
|
159
|
+
// Tag a fetch for on-demand revalidation
|
|
160
|
+
fetch(url, { next: { tags: ['posts'] } });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Routing
|
|
164
|
+
|
|
165
|
+
### File Conventions
|
|
166
|
+
|
|
167
|
+
| File | Purpose |
|
|
168
|
+
|------|---------|
|
|
169
|
+
| `page.tsx` | Route UI (makes segment publicly accessible) |
|
|
170
|
+
| `layout.tsx` | Shared layout (wraps children, persists across navigation) |
|
|
171
|
+
| `template.tsx` | Like layout but re-mounts on navigation |
|
|
172
|
+
| `loading.tsx` | Loading UI (automatic Suspense boundary) |
|
|
173
|
+
| `error.tsx` | Error UI (Client Component, automatic error boundary) |
|
|
174
|
+
| `not-found.tsx` | 404 UI |
|
|
175
|
+
| `route.ts` | API endpoint (no UI) |
|
|
176
|
+
| `default.tsx` | Fallback for parallel routes |
|
|
177
|
+
|
|
178
|
+
### Dynamic Routes
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
app/blog/[slug]/page.tsx → /blog/:slug
|
|
182
|
+
app/shop/[...slug]/page.tsx → /shop/:slug+ (catch-all)
|
|
183
|
+
app/shop/[[...slug]]/page.tsx → /shop or /shop/:slug+ (optional catch-all)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Route Handlers (API Routes)
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// app/api/users/route.ts
|
|
190
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
191
|
+
|
|
192
|
+
export async function GET(request: NextRequest) {
|
|
193
|
+
const { searchParams } = request.nextUrl;
|
|
194
|
+
const query = searchParams.get('q');
|
|
195
|
+
const users = await findUsers(query);
|
|
196
|
+
return NextResponse.json(users);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function POST(request: NextRequest) {
|
|
200
|
+
const body = await request.json();
|
|
201
|
+
const user = await createUser(body);
|
|
202
|
+
return NextResponse.json(user, { status: 201 });
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Error Handling
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// app/dashboard/error.tsx — must be a Client Component
|
|
210
|
+
'use client';
|
|
211
|
+
export default function DashboardError({ error, reset }: { error: Error; reset: () => void }) {
|
|
212
|
+
return <div role="alert"><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
// app/projects/[id]/page.tsx — trigger not-found boundary
|
|
218
|
+
import { notFound } from 'next/navigation';
|
|
219
|
+
export default async function ProjectPage({ params }: { params: { id: string } }) {
|
|
220
|
+
const project = await getProject(params.id);
|
|
221
|
+
if (!project) notFound();
|
|
222
|
+
return <h1>{project.name}</h1>;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Middleware
|
|
227
|
+
|
|
228
|
+
Runs at the Edge before every matched request. Use for auth, redirects, rewrites, headers.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
232
|
+
|
|
233
|
+
export function middleware(request: NextRequest) {
|
|
234
|
+
const token = request.cookies.get('session')?.value;
|
|
235
|
+
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
236
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
237
|
+
}
|
|
238
|
+
// Add custom headers
|
|
239
|
+
const response = NextResponse.next();
|
|
240
|
+
response.headers.set('x-request-id', crypto.randomUUID());
|
|
241
|
+
return response;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const config = { matcher: ['/dashboard/:path*', '/settings/:path*'] };
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Configuration
|
|
248
|
+
|
|
249
|
+
### `next.config.ts`
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import type { NextConfig } from 'next';
|
|
253
|
+
|
|
254
|
+
const nextConfig: NextConfig = {
|
|
255
|
+
reactStrictMode: true,
|
|
256
|
+
images: {
|
|
257
|
+
remotePatterns: [
|
|
258
|
+
{ protocol: 'https', hostname: 'cdn.example.com' },
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
experimental: {
|
|
262
|
+
ppr: true, // Partial Prerendering
|
|
263
|
+
typedRoutes: true, // Type-safe <Link> hrefs
|
|
264
|
+
},
|
|
265
|
+
// Redirects, rewrites, headers
|
|
266
|
+
async redirects() {
|
|
267
|
+
return [{ source: '/old-path', destination: '/new-path', permanent: true }];
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export default nextConfig;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Environment Variables
|
|
275
|
+
|
|
276
|
+
| Prefix | Available in | Use Case |
|
|
277
|
+
|--------|-------------|----------|
|
|
278
|
+
| `NEXT_PUBLIC_` | Server + Client | Public values (API base URLs, feature flags) |
|
|
279
|
+
| No prefix | Server only | Secrets (DB URLs, API keys, tokens) |
|
|
280
|
+
|
|
281
|
+
Files loaded (in priority order): `.env.local`, `.env.development` / `.env.production`, `.env`.
|
|
282
|
+
|
|
283
|
+
## Image and Font Optimization
|
|
284
|
+
|
|
285
|
+
### Images
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
import Image from 'next/image';
|
|
289
|
+
import heroImg from '@/public/hero.jpg';
|
|
290
|
+
|
|
291
|
+
// Local image — auto width/height from import
|
|
292
|
+
<Image src={heroImg} alt="Hero" priority />
|
|
293
|
+
|
|
294
|
+
// Remote image — must specify dimensions
|
|
295
|
+
<Image src="https://cdn.example.com/photo.jpg" alt="Photo" width={800} height={600} />
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Fonts
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
// app/layout.tsx
|
|
302
|
+
import { Inter } from 'next/font/google';
|
|
303
|
+
const inter = Inter({ subsets: ['latin'], display: 'swap' });
|
|
304
|
+
|
|
305
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
306
|
+
return <html lang="en" className={inter.className}><body>{children}</body></html>;
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Metadata and SEO
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
// app/layout.tsx — static metadata
|
|
314
|
+
import type { Metadata } from 'next';
|
|
315
|
+
|
|
316
|
+
export const metadata: Metadata = {
|
|
317
|
+
title: { default: 'My App', template: '%s | My App' },
|
|
318
|
+
description: 'App description',
|
|
319
|
+
openGraph: { title: 'My App', description: 'App description', type: 'website' },
|
|
320
|
+
};
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
// app/blog/[slug]/page.tsx — dynamic metadata
|
|
325
|
+
import type { Metadata } from 'next';
|
|
326
|
+
|
|
327
|
+
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
|
|
328
|
+
const post = await getPost(params.slug);
|
|
329
|
+
return { title: post.title, description: post.excerpt };
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Performance Patterns
|
|
334
|
+
|
|
335
|
+
- **Default to Server Components** — smaller client bundles, faster paint.
|
|
336
|
+
- **`<Suspense>` + `loading.tsx`** — stream content progressively; never block the whole page.
|
|
337
|
+
- **`Promise.all()`** — parallel data fetching for independent data.
|
|
338
|
+
- **Dynamic imports** — lazy-load heavy Client Components with `next/dynamic`.
|
|
339
|
+
- **`<Image>`** — automatic lazy loading, responsive sizing, format conversion.
|
|
340
|
+
- **`next/font`** — zero layout shift, self-hosted fonts.
|
|
341
|
+
- **Route segment config** — fine-tune caching and rendering per route.
|
|
342
|
+
|
|
343
|
+
## Deployment
|
|
344
|
+
|
|
345
|
+
Next.js deploys to multiple targets:
|
|
346
|
+
|
|
347
|
+
| Target | Config | Notes |
|
|
348
|
+
|--------|--------|-------|
|
|
349
|
+
| **Vercel** | Zero-config | Full feature support including Edge, ISR, Middleware |
|
|
350
|
+
| **Node.js server** | `output: 'standalone'` | Minimal `standalone/` folder with server |
|
|
351
|
+
| **Docker** | `output: 'standalone'` | Copy `.next/standalone` + `.next/static` + `public` |
|
|
352
|
+
| **Static export** | `output: 'export'` | No server features (no SSR, API routes, middleware) |
|
|
353
|
+
|
|
354
|
+
## Component Practices & Naming
|
|
355
|
+
|
|
356
|
+
- PascalCase for component files/exports. camelCase for hooks.
|
|
357
|
+
- Shared components in `components/`. Route-specific components co-located in route folder.
|
|
358
|
+
- TypeScript interfaces for props. Explicit types and defaults.
|
|
359
|
+
- Co-locate tests with components.
|
|
360
|
+
- Folders: `kebab-case`. Types/Interfaces: `PascalCase`. Constants: `UPPER_SNAKE_CASE`.
|
|
361
|
+
|
|
362
|
+
## Anti-Patterns
|
|
363
|
+
|
|
364
|
+
| Anti-Pattern | Why It's Wrong | Do This Instead |
|
|
365
|
+
|-------------|---------------|-----------------|
|
|
366
|
+
| `'use client'` on every component | Bloats JS bundle, defeats RSC benefits | Default to Server Components; add `'use client'` only when needed |
|
|
367
|
+
| Sequential `await` for independent data | Creates a waterfall, slows page load | Use `Promise.all()` for parallel fetches |
|
|
368
|
+
| `next/dynamic` with `ssr: false` in Server Components | Build/runtime crash | Extract to a Client Component, import normally |
|
|
369
|
+
| Fetching in `useEffect` when server fetch works | Extra client roundtrip, loading flash | Fetch in the Server Component or use Server Actions |
|
|
370
|
+
| Giant `layout.tsx` with all providers | Hard to test, couples unrelated concerns | Split providers into a `Providers` Client Component |
|
|
371
|
+
| Catching errors without `error.tsx` | Unhandled errors crash the page | Add `error.tsx` per route segment |
|
|
372
|
+
| Hardcoding secrets in source files | Security risk, leaks in version control | Use `.env.local` and `process.env` |
|
|
373
|
+
| Skipping `loading.tsx` / `<Suspense>` | Blank screen while data loads | Add `loading.tsx` or wrap in `<Suspense>` |
|
|
374
|
+
| Using `getServerSideProps` / `getStaticProps` | Legacy Pages Router patterns | Use App Router with `async` Server Components |
|
|
375
|
+
| Ignoring `next.config.ts` for images | Remote images blocked by default | Configure `images.remotePatterns` |
|
|
376
|
+
| Missing `metadata` exports | Poor SEO, no social previews | Export `metadata` or `generateMetadata` per page |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PluginConfig } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const config: PluginConfig = {
|
|
4
|
+
id: 'nextjs',
|
|
5
|
+
name: 'Next.js',
|
|
6
|
+
category: 'tech',
|
|
7
|
+
subCategory: 'framework',
|
|
8
|
+
label: 'Next.js',
|
|
9
|
+
hint: 'React framework with App Router, Server Components, and MCP devtools',
|
|
10
|
+
skillName: 'nextjs-framework',
|
|
11
|
+
mcpServerKey: 'next-devtools',
|
|
12
|
+
mcpConfig: {
|
|
13
|
+
type: 'stdio',
|
|
14
|
+
command: 'npx',
|
|
15
|
+
args: ['-y', 'next-devtools-mcp@latest'],
|
|
16
|
+
},
|
|
17
|
+
authType: 'none',
|
|
18
|
+
envVars: [],
|
|
19
|
+
agentToolMap: {
|
|
20
|
+
'developer': [
|
|
21
|
+
'next-devtools/get_errors', 'next-devtools/get_logs',
|
|
22
|
+
'next-devtools/get_page_metadata', 'next-devtools/get_project_metadata',
|
|
23
|
+
'next-devtools/get_server_action_by_id',
|
|
24
|
+
],
|
|
25
|
+
'performance-expert': [
|
|
26
|
+
'next-devtools/get_errors', 'next-devtools/get_logs',
|
|
27
|
+
'next-devtools/get_page_metadata', 'next-devtools/get_project_metadata',
|
|
28
|
+
],
|
|
29
|
+
'testing-expert': [
|
|
30
|
+
'next-devtools/get_errors', 'next-devtools/get_logs',
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
docsUrl: 'https://www.opencastle.dev/docs/plugins#nextjs',
|
|
34
|
+
officialDocs: 'https://nextjs.org/docs',
|
|
35
|
+
mcpPackage: 'next-devtools-mcp',
|
|
36
|
+
};
|
|
@@ -4,7 +4,7 @@ export const config: PluginConfig = {
|
|
|
4
4
|
id: 'nx',
|
|
5
5
|
name: 'NX',
|
|
6
6
|
category: 'tech',
|
|
7
|
-
subCategory: '
|
|
7
|
+
subCategory: 'codebase-tool',
|
|
8
8
|
label: 'NX',
|
|
9
9
|
hint: 'Monorepo build system',
|
|
10
10
|
skillName: 'nx-workspace',
|
|
@@ -23,7 +23,6 @@ export const config: PluginConfig = {
|
|
|
23
23
|
'performance-expert': ['nx-mcp-server/nx_project_details', 'nx-mcp-server/nx_workspace'],
|
|
24
24
|
'release-manager': ['nx-mcp-server/nx_project_details', 'nx-mcp-server/nx_workspace', 'nx-mcp-server/nx_workspace_path'],
|
|
25
25
|
},
|
|
26
|
-
docsUrl:
|
|
26
|
+
docsUrl: 'https://www.opencastle.dev/docs/plugins#nx',
|
|
27
27
|
officialDocs: 'https://nx.dev/getting-started/intro',
|
|
28
|
-
mcpPackage: null,
|
|
29
28
|
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: playwright-testing
|
|
3
|
+
description: "Playwright E2E testing patterns, cross-browser configuration, page objects, and CI setup. Use when writing E2E tests, visual regression tests, or configuring Playwright in CI pipelines."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .github/customizations/ directory instead. -->
|
|
7
|
+
|
|
8
|
+
# Playwright Testing
|
|
9
|
+
|
|
10
|
+
Playwright-specific E2E testing patterns. For project-specific test configuration and breakpoints, see [testing-config.md](../../customizations/stack/testing-config.md).
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx playwright test # Run all tests
|
|
16
|
+
npx playwright test --ui # Open interactive UI mode
|
|
17
|
+
npx playwright test --headed # Run with visible browsers
|
|
18
|
+
npx playwright test auth.spec.ts # Run specific spec
|
|
19
|
+
npx playwright test --project=chromium # Run in specific browser
|
|
20
|
+
npx playwright test --grep "login" # Filter by test name
|
|
21
|
+
npx playwright test --debug # Step-through debugging
|
|
22
|
+
npx playwright codegen http://localhost:3000 # Generate tests from actions
|
|
23
|
+
npx playwright show-report # View HTML test report
|
|
24
|
+
npx playwright install # Install browsers
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Test Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
tests/
|
|
31
|
+
├── e2e/
|
|
32
|
+
│ ├── auth/
|
|
33
|
+
│ │ ├── login.spec.ts
|
|
34
|
+
│ │ └── signup.spec.ts
|
|
35
|
+
│ └── dashboard/
|
|
36
|
+
│ └── overview.spec.ts
|
|
37
|
+
├── fixtures/
|
|
38
|
+
│ └── auth.fixture.ts # Custom test fixtures
|
|
39
|
+
└── pages/
|
|
40
|
+
├── login.page.ts # Page object
|
|
41
|
+
└── dashboard.page.ts
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Writing Tests
|
|
45
|
+
|
|
46
|
+
### Basic Test Pattern
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { test, expect } from '@playwright/test';
|
|
50
|
+
|
|
51
|
+
test.describe('Login', () => {
|
|
52
|
+
test.beforeEach(async ({ page }) => {
|
|
53
|
+
await page.goto('/login');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should log in with valid credentials', async ({ page }) => {
|
|
57
|
+
await page.getByTestId('email-input').fill('user@example.com');
|
|
58
|
+
await page.getByTestId('password-input').fill('password123');
|
|
59
|
+
await page.getByTestId('login-button').click();
|
|
60
|
+
|
|
61
|
+
await expect(page).toHaveURL(/.*dashboard/);
|
|
62
|
+
await expect(page.getByTestId('user-menu')).toBeVisible();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should show error for invalid credentials', async ({ page }) => {
|
|
66
|
+
await page.getByTestId('email-input').fill('wrong@example.com');
|
|
67
|
+
await page.getByTestId('password-input').fill('wrong');
|
|
68
|
+
await page.getByTestId('login-button').click();
|
|
69
|
+
|
|
70
|
+
await expect(page.getByTestId('error-message')).toContainText('Invalid credentials');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Page Object Model
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// tests/pages/login.page.ts
|
|
79
|
+
import { type Page, type Locator } from '@playwright/test';
|
|
80
|
+
|
|
81
|
+
export class LoginPage {
|
|
82
|
+
readonly emailInput: Locator;
|
|
83
|
+
readonly passwordInput: Locator;
|
|
84
|
+
readonly submitButton: Locator;
|
|
85
|
+
readonly errorMessage: Locator;
|
|
86
|
+
|
|
87
|
+
constructor(private readonly page: Page) {
|
|
88
|
+
this.emailInput = page.getByTestId('email-input');
|
|
89
|
+
this.passwordInput = page.getByTestId('password-input');
|
|
90
|
+
this.submitButton = page.getByTestId('login-button');
|
|
91
|
+
this.errorMessage = page.getByTestId('error-message');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async goto() {
|
|
95
|
+
await this.page.goto('/login');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async login(email: string, password: string) {
|
|
99
|
+
await this.emailInput.fill(email);
|
|
100
|
+
await this.passwordInput.fill(password);
|
|
101
|
+
await this.submitButton.click();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Custom Fixtures
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// tests/fixtures/auth.fixture.ts
|
|
110
|
+
import { test as base } from '@playwright/test';
|
|
111
|
+
|
|
112
|
+
type AuthFixtures = {
|
|
113
|
+
authenticatedPage: Page;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const test = base.extend<AuthFixtures>({
|
|
117
|
+
authenticatedPage: async ({ page }, use) => {
|
|
118
|
+
await page.goto('/login');
|
|
119
|
+
await page.getByTestId('email-input').fill('test@example.com');
|
|
120
|
+
await page.getByTestId('password-input').fill('password');
|
|
121
|
+
await page.getByTestId('login-button').click();
|
|
122
|
+
await page.waitForURL('**/dashboard');
|
|
123
|
+
await use(page);
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Locator Strategy
|
|
129
|
+
|
|
130
|
+
Use Playwright's built-in locators in priority order:
|
|
131
|
+
|
|
132
|
+
1. `page.getByTestId()` — most resilient
|
|
133
|
+
2. `page.getByRole()` — accessible, meaningful
|
|
134
|
+
3. `page.getByLabel()` — for form elements
|
|
135
|
+
4. `page.getByText()` — for unique visible text
|
|
136
|
+
5. `page.locator()` with CSS — last resort
|
|
137
|
+
|
|
138
|
+
## Configuration (playwright.config.ts)
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
142
|
+
|
|
143
|
+
export default defineConfig({
|
|
144
|
+
testDir: './tests',
|
|
145
|
+
fullyParallel: true,
|
|
146
|
+
forbidOnly: !!process.env.CI,
|
|
147
|
+
retries: process.env.CI ? 2 : 0,
|
|
148
|
+
workers: process.env.CI ? 1 : undefined,
|
|
149
|
+
reporter: [['html'], ['list']],
|
|
150
|
+
use: {
|
|
151
|
+
baseURL: 'http://localhost:3000',
|
|
152
|
+
trace: 'on-first-retry',
|
|
153
|
+
screenshot: 'only-on-failure',
|
|
154
|
+
},
|
|
155
|
+
projects: [
|
|
156
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
157
|
+
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
158
|
+
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
159
|
+
{ name: 'mobile', use: { ...devices['iPhone 14'] } },
|
|
160
|
+
],
|
|
161
|
+
webServer: {
|
|
162
|
+
command: 'npm run dev',
|
|
163
|
+
url: 'http://localhost:3000',
|
|
164
|
+
reuseExistingServer: !process.env.CI,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## MCP Tools
|
|
170
|
+
|
|
171
|
+
The Playwright MCP server enables AI agents to interact with browsers directly:
|
|
172
|
+
|
|
173
|
+
| Tool | Purpose |
|
|
174
|
+
|------|---------|
|
|
175
|
+
| `playwright/navigate` | Navigate to a URL |
|
|
176
|
+
| `playwright/screenshot` | Take page screenshots |
|
|
177
|
+
| `playwright/click` | Click elements |
|
|
178
|
+
| `playwright/fill` | Fill form inputs |
|
|
179
|
+
| `playwright/evaluate` | Execute JavaScript in browser |
|
|
180
|
+
| `playwright/expect` | Assert page state |
|
|
181
|
+
|
|
182
|
+
## Best Practices
|
|
183
|
+
|
|
184
|
+
- Use `test.describe` to group related tests
|
|
185
|
+
- Use `test.beforeEach` for common setup — keep tests independent
|
|
186
|
+
- Prefer `getByTestId` and `getByRole` over CSS selectors
|
|
187
|
+
- Use `expect(locator).toBeVisible()` before interacting
|
|
188
|
+
- Use `page.waitForURL()` or `page.waitForResponse()` instead of arbitrary waits
|
|
189
|
+
- Run tests in parallel (`fullyParallel: true`) for speed
|
|
190
|
+
- Use `trace: 'on-first-retry'` to debug flaky tests
|
|
191
|
+
- Use `codegen` to bootstrap tests, then refactor into page objects
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PluginConfig } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const config: PluginConfig = {
|
|
4
|
+
id: 'playwright',
|
|
5
|
+
name: 'Playwright',
|
|
6
|
+
category: 'tech',
|
|
7
|
+
subCategory: 'e2e-testing',
|
|
8
|
+
label: 'Playwright',
|
|
9
|
+
hint: 'Cross-browser E2E testing by Microsoft',
|
|
10
|
+
skillName: 'playwright-testing',
|
|
11
|
+
mcpServerKey: 'Playwright',
|
|
12
|
+
mcpConfig: {
|
|
13
|
+
type: 'stdio',
|
|
14
|
+
command: 'npx',
|
|
15
|
+
args: ['-y', '@playwright/mcp@latest'],
|
|
16
|
+
},
|
|
17
|
+
authType: 'none',
|
|
18
|
+
envVars: [],
|
|
19
|
+
agentToolMap: {
|
|
20
|
+
'testing-expert': ['playwright/*'],
|
|
21
|
+
'ui-ux-expert': ['playwright/*'],
|
|
22
|
+
},
|
|
23
|
+
docsUrl: 'https://www.opencastle.dev/docs/plugins#playwright',
|
|
24
|
+
officialDocs: 'https://playwright.dev/docs/intro',
|
|
25
|
+
mcpPackage: '@playwright/mcp',
|
|
26
|
+
};
|