@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: django-patterns
|
|
3
|
+
description: Django patterns for models, views, serializers, middleware, signals, custom managers, and admin customization.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Django Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building Django 4.2+ applications. Use this skill for
|
|
11
|
+
structuring models with custom managers, writing class-based views, building DRF
|
|
12
|
+
serializers, wiring signals correctly, and customizing the admin interface.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### Models with Custom Managers and QuerySets
|
|
17
|
+
|
|
18
|
+
Define a custom QuerySet, then create a manager from it. This lets you chain custom
|
|
19
|
+
filters naturally. Always add `Meta.ordering` and `__str__`.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
class ArticleQuerySet(models.QuerySet):
|
|
23
|
+
def published(self):
|
|
24
|
+
return self.filter(status="published", pub_date__lte=timezone.now())
|
|
25
|
+
|
|
26
|
+
def by_author(self, user):
|
|
27
|
+
return self.filter(author=user)
|
|
28
|
+
|
|
29
|
+
class Article(models.Model):
|
|
30
|
+
title = models.CharField(max_length=200)
|
|
31
|
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, db_index=True)
|
|
32
|
+
pub_date = models.DateTimeField(null=True, blank=True)
|
|
33
|
+
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
34
|
+
|
|
35
|
+
objects = ArticleQuerySet.as_manager()
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
ordering = ["-pub_date"]
|
|
39
|
+
indexes = [models.Index(fields=["status", "pub_date"])]
|
|
40
|
+
|
|
41
|
+
def __str__(self):
|
|
42
|
+
return self.title
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Views: Fat Models, Thin Views
|
|
46
|
+
|
|
47
|
+
Keep business logic in model methods or service functions. Views handle HTTP
|
|
48
|
+
concerns only: parsing requests, calling services, returning responses.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# services.py
|
|
52
|
+
def publish_article(article: Article, user: User) -> Article:
|
|
53
|
+
if not user.has_perm("articles.publish"):
|
|
54
|
+
raise PermissionDenied("Cannot publish")
|
|
55
|
+
article.status = "published"
|
|
56
|
+
article.pub_date = timezone.now()
|
|
57
|
+
article.save(update_fields=["status", "pub_date"])
|
|
58
|
+
return article
|
|
59
|
+
|
|
60
|
+
# views.py
|
|
61
|
+
class ArticlePublishView(LoginRequiredMixin, View):
|
|
62
|
+
def post(self, request, pk):
|
|
63
|
+
article = get_object_or_404(Article, pk=pk)
|
|
64
|
+
publish_article(article, request.user)
|
|
65
|
+
return redirect("article-detail", pk=pk)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### DRF Serializers
|
|
69
|
+
|
|
70
|
+
Use `ModelSerializer` for CRUD. Override `validate_<field>` for field-level
|
|
71
|
+
validation. Use `SerializerMethodField` for computed fields. Nest read serializers;
|
|
72
|
+
accept IDs for writes.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
class ArticleSerializer(serializers.ModelSerializer):
|
|
76
|
+
author_name = serializers.SerializerMethodField()
|
|
77
|
+
word_count = serializers.SerializerMethodField()
|
|
78
|
+
|
|
79
|
+
class Meta:
|
|
80
|
+
model = Article
|
|
81
|
+
fields = ["id", "title", "status", "pub_date", "author", "author_name", "word_count"]
|
|
82
|
+
read_only_fields = ["pub_date"]
|
|
83
|
+
|
|
84
|
+
def get_author_name(self, obj):
|
|
85
|
+
return obj.author.get_full_name()
|
|
86
|
+
|
|
87
|
+
def get_word_count(self, obj):
|
|
88
|
+
return len(obj.body.split())
|
|
89
|
+
|
|
90
|
+
def validate_title(self, value):
|
|
91
|
+
if len(value) < 5:
|
|
92
|
+
raise serializers.ValidationError("Title must be at least 5 characters")
|
|
93
|
+
return value
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Middleware
|
|
97
|
+
|
|
98
|
+
Middleware processes every request/response. Keep it fast. Use `__init__` for
|
|
99
|
+
one-time setup and `__call__` for per-request logic.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
class RequestTimingMiddleware:
|
|
103
|
+
def __init__(self, get_response):
|
|
104
|
+
self.get_response = get_response
|
|
105
|
+
|
|
106
|
+
def __call__(self, request):
|
|
107
|
+
start = time.monotonic()
|
|
108
|
+
response = self.get_response(request)
|
|
109
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
110
|
+
response["X-Request-Duration-Ms"] = f"{duration_ms:.1f}"
|
|
111
|
+
return response
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Signals: Use Sparingly
|
|
115
|
+
|
|
116
|
+
Signals create implicit coupling. Prefer explicit service calls. When you do use
|
|
117
|
+
signals, connect them in `AppConfig.ready()` and keep handlers idempotent.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# apps.py
|
|
121
|
+
class ArticlesConfig(AppConfig):
|
|
122
|
+
name = "articles"
|
|
123
|
+
def ready(self):
|
|
124
|
+
from . import signals # noqa: F401
|
|
125
|
+
|
|
126
|
+
# signals.py
|
|
127
|
+
from django.db.models.signals import post_save
|
|
128
|
+
from django.dispatch import receiver
|
|
129
|
+
|
|
130
|
+
@receiver(post_save, sender=Article)
|
|
131
|
+
def notify_subscribers(sender, instance, created, **kwargs):
|
|
132
|
+
if created and instance.status == "published":
|
|
133
|
+
enqueue_notification_task(instance.pk)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Admin Customization
|
|
137
|
+
|
|
138
|
+
Use `list_display`, `list_filter`, `search_fields` for usability. Add custom
|
|
139
|
+
actions for bulk operations. Use `readonly_fields` for computed displays.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
@admin.register(Article)
|
|
143
|
+
class ArticleAdmin(admin.ModelAdmin):
|
|
144
|
+
list_display = ["title", "author", "status", "pub_date"]
|
|
145
|
+
list_filter = ["status", "pub_date"]
|
|
146
|
+
search_fields = ["title", "body"]
|
|
147
|
+
raw_id_fields = ["author"]
|
|
148
|
+
actions = ["mark_published"]
|
|
149
|
+
|
|
150
|
+
@admin.action(description="Mark selected as published")
|
|
151
|
+
def mark_published(self, request, queryset):
|
|
152
|
+
queryset.update(status="published", pub_date=timezone.now())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Examples
|
|
156
|
+
|
|
157
|
+
**Pattern: select_related / prefetch_related to avoid N+1**
|
|
158
|
+
```python
|
|
159
|
+
articles = Article.objects.published().select_related("author").prefetch_related("tags")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Pattern: Database constraint at the model level**
|
|
163
|
+
```python
|
|
164
|
+
class Meta:
|
|
165
|
+
constraints = [
|
|
166
|
+
models.UniqueConstraint(fields=["author", "slug"], name="unique_author_slug"),
|
|
167
|
+
models.CheckConstraint(check=models.Q(price__gte=0), name="price_non_negative"),
|
|
168
|
+
]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Checklist
|
|
172
|
+
|
|
173
|
+
- [ ] Every model has `__str__`, `Meta.ordering`, and relevant indexes
|
|
174
|
+
- [ ] Custom QuerySet methods chained via `.as_manager()`
|
|
175
|
+
- [ ] Business logic in service functions, not views
|
|
176
|
+
- [ ] `select_related` / `prefetch_related` for related lookups (no N+1 queries)
|
|
177
|
+
- [ ] Serializers validate at field and object level
|
|
178
|
+
- [ ] Signals connected in `AppConfig.ready()`, kept idempotent
|
|
179
|
+
- [ ] Middleware is fast and stateless
|
|
180
|
+
- [ ] Admin uses `list_display`, `list_filter`, `search_fields`
|
|
181
|
+
- [ ] Database constraints (`UniqueConstraint`, `CheckConstraint`) enforce integrity
|
|
182
|
+
- [ ] `update_fields` passed to `.save()` when only specific columns change
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docker-patterns
|
|
3
|
+
description: Build efficient Docker images with multi-stage builds, layer caching, health checks, and Compose orchestration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Docker Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Use these patterns when containerizing any application for development, CI, or
|
|
11
|
+
production. A poorly written Dockerfile wastes minutes per build, ships hundreds
|
|
12
|
+
of megabytes of unnecessary files, and creates security vulnerabilities.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### 1. Multi-Stage Builds
|
|
17
|
+
|
|
18
|
+
Separate build dependencies from the runtime image. Final image contains only
|
|
19
|
+
what's needed to run.
|
|
20
|
+
|
|
21
|
+
```dockerfile
|
|
22
|
+
# Stage 1: Build
|
|
23
|
+
FROM node:20-alpine AS builder
|
|
24
|
+
WORKDIR /app
|
|
25
|
+
COPY package.json package-lock.json ./
|
|
26
|
+
RUN npm ci --ignore-scripts
|
|
27
|
+
COPY tsconfig.json ./
|
|
28
|
+
COPY src/ src/
|
|
29
|
+
RUN npm run build
|
|
30
|
+
|
|
31
|
+
# Stage 2: Production
|
|
32
|
+
FROM node:20-alpine AS runner
|
|
33
|
+
WORKDIR /app
|
|
34
|
+
ENV NODE_ENV=production
|
|
35
|
+
|
|
36
|
+
RUN addgroup -g 1001 appuser && adduser -u 1001 -G appuser -s /bin/sh -D appuser
|
|
37
|
+
|
|
38
|
+
COPY --from=builder /app/dist ./dist
|
|
39
|
+
COPY --from=builder /app/package.json /app/package-lock.json ./
|
|
40
|
+
RUN npm ci --ignore-scripts --omit=dev && npm cache clean --force
|
|
41
|
+
|
|
42
|
+
USER appuser
|
|
43
|
+
EXPOSE 3000
|
|
44
|
+
CMD ["node", "dist/index.js"]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Result: builder stage has TypeScript, dev dependencies, source. Runner stage has
|
|
48
|
+
only compiled JS and production deps. Image shrinks from ~800MB to ~150MB.
|
|
49
|
+
|
|
50
|
+
### 2. Layer Caching
|
|
51
|
+
|
|
52
|
+
Docker caches each layer. Order instructions from least-changing to most-changing.
|
|
53
|
+
|
|
54
|
+
```dockerfile
|
|
55
|
+
# Good order — dependencies change less often than source code
|
|
56
|
+
COPY package.json package-lock.json ./ # Layer 1: cached unless deps change
|
|
57
|
+
RUN npm ci # Layer 2: cached with Layer 1
|
|
58
|
+
COPY src/ src/ # Layer 3: busted on every code change
|
|
59
|
+
RUN npm run build # Layer 4: busted with Layer 3
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```dockerfile
|
|
63
|
+
# Bad order — every code change reinstalls dependencies
|
|
64
|
+
COPY . . # Always busted
|
|
65
|
+
RUN npm ci && npm run build # Always reinstalls
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. .dockerignore
|
|
69
|
+
|
|
70
|
+
Exclude files that bloat context and leak secrets.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
node_modules
|
|
74
|
+
.git
|
|
75
|
+
.env*
|
|
76
|
+
*.md
|
|
77
|
+
coverage
|
|
78
|
+
.next
|
|
79
|
+
dist
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 4. Health Checks
|
|
83
|
+
|
|
84
|
+
Let orchestrators know when your app is actually ready to serve traffic.
|
|
85
|
+
|
|
86
|
+
```dockerfile
|
|
87
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
88
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The health endpoint should verify critical dependencies:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
app.get('/health', async (req, res) => {
|
|
95
|
+
try {
|
|
96
|
+
await db.$queryRaw`SELECT 1`;
|
|
97
|
+
res.json({ status: 'ok', db: 'connected' });
|
|
98
|
+
} catch {
|
|
99
|
+
res.status(503).json({ status: 'unhealthy', db: 'disconnected' });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 5. Docker Compose for Development
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
# docker-compose.yml
|
|
108
|
+
services:
|
|
109
|
+
app:
|
|
110
|
+
build:
|
|
111
|
+
context: .
|
|
112
|
+
target: builder # use the builder stage for dev
|
|
113
|
+
ports:
|
|
114
|
+
- "3000:3000"
|
|
115
|
+
volumes:
|
|
116
|
+
- ./src:/app/src # hot reload
|
|
117
|
+
- /app/node_modules # prevent host overwrite
|
|
118
|
+
environment:
|
|
119
|
+
- DATABASE_URL=postgresql://postgres:postgres@db:5432/app
|
|
120
|
+
- REDIS_URL=redis://cache:6379
|
|
121
|
+
depends_on:
|
|
122
|
+
db:
|
|
123
|
+
condition: service_healthy
|
|
124
|
+
command: npm run dev
|
|
125
|
+
|
|
126
|
+
db:
|
|
127
|
+
image: postgres:16-alpine
|
|
128
|
+
environment:
|
|
129
|
+
POSTGRES_DB: app
|
|
130
|
+
POSTGRES_PASSWORD: postgres
|
|
131
|
+
volumes:
|
|
132
|
+
- pgdata:/var/lib/postgresql/data
|
|
133
|
+
ports:
|
|
134
|
+
- "5432:5432"
|
|
135
|
+
healthcheck:
|
|
136
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
137
|
+
interval: 5s
|
|
138
|
+
timeout: 3s
|
|
139
|
+
retries: 5
|
|
140
|
+
|
|
141
|
+
cache:
|
|
142
|
+
image: redis:7-alpine
|
|
143
|
+
ports:
|
|
144
|
+
- "6379:6379"
|
|
145
|
+
|
|
146
|
+
volumes:
|
|
147
|
+
pgdata:
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 6. Secrets Management
|
|
151
|
+
|
|
152
|
+
Never bake secrets into images. Use build-time secrets or runtime injection.
|
|
153
|
+
|
|
154
|
+
```dockerfile
|
|
155
|
+
# Build-time secret (Docker BuildKit)
|
|
156
|
+
RUN --mount=type=secret,id=npm_token \
|
|
157
|
+
NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
docker build --secret id=npm_token,src=.npm_token .
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Runtime: pass via environment variables or mounted secret files.
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
# docker-compose.yml
|
|
168
|
+
services:
|
|
169
|
+
app:
|
|
170
|
+
environment:
|
|
171
|
+
- DATABASE_URL # inherits from host env
|
|
172
|
+
secrets:
|
|
173
|
+
- api_key
|
|
174
|
+
|
|
175
|
+
secrets:
|
|
176
|
+
api_key:
|
|
177
|
+
file: ./secrets/api_key.txt
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 7. Volume Patterns
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
volumes:
|
|
184
|
+
# Named volume — persists across restarts
|
|
185
|
+
pgdata:
|
|
186
|
+
driver: local
|
|
187
|
+
|
|
188
|
+
# Bind mount — sync host files into container
|
|
189
|
+
# Good for dev, bad for production
|
|
190
|
+
./src:/app/src
|
|
191
|
+
|
|
192
|
+
# Anonymous volume — prevent host from overwriting container dirs
|
|
193
|
+
/app/node_modules
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 8. Production Best Practices
|
|
197
|
+
|
|
198
|
+
```dockerfile
|
|
199
|
+
# Pin exact versions, not :latest
|
|
200
|
+
FROM node:20.11.1-alpine3.19
|
|
201
|
+
|
|
202
|
+
# Run as non-root
|
|
203
|
+
USER 1001
|
|
204
|
+
|
|
205
|
+
# Set memory limits
|
|
206
|
+
ENV NODE_OPTIONS="--max-old-space-size=256"
|
|
207
|
+
|
|
208
|
+
# Use tini as PID 1 for proper signal handling
|
|
209
|
+
RUN apk add --no-cache tini
|
|
210
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
211
|
+
CMD ["node", "dist/index.js"]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Examples
|
|
215
|
+
|
|
216
|
+
| Goal | Pattern |
|
|
217
|
+
|------|---------|
|
|
218
|
+
| Small image | Multi-stage build, Alpine base |
|
|
219
|
+
| Fast rebuilds | Layer caching, .dockerignore |
|
|
220
|
+
| Reliable orchestration | Health checks with dependency verification |
|
|
221
|
+
| Local development | Compose with bind mounts and hot reload |
|
|
222
|
+
| Secret protection | BuildKit secrets, never `ENV` or `ARG` for secrets |
|
|
223
|
+
|
|
224
|
+
## Checklist
|
|
225
|
+
|
|
226
|
+
- [ ] Dockerfile uses multi-stage build — final image has no build tools
|
|
227
|
+
- [ ] Base image is pinned to a specific version, not `:latest`
|
|
228
|
+
- [ ] `.dockerignore` excludes `node_modules`, `.git`, `.env*`, and build artifacts
|
|
229
|
+
- [ ] COPY order puts `package*.json` before source code for layer caching
|
|
230
|
+
- [ ] `HEALTHCHECK` instruction is present with reasonable intervals
|
|
231
|
+
- [ ] Application runs as a non-root user
|
|
232
|
+
- [ ] No secrets in the image — use BuildKit secrets or runtime env vars
|
|
233
|
+
- [ ] `tini` or equivalent handles PID 1 for proper signal forwarding
|
|
234
|
+
- [ ] Compose `depends_on` uses `condition: service_healthy`
|
|
235
|
+
- [ ] Production image is < 200MB (verify with `docker images`)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: e2e-testing
|
|
3
|
+
description: E2E testing patterns with Playwright including page objects, fixtures, network mocking, visual comparisons, and CI integration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# E2E Testing Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Write end-to-end tests for critical user journeys: authentication, checkout, onboarding, data-entry workflows, and any path where a failure means lost revenue or broken trust. Use Playwright for cross-browser coverage (Chromium, Firefox, WebKit), built-in auto-waiting, and powerful network interception. Reserve E2E for high-value flows and use unit/integration tests for everything else.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Playwright Configuration
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// playwright.config.ts
|
|
17
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
18
|
+
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
testDir: './e2e',
|
|
21
|
+
fullyParallel: true,
|
|
22
|
+
forbidOnly: !!process.env.CI,
|
|
23
|
+
retries: process.env.CI ? 2 : 0,
|
|
24
|
+
workers: process.env.CI ? 1 : undefined,
|
|
25
|
+
reporter: [
|
|
26
|
+
['html', { open: 'never' }],
|
|
27
|
+
['junit', { outputFile: 'results/e2e.xml' }],
|
|
28
|
+
],
|
|
29
|
+
use: {
|
|
30
|
+
baseURL: 'http://localhost:3000',
|
|
31
|
+
trace: 'on-first-retry',
|
|
32
|
+
screenshot: 'only-on-failure',
|
|
33
|
+
video: 'on-first-retry',
|
|
34
|
+
},
|
|
35
|
+
projects: [
|
|
36
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
37
|
+
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
38
|
+
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
39
|
+
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
|
|
40
|
+
{ name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
|
|
41
|
+
],
|
|
42
|
+
webServer: {
|
|
43
|
+
command: 'npm run dev',
|
|
44
|
+
url: 'http://localhost:3000',
|
|
45
|
+
reuseExistingServer: !process.env.CI,
|
|
46
|
+
timeout: 120_000,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Page Object Model
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// e2e/pages/login.page.ts
|
|
55
|
+
import { Page, Locator, expect } from '@playwright/test';
|
|
56
|
+
|
|
57
|
+
export class LoginPage {
|
|
58
|
+
readonly page: Page;
|
|
59
|
+
readonly emailInput: Locator;
|
|
60
|
+
readonly passwordInput: Locator;
|
|
61
|
+
readonly submitButton: Locator;
|
|
62
|
+
readonly errorAlert: Locator;
|
|
63
|
+
|
|
64
|
+
constructor(page: Page) {
|
|
65
|
+
this.page = page;
|
|
66
|
+
this.emailInput = page.getByLabel('Email');
|
|
67
|
+
this.passwordInput = page.getByLabel('Password');
|
|
68
|
+
this.submitButton = page.getByRole('button', { name: 'Sign in' });
|
|
69
|
+
this.errorAlert = page.getByRole('alert');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async goto() {
|
|
73
|
+
await this.page.goto('/login');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async login(email: string, password: string) {
|
|
77
|
+
await this.emailInput.fill(email);
|
|
78
|
+
await this.passwordInput.fill(password);
|
|
79
|
+
await this.submitButton.click();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async expectError(message: string) {
|
|
83
|
+
await expect(this.errorAlert).toContainText(message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async expectRedirectTo(path: string) {
|
|
87
|
+
await expect(this.page).toHaveURL(new RegExp(path));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// e2e/pages/dashboard.page.ts
|
|
94
|
+
import { Page, Locator, expect } from '@playwright/test';
|
|
95
|
+
|
|
96
|
+
export class DashboardPage {
|
|
97
|
+
readonly page: Page;
|
|
98
|
+
readonly heading: Locator;
|
|
99
|
+
readonly userMenu: Locator;
|
|
100
|
+
readonly navLinks: Locator;
|
|
101
|
+
|
|
102
|
+
constructor(page: Page) {
|
|
103
|
+
this.page = page;
|
|
104
|
+
this.heading = page.getByRole('heading', { level: 1 });
|
|
105
|
+
this.userMenu = page.getByTestId('user-menu');
|
|
106
|
+
this.navLinks = page.getByRole('navigation').getByRole('link');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async expectLoaded() {
|
|
110
|
+
await expect(this.heading).toBeVisible();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async logout() {
|
|
114
|
+
await this.userMenu.click();
|
|
115
|
+
await this.page.getByRole('menuitem', { name: 'Logout' }).click();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Custom Fixtures
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// e2e/fixtures.ts
|
|
124
|
+
import { test as base, expect } from '@playwright/test';
|
|
125
|
+
import { LoginPage } from './pages/login.page';
|
|
126
|
+
import { DashboardPage } from './pages/dashboard.page';
|
|
127
|
+
|
|
128
|
+
type Fixtures = {
|
|
129
|
+
loginPage: LoginPage;
|
|
130
|
+
dashboardPage: DashboardPage;
|
|
131
|
+
authenticatedPage: Page;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const test = base.extend<Fixtures>({
|
|
135
|
+
loginPage: async ({ page }, use) => {
|
|
136
|
+
await use(new LoginPage(page));
|
|
137
|
+
},
|
|
138
|
+
dashboardPage: async ({ page }, use) => {
|
|
139
|
+
await use(new DashboardPage(page));
|
|
140
|
+
},
|
|
141
|
+
authenticatedPage: async ({ browser }, use) => {
|
|
142
|
+
const context = await browser.newContext({
|
|
143
|
+
storageState: 'e2e/.auth/user.json',
|
|
144
|
+
});
|
|
145
|
+
const page = await context.newPage();
|
|
146
|
+
await use(page);
|
|
147
|
+
await context.close();
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export { expect };
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Authentication Setup
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// e2e/auth.setup.ts
|
|
158
|
+
import { test as setup, expect } from '@playwright/test';
|
|
159
|
+
|
|
160
|
+
setup('authenticate', async ({ page }) => {
|
|
161
|
+
await page.goto('/login');
|
|
162
|
+
await page.getByLabel('Email').fill('test@example.com');
|
|
163
|
+
await page.getByLabel('Password').fill('TestPassword123');
|
|
164
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
165
|
+
await expect(page).toHaveURL('/dashboard');
|
|
166
|
+
|
|
167
|
+
// Save auth state for reuse
|
|
168
|
+
await page.context().storageState({ path: 'e2e/.auth/user.json' });
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Network Mocking
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// e2e/tests/dashboard.spec.ts
|
|
176
|
+
import { test, expect } from '../fixtures';
|
|
177
|
+
|
|
178
|
+
test.describe('Dashboard', () => {
|
|
179
|
+
test('displays analytics data', async ({ page }) => {
|
|
180
|
+
// Mock the API response
|
|
181
|
+
await page.route('**/api/analytics*', (route) =>
|
|
182
|
+
route.fulfill({
|
|
183
|
+
status: 200,
|
|
184
|
+
contentType: 'application/json',
|
|
185
|
+
body: JSON.stringify({
|
|
186
|
+
visitors: 1234,
|
|
187
|
+
pageViews: 5678,
|
|
188
|
+
bounceRate: 0.42,
|
|
189
|
+
}),
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await page.goto('/dashboard');
|
|
194
|
+
await expect(page.getByText('1,234')).toBeVisible();
|
|
195
|
+
await expect(page.getByText('5,678')).toBeVisible();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('handles API failure gracefully', async ({ page }) => {
|
|
199
|
+
await page.route('**/api/analytics*', (route) =>
|
|
200
|
+
route.fulfill({ status: 500 })
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await page.goto('/dashboard');
|
|
204
|
+
await expect(page.getByText('Failed to load analytics')).toBeVisible();
|
|
205
|
+
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('waits for network idle', async ({ page }) => {
|
|
209
|
+
const responsePromise = page.waitForResponse('**/api/analytics*');
|
|
210
|
+
await page.goto('/dashboard');
|
|
211
|
+
const response = await responsePromise;
|
|
212
|
+
expect(response.status()).toBe(200);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Visual Comparisons
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// e2e/tests/visual.spec.ts
|
|
221
|
+
import { test, expect } from '@playwright/test';
|
|
222
|
+
|
|
223
|
+
test('homepage visual regression', async ({ page }) => {
|
|
224
|
+
await page.goto('/');
|
|
225
|
+
await expect(page).toHaveScreenshot('homepage.png', {
|
|
226
|
+
maxDiffPixelRatio: 0.01,
|
|
227
|
+
fullPage: true,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('component visual states', async ({ page }) => {
|
|
232
|
+
await page.goto('/storybook/iframe.html?id=button--primary');
|
|
233
|
+
await expect(page.locator('#storybook-root')).toHaveScreenshot('button-primary.png');
|
|
234
|
+
|
|
235
|
+
// Hover state
|
|
236
|
+
await page.locator('button').hover();
|
|
237
|
+
await expect(page.locator('#storybook-root')).toHaveScreenshot('button-primary-hover.png');
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### CI Integration
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
# .github/workflows/e2e.yml
|
|
245
|
+
name: E2E Tests
|
|
246
|
+
on:
|
|
247
|
+
pull_request:
|
|
248
|
+
branches: [main]
|
|
249
|
+
|
|
250
|
+
jobs:
|
|
251
|
+
e2e:
|
|
252
|
+
runs-on: ubuntu-latest
|
|
253
|
+
timeout-minutes: 30
|
|
254
|
+
steps:
|
|
255
|
+
- uses: actions/checkout@v4
|
|
256
|
+
- uses: actions/setup-node@v4
|
|
257
|
+
with: { node-version: 20, cache: npm }
|
|
258
|
+
- run: npm ci
|
|
259
|
+
- run: npx playwright install --with-deps
|
|
260
|
+
- run: npx playwright test
|
|
261
|
+
- uses: actions/upload-artifact@v4
|
|
262
|
+
if: failure()
|
|
263
|
+
with:
|
|
264
|
+
name: playwright-report
|
|
265
|
+
path: playwright-report/
|
|
266
|
+
retention-days: 7
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Examples
|
|
270
|
+
|
|
271
|
+
| Pattern | Problem | Solution |
|
|
272
|
+
|---------|---------|----------|
|
|
273
|
+
| Page Object | Duplicate locators across tests | Centralized selectors and actions |
|
|
274
|
+
| Custom fixture | Repeated setup per test | Shared auth, page objects via fixture |
|
|
275
|
+
| Storage state | Re-login per test is slow | Save/restore auth cookies |
|
|
276
|
+
| `page.route()` | Tests depend on live API | Mock API for deterministic data |
|
|
277
|
+
| `toHaveScreenshot()` | Visual bugs missed in review | Pixel-diff screenshots in CI |
|
|
278
|
+
|
|
279
|
+
## Checklist
|
|
280
|
+
- [ ] Tests use Page Object Model for reusable selectors and actions
|
|
281
|
+
- [ ] Custom fixtures provide page objects and authenticated contexts
|
|
282
|
+
- [ ] Auth state saved to file and reused across tests (not re-login per test)
|
|
283
|
+
- [ ] Network requests mocked for deterministic test data
|
|
284
|
+
- [ ] Locators use accessible roles (`getByRole`, `getByLabel`) not CSS selectors
|
|
285
|
+
- [ ] Visual regression screenshots baselined and checked on PR
|
|
286
|
+
- [ ] CI configured with retries, trace-on-failure, and artifact upload
|
|
287
|
+
- [ ] Tests run across at least Chromium, Firefox, and one mobile viewport
|