@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,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: refactoring
|
|
3
|
+
description: Refactoring patterns including extract function, introduce parameter object, replace conditional with polymorphism, and strangler fig.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Refactoring Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when code is correct but hard to understand, extend, or test. Refactor before adding new features to the area, after getting a feature working in TDD, or when code review reveals structural problems. Never refactor and change behavior in the same commit.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Extract Function
|
|
14
|
+
|
|
15
|
+
Pull a block of code into a named function when it does one identifiable thing. The signal: you write a comment explaining what the next block does. The comment is the function name.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Before -- one function doing three things
|
|
19
|
+
async function processOrder(order: Order) {
|
|
20
|
+
if (!order.items.length) throw new Error("Empty order");
|
|
21
|
+
if (order.items.some((i) => i.qty <= 0)) throw new Error("Invalid quantity");
|
|
22
|
+
|
|
23
|
+
let total = 0;
|
|
24
|
+
for (const item of order.items) {
|
|
25
|
+
total += item.price * item.qty;
|
|
26
|
+
if (item.taxable) total += item.price * item.qty * 0.08;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await db.orders.insert({ ...order, total, status: "pending" });
|
|
30
|
+
await emailService.send(order.email, "Order confirmed", `Total: $${total}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// After -- each concern is a named, testable function
|
|
34
|
+
const TAX_RATE = 0.08;
|
|
35
|
+
|
|
36
|
+
function validateOrder(order: Order): void {
|
|
37
|
+
if (!order.items.length) throw new Error("Empty order");
|
|
38
|
+
if (order.items.some((i) => i.qty <= 0)) throw new Error("Invalid quantity");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function calculateTotal(items: OrderItem[]): number {
|
|
42
|
+
return items.reduce((sum, item) => {
|
|
43
|
+
const subtotal = item.price * item.qty;
|
|
44
|
+
return sum + subtotal + (item.taxable ? subtotal * TAX_RATE : 0);
|
|
45
|
+
}, 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function processOrder(order: Order) {
|
|
49
|
+
validateOrder(order);
|
|
50
|
+
const total = calculateTotal(order.items);
|
|
51
|
+
await db.orders.insert({ ...order, total, status: "pending" });
|
|
52
|
+
await emailService.send(order.email, "Order confirmed", `Total: $${total}`);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Introduce Parameter Object
|
|
57
|
+
|
|
58
|
+
When three or more parameters travel together, group them:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// Before -- long parameter list
|
|
62
|
+
function searchProducts(
|
|
63
|
+
query: string, minPrice: number, maxPrice: number,
|
|
64
|
+
category: string, sortBy: string, page: number, pageSize: number
|
|
65
|
+
) { /* ... */ }
|
|
66
|
+
|
|
67
|
+
// After -- cohesive parameter object
|
|
68
|
+
interface ProductSearchParams {
|
|
69
|
+
query: string;
|
|
70
|
+
priceRange: { min: number; max: number };
|
|
71
|
+
category: string;
|
|
72
|
+
sort: { field: string; direction: "asc" | "desc" };
|
|
73
|
+
pagination: { page: number; pageSize: number };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function searchProducts(params: ProductSearchParams) { /* ... */ }
|
|
77
|
+
|
|
78
|
+
// Call site is self-documenting
|
|
79
|
+
const results = searchProducts({
|
|
80
|
+
query: "widget",
|
|
81
|
+
priceRange: { min: 10, max: 100 },
|
|
82
|
+
category: "hardware",
|
|
83
|
+
sort: { field: "price", direction: "asc" },
|
|
84
|
+
pagination: { page: 1, pageSize: 20 },
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Replace Conditional with Polymorphism
|
|
89
|
+
|
|
90
|
+
When a switch/if-else chain selects behavior based on a type field, use a strategy map:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Before -- switch grows with every new notification type
|
|
94
|
+
function sendNotification(notification: Notification) {
|
|
95
|
+
switch (notification.type) {
|
|
96
|
+
case "email": return sendEmail(notification.to, notification.body);
|
|
97
|
+
case "sms": return sendSms(notification.phone, notification.body);
|
|
98
|
+
case "push": return sendPush(notification.token, notification.body);
|
|
99
|
+
case "slack": return postSlack(notification.channel, notification.body);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// After -- each type handles itself
|
|
104
|
+
interface NotificationSender {
|
|
105
|
+
send(notification: Notification): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const senders: Record<string, NotificationSender> = {
|
|
109
|
+
email: new EmailSender(),
|
|
110
|
+
sms: new SmsSender(),
|
|
111
|
+
push: new PushSender(),
|
|
112
|
+
slack: new SlackSender(),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function sendNotification(notification: Notification) {
|
|
116
|
+
const sender = senders[notification.type];
|
|
117
|
+
if (!sender) throw new Error(`Unknown type: ${notification.type}`);
|
|
118
|
+
return sender.send(notification);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Strangler Fig Pattern
|
|
123
|
+
|
|
124
|
+
Migrate a legacy system incrementally by routing traffic through a facade:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Phase 1: Facade routes to old system
|
|
128
|
+
class OrderFacade {
|
|
129
|
+
async createOrder(data: OrderData): Promise<Order> {
|
|
130
|
+
return this.legacyOrderService.create(data);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Phase 2: Route some traffic to new system
|
|
135
|
+
class OrderFacade {
|
|
136
|
+
async createOrder(data: OrderData): Promise<Order> {
|
|
137
|
+
if (await this.featureFlags.isEnabled("new-order-service", data.userId)) {
|
|
138
|
+
return this.newOrderService.create(data);
|
|
139
|
+
}
|
|
140
|
+
return this.legacyOrderService.create(data);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Phase 3: All traffic to new system, legacy removed
|
|
145
|
+
class OrderFacade {
|
|
146
|
+
async createOrder(data: OrderData): Promise<Order> {
|
|
147
|
+
return this.newOrderService.create(data);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Simplify Boolean Logic with Guard Clauses
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Before -- nested conditions
|
|
156
|
+
function canUserPurchase(user: User, product: Product): boolean {
|
|
157
|
+
if (user.isActive) {
|
|
158
|
+
if (!user.isBanned) {
|
|
159
|
+
if (product.inStock) {
|
|
160
|
+
if (user.balance >= product.price) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// After -- guard clauses with early returns
|
|
170
|
+
function canUserPurchase(user: User, product: Product): boolean {
|
|
171
|
+
if (!user.isActive) return false;
|
|
172
|
+
if (user.isBanned) return false;
|
|
173
|
+
if (!product.inStock) return false;
|
|
174
|
+
if (user.balance < product.price) return false;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Replace Temp with Query
|
|
180
|
+
|
|
181
|
+
Eliminate intermediate variables that exist only to hold a computed value:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// Before -- temps obscure the logic
|
|
185
|
+
const basePrice = order.quantity * order.itemPrice;
|
|
186
|
+
const discount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
|
|
187
|
+
const shipping = Math.min(basePrice * 0.1, 100);
|
|
188
|
+
const total = basePrice - discount + shipping;
|
|
189
|
+
|
|
190
|
+
// After -- named, testable computed properties
|
|
191
|
+
class Order {
|
|
192
|
+
get basePrice(): number { return this.quantity * this.itemPrice; }
|
|
193
|
+
get discount(): number { return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05; }
|
|
194
|
+
get shipping(): number { return Math.min(this.basePrice * 0.1, 100); }
|
|
195
|
+
get total(): number { return this.basePrice - this.discount + this.shipping; }
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Examples
|
|
200
|
+
|
|
201
|
+
| Code Smell | Refactoring | Benefit |
|
|
202
|
+
|-----------|-------------|---------|
|
|
203
|
+
| Function > 30 lines | Extract Function | Each piece testable and named |
|
|
204
|
+
| 5+ positional parameters | Introduce Parameter Object | Self-documenting call sites |
|
|
205
|
+
| switch on type field | Replace Conditional with Polymorphism | Open for extension |
|
|
206
|
+
| Nested if/else 4 levels deep | Guard clauses | Linear, readable flow |
|
|
207
|
+
| Legacy system rewrite | Strangler Fig | Incremental, zero-downtime migration |
|
|
208
|
+
| Same 3 lines copy-pasted | Extract Function + call from each site | Single source of truth |
|
|
209
|
+
|
|
210
|
+
## Checklist
|
|
211
|
+
- [ ] Tests pass before and after every refactoring step
|
|
212
|
+
- [ ] Each commit is either a refactoring OR a behavior change, never both
|
|
213
|
+
- [ ] No function exceeds 30 lines (extract when it does)
|
|
214
|
+
- [ ] No parameter list exceeds 3 positional arguments
|
|
215
|
+
- [ ] Switch/if-else chains on type fields replaced with polymorphism
|
|
216
|
+
- [ ] Duplicated code extracted to a shared function or module
|
|
217
|
+
- [ ] Boolean conditions use guard clauses or named predicates
|
|
218
|
+
- [ ] Strangler fig pattern used for incremental legacy migration
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: regex-patterns
|
|
3
|
+
description: Write effective regex — lookahead/behind, named groups, common validation patterns, and performance pitfalls.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Regex Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Use regex for input validation, text extraction, search-and-replace, and log
|
|
11
|
+
parsing. This skill covers modern regex features (named groups, lookbehind,
|
|
12
|
+
Unicode properties), common real-world patterns (emails, URLs, dates), and
|
|
13
|
+
performance pitfalls that cause catastrophic backtracking. Apply when you need
|
|
14
|
+
structured pattern matching — but prefer dedicated parsers for HTML, JSON, or
|
|
15
|
+
complex grammars.
|
|
16
|
+
|
|
17
|
+
## How It Works
|
|
18
|
+
|
|
19
|
+
### 1. Named Groups — Readable Captures
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// Named groups make extracted data self-documenting
|
|
23
|
+
const datePattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
|
|
24
|
+
const match = '2026-05-26'.match(datePattern);
|
|
25
|
+
if (match?.groups) {
|
|
26
|
+
const { year, month, day } = match.groups;
|
|
27
|
+
// year = "2026", month = "05", day = "26"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Backreference by name
|
|
31
|
+
const duplicateWord = /\b(?<word>\w+)\s+\k<word>\b/gi;
|
|
32
|
+
'the the quick brown fox'.replace(duplicateWord, '$<word>');
|
|
33
|
+
// "the quick brown fox"
|
|
34
|
+
|
|
35
|
+
// Named groups in replace
|
|
36
|
+
const isoDate = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/g;
|
|
37
|
+
'Date: 2026-05-26'.replace(isoDate, '$<m>/$<d>/$<y>');
|
|
38
|
+
// "Date: 05/26/2026"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Lookahead and Lookbehind
|
|
42
|
+
|
|
43
|
+
Zero-width assertions — match a position, not characters.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Positive lookahead: match "foo" only if followed by "bar"
|
|
47
|
+
/foo(?=bar)/ // matches "foo" in "foobar", not in "foobaz"
|
|
48
|
+
|
|
49
|
+
// Negative lookahead: match "foo" only if NOT followed by "bar"
|
|
50
|
+
/foo(?!bar)/ // matches "foo" in "foobaz", not in "foobar"
|
|
51
|
+
|
|
52
|
+
// Positive lookbehind: match "bar" only if preceded by "foo"
|
|
53
|
+
/(?<=foo)bar/ // matches "bar" in "foobar", not in "bazbar"
|
|
54
|
+
|
|
55
|
+
// Negative lookbehind: match digits NOT preceded by "$"
|
|
56
|
+
/(?<!\$)\d+/ // matches "42" in "item 42", not "42" in "$42"
|
|
57
|
+
|
|
58
|
+
// Practical: extract price values without the currency symbol
|
|
59
|
+
const prices = 'Items: $29.99, EUR49.50, $100.00';
|
|
60
|
+
const usdPrices = prices.matchAll(/(?<=\$)\d+\.\d{2}/g);
|
|
61
|
+
// ["29.99", "100.00"]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Common Validation Patterns
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// Email (simplified — use a library for RFC 5322 compliance)
|
|
68
|
+
const EMAIL = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
69
|
+
|
|
70
|
+
// URL (http/https)
|
|
71
|
+
const URL_PATTERN = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
|
|
72
|
+
|
|
73
|
+
// ISO 8601 date (YYYY-MM-DD)
|
|
74
|
+
const ISO_DATE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
|
|
75
|
+
|
|
76
|
+
// Semantic version
|
|
77
|
+
const SEMVER = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<pre>[a-zA-Z0-9.]+))?(?:\+(?<build>[a-zA-Z0-9.]+))?$/;
|
|
78
|
+
|
|
79
|
+
// UUID v4
|
|
80
|
+
const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
81
|
+
|
|
82
|
+
// Password (min 8 chars, at least one upper, one lower, one digit)
|
|
83
|
+
const STRONG_PASSWORD = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
|
|
84
|
+
|
|
85
|
+
// IPv4 address
|
|
86
|
+
const IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})$/;
|
|
87
|
+
|
|
88
|
+
// Hex color (#RGB or #RRGGBB)
|
|
89
|
+
const HEX_COLOR = /^#(?:[0-9a-fA-F]{3}){1,2}$/;
|
|
90
|
+
|
|
91
|
+
// Phone (international E.164)
|
|
92
|
+
const PHONE_E164 = /^\+[1-9]\d{1,14}$/;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4. Text Extraction Patterns
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Extract all hashtags
|
|
99
|
+
const hashtags = text.matchAll(/#(\w+)/g);
|
|
100
|
+
|
|
101
|
+
// Extract key-value pairs from logs
|
|
102
|
+
const logLine = '2026-05-26T10:30:00Z level=error msg="Connection refused" host=db.prod retries=3';
|
|
103
|
+
const kvPattern = /(?<key>\w+)=(?:"(?<quoted>[^"]*)"|(?<bare>\S+))/g;
|
|
104
|
+
for (const m of logLine.matchAll(kvPattern)) {
|
|
105
|
+
const key = m.groups!.key;
|
|
106
|
+
const value = m.groups!.quoted ?? m.groups!.bare;
|
|
107
|
+
// { level: "error", msg: "Connection refused", host: "db.prod", retries: "3" }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Extract markdown links
|
|
111
|
+
const mdLinks = /\[(?<text>[^\]]+)\]\((?<url>[^)]+)\)/g;
|
|
112
|
+
for (const m of content.matchAll(mdLinks)) {
|
|
113
|
+
console.log(m.groups!.text, m.groups!.url);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Split on multiple delimiters
|
|
117
|
+
'one, two;three four'.split(/[,;\s]+/);
|
|
118
|
+
// ["one", "two", "three", "four"]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 5. Unicode-Aware Patterns
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Match any letter (including accented, CJK, etc.)
|
|
125
|
+
const anyLetter = /\p{Letter}+/gu;
|
|
126
|
+
'cafe resume'.match(anyLetter); // ["cafe", "resume"]
|
|
127
|
+
|
|
128
|
+
// Match emoji
|
|
129
|
+
const emojiPattern = /\p{Emoji_Presentation}/gu;
|
|
130
|
+
|
|
131
|
+
// Match currency symbols
|
|
132
|
+
const currencyPattern = /\p{Currency_Symbol}/gu;
|
|
133
|
+
|
|
134
|
+
// Always use the 'u' flag for Unicode correctness
|
|
135
|
+
const wordBoundary = /\b\w+\b/gu; // Unicode-safe word matching
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 6. Performance — Avoid Catastrophic Backtracking
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// BAD — exponential backtracking on non-matching input
|
|
142
|
+
const bad = /^(a+)+$/; // O(2^n) on "aaaaaaaaaaab"
|
|
143
|
+
|
|
144
|
+
// GOOD — possessive-style (use atomic groups or rewrite)
|
|
145
|
+
const good = /^a+$/; // O(n)
|
|
146
|
+
|
|
147
|
+
// BAD — nested quantifiers with overlap
|
|
148
|
+
const bad2 = /^(\w+\s*)*$/;
|
|
149
|
+
|
|
150
|
+
// GOOD — specific alternation
|
|
151
|
+
const good2 = /^[\w\s]+$/;
|
|
152
|
+
|
|
153
|
+
// Rule: Never nest quantifiers on overlapping character classes
|
|
154
|
+
// Test with: "aaaaaaaaaaaaaaaaaab" — if it hangs, you have backtracking
|
|
155
|
+
|
|
156
|
+
// Use RegExp timeout (Node.js 20+) or test with redos-detector
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 7. Flags Reference
|
|
160
|
+
|
|
161
|
+
| Flag | Name | Effect |
|
|
162
|
+
|------|------|--------|
|
|
163
|
+
| `g` | global | Match all occurrences, not just first |
|
|
164
|
+
| `i` | ignoreCase | Case-insensitive matching |
|
|
165
|
+
| `m` | multiline | `^`/`$` match line starts/ends |
|
|
166
|
+
| `s` | dotAll | `.` matches newlines too |
|
|
167
|
+
| `u` | unicode | Enable Unicode property escapes |
|
|
168
|
+
| `v` | unicodeSets | Extended Unicode sets (ES2024) |
|
|
169
|
+
| `d` | hasIndices | Include start/end indices in match |
|
|
170
|
+
|
|
171
|
+
## Examples
|
|
172
|
+
|
|
173
|
+
| Pattern | Matches | Note |
|
|
174
|
+
|---------|---------|------|
|
|
175
|
+
| `(?<=@)\w+\.\w+` | Domain from email | Lookbehind extracts without `@` |
|
|
176
|
+
| `\b\d{1,3}(,\d{3})*\b` | Formatted numbers | `1,000,000` but not `12,3` |
|
|
177
|
+
| `(?:https?://)?\S+\.\w{2,}` | URLs with optional scheme | Non-capturing group |
|
|
178
|
+
| `^(?!.*password).*$` | Lines without "password" | Negative lookahead filter |
|
|
179
|
+
|
|
180
|
+
## Checklist
|
|
181
|
+
|
|
182
|
+
- [ ] Named groups used instead of positional `$1`, `$2` captures
|
|
183
|
+
- [ ] Unicode flag (`u` or `v`) enabled for international text
|
|
184
|
+
- [ ] No nested quantifiers on overlapping character classes
|
|
185
|
+
- [ ] Patterns tested against non-matching input for backtracking
|
|
186
|
+
- [ ] `matchAll` used instead of `exec` loop for global matches
|
|
187
|
+
- [ ] Complex validation uses a library, not a single mega-regex
|
|
188
|
+
- [ ] Regex documented with comments (use `x` flag or string concatenation)
|
|
189
|
+
- [ ] Lookaheads/lookbehinds used to avoid consuming matched text
|
|
190
|
+
- [ ] Common patterns extracted to named constants, not inline literals
|
|
191
|
+
- [ ] Regex tested with edge cases: empty string, Unicode, very long input
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: remix-patterns
|
|
3
|
+
description: Remix patterns for loaders, actions, nested routes, error boundaries, and progressive enhancement.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Remix Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use Remix for full-stack web applications where you want server-rendered pages with progressive enhancement. Remix excels at data loading, form handling, and nested layouts. It works without JavaScript on the client by default, making it ideal for apps that need accessibility, SEO, and resilient UX. Choose Remix when you want to lean into web platform standards rather than fight them.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Loaders (Server-Side Data Fetching)
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// app/routes/posts._index.tsx
|
|
17
|
+
import type { LoaderFunctionArgs } from '@remix-run/node';
|
|
18
|
+
import { json } from '@remix-run/node';
|
|
19
|
+
import { useLoaderData, Link } from '@remix-run/react';
|
|
20
|
+
import { db } from '~/lib/db.server';
|
|
21
|
+
|
|
22
|
+
export async function loader({ request }: LoaderFunctionArgs) {
|
|
23
|
+
const url = new URL(request.url);
|
|
24
|
+
const page = parseInt(url.searchParams.get('page') ?? '1');
|
|
25
|
+
const limit = 20;
|
|
26
|
+
|
|
27
|
+
const [posts, total] = await Promise.all([
|
|
28
|
+
db.post.findMany({
|
|
29
|
+
take: limit,
|
|
30
|
+
skip: (page - 1) * limit,
|
|
31
|
+
orderBy: { createdAt: 'desc' },
|
|
32
|
+
select: { id: true, title: true, slug: true, createdAt: true },
|
|
33
|
+
}),
|
|
34
|
+
db.post.count(),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
return json(
|
|
38
|
+
{ posts, page, totalPages: Math.ceil(total / limit) },
|
|
39
|
+
{ headers: { 'Cache-Control': 'public, max-age=60, s-maxage=300' } }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function Posts() {
|
|
44
|
+
const { posts, page, totalPages } = useLoaderData<typeof loader>();
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<h1>Posts</h1>
|
|
49
|
+
<ul>
|
|
50
|
+
{posts.map((post) => (
|
|
51
|
+
<li key={post.id}>
|
|
52
|
+
<Link to={`/posts/${post.slug}`} prefetch="intent">{post.title}</Link>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
<nav>
|
|
57
|
+
{page > 1 && <Link to={`?page=${page - 1}`}>Previous</Link>}
|
|
58
|
+
<span>Page {page} of {totalPages}</span>
|
|
59
|
+
{page < totalPages && <Link to={`?page=${page + 1}`}>Next</Link>}
|
|
60
|
+
</nav>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Actions (Form Handling)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// app/routes/posts.new.tsx
|
|
70
|
+
import type { ActionFunctionArgs } from '@remix-run/node';
|
|
71
|
+
import { json, redirect } from '@remix-run/node';
|
|
72
|
+
import { Form, useActionData, useNavigation } from '@remix-run/react';
|
|
73
|
+
import { db } from '~/lib/db.server';
|
|
74
|
+
import { requireAuth } from '~/lib/auth.server';
|
|
75
|
+
|
|
76
|
+
export async function action({ request }: ActionFunctionArgs) {
|
|
77
|
+
const user = await requireAuth(request);
|
|
78
|
+
const formData = await request.formData();
|
|
79
|
+
|
|
80
|
+
const title = formData.get('title')?.toString().trim();
|
|
81
|
+
const body = formData.get('body')?.toString().trim();
|
|
82
|
+
|
|
83
|
+
const errors: Record<string, string> = {};
|
|
84
|
+
if (!title || title.length < 3) errors.title = 'Title must be at least 3 characters';
|
|
85
|
+
if (!body || body.length < 10) errors.body = 'Body must be at least 10 characters';
|
|
86
|
+
|
|
87
|
+
if (Object.keys(errors).length > 0) {
|
|
88
|
+
return json({ errors, values: { title, body } }, { status: 400 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const slug = title!.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
92
|
+
const post = await db.post.create({ data: { title: title!, body: body!, slug, authorId: user.id } });
|
|
93
|
+
|
|
94
|
+
return redirect(`/posts/${post.slug}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default function NewPost() {
|
|
98
|
+
const actionData = useActionData<typeof action>();
|
|
99
|
+
const navigation = useNavigation();
|
|
100
|
+
const isSubmitting = navigation.state === 'submitting';
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Form method="post">
|
|
104
|
+
<div>
|
|
105
|
+
<label htmlFor="title">Title</label>
|
|
106
|
+
<input
|
|
107
|
+
id="title"
|
|
108
|
+
name="title"
|
|
109
|
+
defaultValue={actionData?.values?.title ?? ''}
|
|
110
|
+
aria-invalid={!!actionData?.errors?.title}
|
|
111
|
+
aria-describedby={actionData?.errors?.title ? 'title-error' : undefined}
|
|
112
|
+
/>
|
|
113
|
+
{actionData?.errors?.title && <p id="title-error">{actionData.errors.title}</p>}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div>
|
|
117
|
+
<label htmlFor="body">Body</label>
|
|
118
|
+
<textarea id="body" name="body" rows={10} defaultValue={actionData?.values?.body ?? ''} />
|
|
119
|
+
{actionData?.errors?.body && <p>{actionData.errors.body}</p>}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<button type="submit" disabled={isSubmitting}>
|
|
123
|
+
{isSubmitting ? 'Creating...' : 'Create Post'}
|
|
124
|
+
</button>
|
|
125
|
+
</Form>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Nested Routes and Layouts
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// app/routes/dashboard.tsx — layout route
|
|
134
|
+
import { Outlet, NavLink } from '@remix-run/react';
|
|
135
|
+
|
|
136
|
+
export default function DashboardLayout() {
|
|
137
|
+
return (
|
|
138
|
+
<div className="flex">
|
|
139
|
+
<aside className="w-64">
|
|
140
|
+
<nav>
|
|
141
|
+
<NavLink to="/dashboard" end className={({ isActive }) => isActive ? 'font-bold' : ''}>
|
|
142
|
+
Overview
|
|
143
|
+
</NavLink>
|
|
144
|
+
<NavLink to="/dashboard/analytics" className={({ isActive }) => isActive ? 'font-bold' : ''}>
|
|
145
|
+
Analytics
|
|
146
|
+
</NavLink>
|
|
147
|
+
<NavLink to="/dashboard/settings" className={({ isActive }) => isActive ? 'font-bold' : ''}>
|
|
148
|
+
Settings
|
|
149
|
+
</NavLink>
|
|
150
|
+
</nav>
|
|
151
|
+
</aside>
|
|
152
|
+
<main className="flex-1">
|
|
153
|
+
<Outlet /> {/* Child routes render here */}
|
|
154
|
+
</main>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// app/routes/dashboard._index.tsx — renders at /dashboard
|
|
160
|
+
// app/routes/dashboard.analytics.tsx — renders at /dashboard/analytics
|
|
161
|
+
// app/routes/dashboard.settings.tsx — renders at /dashboard/settings
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Error Boundaries
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// app/routes/posts.$slug.tsx
|
|
168
|
+
import { isRouteErrorResponse, useRouteError } from '@remix-run/react';
|
|
169
|
+
|
|
170
|
+
export function ErrorBoundary() {
|
|
171
|
+
const error = useRouteError();
|
|
172
|
+
|
|
173
|
+
if (isRouteErrorResponse(error)) {
|
|
174
|
+
return (
|
|
175
|
+
<div className="error-container">
|
|
176
|
+
<h1>{error.status} {error.statusText}</h1>
|
|
177
|
+
<p>{error.data?.message ?? 'Something went wrong'}</p>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className="error-container">
|
|
184
|
+
<h1>Unexpected Error</h1>
|
|
185
|
+
<p>{error instanceof Error ? error.message : 'Unknown error'}</p>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// In loaders, throw responses to trigger error boundaries:
|
|
191
|
+
export async function loader({ params }: LoaderFunctionArgs) {
|
|
192
|
+
const post = await db.post.findUnique({ where: { slug: params.slug } });
|
|
193
|
+
if (!post) throw json({ message: 'Post not found' }, { status: 404 });
|
|
194
|
+
return json({ post });
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Resource Routes (API Endpoints)
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// app/routes/api.posts.ts — no default export = resource route
|
|
202
|
+
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node';
|
|
203
|
+
import { json } from '@remix-run/node';
|
|
204
|
+
|
|
205
|
+
export async function loader({ request }: LoaderFunctionArgs) {
|
|
206
|
+
const posts = await db.post.findMany({ take: 50 });
|
|
207
|
+
return json(posts);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function action({ request }: ActionFunctionArgs) {
|
|
211
|
+
if (request.method === 'DELETE') {
|
|
212
|
+
const { id } = await request.json();
|
|
213
|
+
await db.post.delete({ where: { id } });
|
|
214
|
+
return json({ success: true });
|
|
215
|
+
}
|
|
216
|
+
return json({ error: 'Method not allowed' }, { status: 405 });
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Optimistic UI
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// app/routes/todos.tsx
|
|
224
|
+
import { useFetcher } from '@remix-run/react';
|
|
225
|
+
|
|
226
|
+
function TodoItem({ todo }: { todo: { id: string; title: string; done: boolean } }) {
|
|
227
|
+
const fetcher = useFetcher();
|
|
228
|
+
const optimisticDone = fetcher.formData
|
|
229
|
+
? fetcher.formData.get('done') === 'true'
|
|
230
|
+
: todo.done;
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<fetcher.Form method="post" action="/todos/toggle">
|
|
234
|
+
<input type="hidden" name="id" value={todo.id} />
|
|
235
|
+
<input type="hidden" name="done" value={String(!todo.done)} />
|
|
236
|
+
<button type="submit" className={optimisticDone ? 'line-through' : ''}>
|
|
237
|
+
{todo.title}
|
|
238
|
+
</button>
|
|
239
|
+
</fetcher.Form>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Examples
|
|
245
|
+
|
|
246
|
+
| Pattern | HTTP Method | URL | Purpose |
|
|
247
|
+
|---------|-------------|-----|---------|
|
|
248
|
+
| Loader | GET | `/posts` | Fetch list of posts |
|
|
249
|
+
| Action | POST | `/posts/new` | Create a new post |
|
|
250
|
+
| Resource route | GET/DELETE | `/api/posts` | JSON API endpoint |
|
|
251
|
+
| Nested route | GET | `/dashboard/analytics` | Child layout rendering |
|
|
252
|
+
| Fetcher | POST | `/todos/toggle` | In-place mutation without navigation |
|
|
253
|
+
|
|
254
|
+
## Checklist
|
|
255
|
+
- [ ] Loaders use `json()` with appropriate Cache-Control headers
|
|
256
|
+
- [ ] Actions validate form data and return errors with `status: 400`
|
|
257
|
+
- [ ] Forms use `<Form method="post">` for progressive enhancement
|
|
258
|
+
- [ ] Error boundaries handle both route errors and unexpected errors
|
|
259
|
+
- [ ] Links use `prefetch="intent"` for preloading on hover
|
|
260
|
+
- [ ] `useNavigation().state` used to show loading states during transitions
|
|
261
|
+
- [ ] Resource routes return proper status codes and Content-Type headers
|
|
262
|
+
- [ ] Server-only imports use `.server.ts` suffix to prevent client bundling
|