mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-20 06:11:27 -07:00
update skills
This commit is contained in:
105
.agents/skills/cloudflare-deploy/references/miniflare/README.md
Normal file
105
.agents/skills/cloudflare-deploy/references/miniflare/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Miniflare
|
||||
|
||||
Local simulator for Cloudflare Workers development/testing. Runs Workers in workerd sandbox implementing runtime APIs - no internet required.
|
||||
|
||||
## Features
|
||||
|
||||
- Full-featured: KV, Durable Objects, R2, D1, WebSockets, Queues
|
||||
- Fully-local: test without internet, instant reload
|
||||
- TypeScript-native: detailed logging, source maps
|
||||
- Advanced testing: dispatch events without HTTP, simulate Worker connections
|
||||
|
||||
## When to Use
|
||||
|
||||
**Decision tree for testing Workers:**
|
||||
|
||||
```
|
||||
Need to test Workers?
|
||||
│
|
||||
├─ Unit tests for business logic only?
|
||||
│ └─ getPlatformProxy (Vitest/Jest) → [patterns.md](./patterns.md#getplatformproxy)
|
||||
│ Fast, no HTTP, direct binding access
|
||||
│
|
||||
├─ Integration tests with full runtime?
|
||||
│ ├─ Single Worker?
|
||||
│ │ └─ Miniflare API → [Quick Start](#quick-start)
|
||||
│ │ Full control, programmatic access
|
||||
│ │
|
||||
│ ├─ Multiple Workers + service bindings?
|
||||
│ │ └─ Miniflare workers array → [configuration.md](./configuration.md#multiple-workers)
|
||||
│ │ Shared storage, inter-worker calls
|
||||
│ │
|
||||
│ └─ Vitest test runner integration?
|
||||
│ └─ vitest-pool-workers → [patterns.md](./patterns.md#vitest-pool-workers)
|
||||
│ Full Workers env in Vitest
|
||||
│
|
||||
└─ Local dev server?
|
||||
└─ wrangler dev (not Miniflare)
|
||||
Hot reload, automatic config
|
||||
```
|
||||
|
||||
**Use Miniflare for:**
|
||||
- Integration tests with full Worker runtime
|
||||
- Testing bindings/storage locally
|
||||
- Multiple Workers with service bindings
|
||||
- Programmatic event dispatch (fetch, queue, scheduled)
|
||||
|
||||
**Use getPlatformProxy for:**
|
||||
- Fast unit tests of business logic
|
||||
- Testing without HTTP overhead
|
||||
- Vitest/Jest environments
|
||||
|
||||
**Use Wrangler for:**
|
||||
- Local development workflow
|
||||
- Production deployments
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
npm i -D miniflare
|
||||
```
|
||||
|
||||
Requires ES modules in `package.json`:
|
||||
```json
|
||||
{"type": "module"}
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```js
|
||||
import { Miniflare } from "miniflare";
|
||||
|
||||
const mf = new Miniflare({
|
||||
modules: true,
|
||||
script: `
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return new Response("Hello Miniflare!");
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const res = await mf.dispatchFetch("http://localhost:8787/");
|
||||
console.log(await res.text()); // Hello Miniflare!
|
||||
await mf.dispose();
|
||||
```
|
||||
|
||||
## Reading Order
|
||||
|
||||
**New to Miniflare?** Start here:
|
||||
1. [Quick Start](#quick-start) - Running in 2 minutes
|
||||
2. [When to Use](#when-to-use) - Choose your testing approach
|
||||
3. [patterns.md](./patterns.md) - Testing patterns (getPlatformProxy, Vitest, node:test)
|
||||
4. [configuration.md](./configuration.md) - Configure bindings, storage, multiple workers
|
||||
|
||||
**Troubleshooting:**
|
||||
- [gotchas.md](./gotchas.md) - Common errors and debugging
|
||||
|
||||
**API reference:**
|
||||
- [api.md](./api.md) - Complete method reference
|
||||
|
||||
## See Also
|
||||
- [wrangler](../wrangler/) - CLI tool that embeds Miniflare for `wrangler dev`
|
||||
- [workerd](../workerd/) - Runtime that powers Miniflare
|
||||
- [workers](../workers/) - Workers runtime API documentation
|
||||
187
.agents/skills/cloudflare-deploy/references/miniflare/api.md
Normal file
187
.agents/skills/cloudflare-deploy/references/miniflare/api.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Programmatic API
|
||||
|
||||
## Miniflare Class
|
||||
|
||||
```typescript
|
||||
class Miniflare {
|
||||
constructor(options: MiniflareOptions);
|
||||
|
||||
// Lifecycle
|
||||
ready: Promise<URL>; // Resolves when server ready, returns URL
|
||||
dispose(): Promise<void>; // Cleanup resources
|
||||
setOptions(options: MiniflareOptions): Promise<void>; // Reload config
|
||||
|
||||
// Event dispatching
|
||||
dispatchFetch(url: string | URL | Request, init?: RequestInit): Promise<Response>;
|
||||
getWorker(name?: string): Promise<Worker>;
|
||||
|
||||
// Bindings access
|
||||
getBindings<Bindings = Record<string, unknown>>(name?: string): Promise<Bindings>;
|
||||
getCf(name?: string): Promise<IncomingRequestCfProperties | undefined>;
|
||||
getKVNamespace(name: string): Promise<KVNamespace>;
|
||||
getR2Bucket(name: string): Promise<R2Bucket>;
|
||||
getDurableObjectNamespace(name: string): Promise<DurableObjectNamespace>;
|
||||
getDurableObjectStorage(id: DurableObjectId): Promise<DurableObjectStorage>;
|
||||
getD1Database(name: string): Promise<D1Database>;
|
||||
getCaches(): Promise<CacheStorage>;
|
||||
getQueueProducer(name: string): Promise<QueueProducer>;
|
||||
|
||||
// Debugging
|
||||
getInspectorURL(): Promise<URL>; // Chrome DevTools inspector URL
|
||||
}
|
||||
```
|
||||
|
||||
## Event Dispatching
|
||||
|
||||
**Fetch (no HTTP server):**
|
||||
```js
|
||||
const res = await mf.dispatchFetch("http://localhost:8787/path", {
|
||||
method: "POST",
|
||||
headers: { "Authorization": "Bearer token" },
|
||||
body: JSON.stringify({ data: "value" }),
|
||||
});
|
||||
```
|
||||
|
||||
**Custom Host routing:**
|
||||
```js
|
||||
const res = await mf.dispatchFetch("http://localhost:8787/", {
|
||||
headers: { "Host": "api.example.com" },
|
||||
});
|
||||
```
|
||||
|
||||
**Scheduled:**
|
||||
```js
|
||||
const worker = await mf.getWorker();
|
||||
const result = await worker.scheduled({ cron: "30 * * * *" });
|
||||
// result: { outcome: "ok", noRetry: false }
|
||||
```
|
||||
|
||||
**Queue:**
|
||||
```js
|
||||
const worker = await mf.getWorker();
|
||||
const result = await worker.queue("queue-name", [
|
||||
{ id: "msg1", timestamp: new Date(), body: "data", attempts: 1 },
|
||||
]);
|
||||
// result: { outcome: "ok", retryAll: false, ackAll: false, ... }
|
||||
```
|
||||
|
||||
## Bindings Access
|
||||
|
||||
**Environment variables:**
|
||||
```js
|
||||
// Basic usage
|
||||
const bindings = await mf.getBindings();
|
||||
console.log(bindings.SECRET_KEY);
|
||||
|
||||
// With type safety (recommended):
|
||||
interface Env {
|
||||
SECRET_KEY: string;
|
||||
API_URL: string;
|
||||
KV: KVNamespace;
|
||||
}
|
||||
const env = await mf.getBindings<Env>();
|
||||
env.SECRET_KEY; // string (typed!)
|
||||
env.KV.get("key"); // KVNamespace methods available
|
||||
```
|
||||
|
||||
**Request.cf object:**
|
||||
```js
|
||||
const cf = await mf.getCf();
|
||||
console.log(cf?.colo); // "DFW"
|
||||
console.log(cf?.country); // "US"
|
||||
```
|
||||
|
||||
**KV:**
|
||||
```js
|
||||
const ns = await mf.getKVNamespace("TEST_NAMESPACE");
|
||||
await ns.put("key", "value");
|
||||
const value = await ns.get("key");
|
||||
```
|
||||
|
||||
**R2:**
|
||||
```js
|
||||
const bucket = await mf.getR2Bucket("BUCKET");
|
||||
await bucket.put("file.txt", "content");
|
||||
const object = await bucket.get("file.txt");
|
||||
```
|
||||
|
||||
**Durable Objects:**
|
||||
```js
|
||||
const ns = await mf.getDurableObjectNamespace("COUNTER");
|
||||
const id = ns.idFromName("test");
|
||||
const stub = ns.get(id);
|
||||
const res = await stub.fetch("http://localhost/");
|
||||
|
||||
// Access storage directly:
|
||||
const storage = await mf.getDurableObjectStorage(id);
|
||||
await storage.put("key", "value");
|
||||
```
|
||||
|
||||
**D1:**
|
||||
```js
|
||||
const db = await mf.getD1Database("DB");
|
||||
await db.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`);
|
||||
await db.prepare("INSERT INTO users (name) VALUES (?)").bind("Alice").run();
|
||||
```
|
||||
|
||||
**Cache:**
|
||||
```js
|
||||
const caches = await mf.getCaches();
|
||||
const defaultCache = caches.default;
|
||||
await defaultCache.put("http://example.com", new Response("cached"));
|
||||
```
|
||||
|
||||
**Queue producer:**
|
||||
```js
|
||||
const producer = await mf.getQueueProducer("QUEUE");
|
||||
await producer.send({ body: "message data" });
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
**Reload:**
|
||||
```js
|
||||
await mf.setOptions({
|
||||
scriptPath: "worker.js",
|
||||
bindings: { VERSION: "2.0" },
|
||||
});
|
||||
```
|
||||
|
||||
**Watch (manual):**
|
||||
```js
|
||||
import { watch } from "fs";
|
||||
|
||||
const config = { scriptPath: "worker.js" };
|
||||
const mf = new Miniflare(config);
|
||||
|
||||
watch("worker.js", async () => {
|
||||
console.log("Reloading...");
|
||||
await mf.setOptions(config);
|
||||
});
|
||||
```
|
||||
|
||||
**Cleanup:**
|
||||
```js
|
||||
await mf.dispose();
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
**Inspector URL for DevTools:**
|
||||
```js
|
||||
const url = await mf.getInspectorURL();
|
||||
console.log(`DevTools: ${url}`);
|
||||
// Open in Chrome DevTools for breakpoints, profiling
|
||||
```
|
||||
|
||||
**Wait for server ready:**
|
||||
```js
|
||||
const mf = new Miniflare({ scriptPath: "worker.js" });
|
||||
const url = await mf.ready; // Promise<URL>
|
||||
console.log(`Server running at ${url}`); // http://127.0.0.1:8787
|
||||
|
||||
// Note: dispatchFetch() waits automatically, no need to await ready
|
||||
const res = await mf.dispatchFetch("http://localhost/"); // Works immediately
|
||||
```
|
||||
|
||||
See [configuration.md](./configuration.md) for all constructor options.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Configuration
|
||||
|
||||
## Script Loading
|
||||
|
||||
```js
|
||||
// Inline
|
||||
new Miniflare({ modules: true, script: `export default { ... }` });
|
||||
|
||||
// File-based
|
||||
new Miniflare({ scriptPath: "worker.js" });
|
||||
|
||||
// Multi-module
|
||||
new Miniflare({
|
||||
scriptPath: "src/index.js",
|
||||
modules: true,
|
||||
modulesRules: [
|
||||
{ type: "ESModule", include: ["**/*.js"] },
|
||||
{ type: "Text", include: ["**/*.txt"] },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
```js
|
||||
new Miniflare({
|
||||
compatibilityDate: "2026-01-01", // Use recent date for latest features
|
||||
compatibilityFlags: [
|
||||
"nodejs_compat", // Node.js APIs (process, Buffer, etc)
|
||||
"streams_enable_constructors", // Stream constructors
|
||||
],
|
||||
upstream: "https://example.com", // Fallback for unhandled requests
|
||||
});
|
||||
```
|
||||
|
||||
**Critical:** Use `compatibilityDate: "2026-01-01"` or latest to match production runtime. Old dates limit available APIs.
|
||||
|
||||
## HTTP Server & Request.cf
|
||||
|
||||
```js
|
||||
new Miniflare({
|
||||
port: 8787, // Default: 8787
|
||||
host: "127.0.0.1",
|
||||
https: true, // Self-signed cert
|
||||
liveReload: true, // Auto-reload HTML
|
||||
|
||||
cf: true, // Fetch live Request.cf data (cached)
|
||||
// cf: "./cf.json", // Or load from file
|
||||
// cf: { colo: "DFW" }, // Or inline mock
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** For tests, use `dispatchFetch()` (no port conflicts).
|
||||
|
||||
## Storage Bindings
|
||||
|
||||
```js
|
||||
new Miniflare({
|
||||
// KV
|
||||
kvNamespaces: ["TEST_NAMESPACE", "CACHE"],
|
||||
kvPersist: "./kv-data", // Optional: persist to disk
|
||||
|
||||
// R2
|
||||
r2Buckets: ["BUCKET", "IMAGES"],
|
||||
r2Persist: "./r2-data",
|
||||
|
||||
// Durable Objects
|
||||
modules: true,
|
||||
durableObjects: {
|
||||
COUNTER: "Counter", // className
|
||||
API_OBJECT: { className: "ApiObject", scriptName: "api-worker" },
|
||||
},
|
||||
durableObjectsPersist: "./do-data",
|
||||
|
||||
// D1
|
||||
d1Databases: ["DB"],
|
||||
d1Persist: "./d1-data",
|
||||
|
||||
// Cache
|
||||
cache: true, // Default
|
||||
cachePersist: "./cache-data",
|
||||
});
|
||||
```
|
||||
|
||||
## Bindings
|
||||
|
||||
```js
|
||||
new Miniflare({
|
||||
// Environment variables
|
||||
bindings: {
|
||||
SECRET_KEY: "my-secret-value",
|
||||
API_URL: "https://api.example.com",
|
||||
DEBUG: true,
|
||||
},
|
||||
|
||||
// Other bindings
|
||||
wasmBindings: { ADD_MODULE: "./add.wasm" },
|
||||
textBlobBindings: { TEXT: "./data.txt" },
|
||||
queueProducers: ["QUEUE"],
|
||||
});
|
||||
```
|
||||
|
||||
## Multiple Workers
|
||||
|
||||
```js
|
||||
new Miniflare({
|
||||
workers: [
|
||||
{
|
||||
name: "main",
|
||||
kvNamespaces: { DATA: "shared" },
|
||||
serviceBindings: { API: "api-worker" },
|
||||
script: `export default { ... }`,
|
||||
},
|
||||
{
|
||||
name: "api-worker",
|
||||
kvNamespaces: { DATA: "shared" }, // Shared storage
|
||||
script: `export default { ... }`,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
**With routing:**
|
||||
```js
|
||||
workers: [
|
||||
{ name: "api", scriptPath: "./api.js", routes: ["api.example.com/*"] },
|
||||
{ name: "web", scriptPath: "./web.js", routes: ["example.com/*"] },
|
||||
],
|
||||
```
|
||||
|
||||
## Logging & Performance
|
||||
|
||||
```js
|
||||
import { Log, LogLevel } from "miniflare";
|
||||
|
||||
new Miniflare({
|
||||
log: new Log(LogLevel.DEBUG), // DEBUG | INFO | WARN | ERROR | NONE
|
||||
scriptTimeout: 30000, // CPU limit (ms)
|
||||
workersConcurrencyLimit: 10, // Max concurrent workers
|
||||
});
|
||||
```
|
||||
|
||||
## Workers Sites
|
||||
|
||||
```js
|
||||
new Miniflare({
|
||||
sitePath: "./public",
|
||||
siteInclude: ["**/*.html", "**/*.css"],
|
||||
siteExclude: ["**/*.map"],
|
||||
});
|
||||
```
|
||||
|
||||
## From wrangler.toml
|
||||
|
||||
Miniflare doesn't auto-read `wrangler.toml`:
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
name = "my-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2026-01-01"
|
||||
[[kv_namespaces]]
|
||||
binding = "KV"
|
||||
```
|
||||
|
||||
```js
|
||||
// Miniflare equivalent
|
||||
new Miniflare({
|
||||
scriptPath: "src/index.ts",
|
||||
compatibilityDate: "2026-01-01",
|
||||
kvNamespaces: ["KV"],
|
||||
});
|
||||
```
|
||||
160
.agents/skills/cloudflare-deploy/references/miniflare/gotchas.md
Normal file
160
.agents/skills/cloudflare-deploy/references/miniflare/gotchas.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Gotchas & Troubleshooting
|
||||
|
||||
## Miniflare Limitations
|
||||
|
||||
**Not supported:**
|
||||
- Analytics Engine (use mocks)
|
||||
- Cloudflare Images/Stream
|
||||
- Browser Rendering API
|
||||
- Tail Workers
|
||||
- Workers for Platforms (partial support)
|
||||
|
||||
**Behavior differences from production:**
|
||||
- Runs workerd locally, not Cloudflare edge
|
||||
- Storage is local (filesystem/memory), not distributed
|
||||
- `Request.cf` is cached/mocked, not real edge data
|
||||
- Performance differs from edge
|
||||
- Caching implementation may vary slightly
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Cannot find module"
|
||||
**Cause:** Module path wrong or `modulesRules` not configured
|
||||
**Solution:**
|
||||
```js
|
||||
new Miniflare({
|
||||
modules: true,
|
||||
modulesRules: [{ type: "ESModule", include: ["**/*.js"] }],
|
||||
});
|
||||
```
|
||||
|
||||
### "Data not persisting"
|
||||
**Cause:** Persist paths are files, not directories
|
||||
**Solution:**
|
||||
```js
|
||||
kvPersist: "./data/kv", // Directory, not file
|
||||
```
|
||||
|
||||
### "Cannot run TypeScript"
|
||||
**Cause:** Miniflare doesn't transpile TypeScript
|
||||
**Solution:** Build first with esbuild/tsc, then run compiled JS
|
||||
|
||||
### "`request.cf` is undefined"
|
||||
**Cause:** CF data not configured
|
||||
**Solution:**
|
||||
```js
|
||||
new Miniflare({ cf: true }); // Or cf: "./cf.json"
|
||||
```
|
||||
|
||||
### "EADDRINUSE" port conflict
|
||||
**Cause:** Multiple instances using same port
|
||||
**Solution:** Use `dispatchFetch()` (no HTTP server) or `port: 0` for auto-assign
|
||||
|
||||
### "Durable Object not found"
|
||||
**Cause:** Class export doesn't match config name
|
||||
**Solution:**
|
||||
```js
|
||||
export class Counter {} // Must match
|
||||
new Miniflare({ durableObjects: { COUNTER: "Counter" } });
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
**Enable verbose logging:**
|
||||
```js
|
||||
import { Log, LogLevel } from "miniflare";
|
||||
new Miniflare({ log: new Log(LogLevel.DEBUG) });
|
||||
```
|
||||
|
||||
**Chrome DevTools:**
|
||||
```js
|
||||
const url = await mf.getInspectorURL();
|
||||
console.log(`DevTools: ${url}`); // Open in Chrome
|
||||
```
|
||||
|
||||
**Inspect bindings:**
|
||||
```js
|
||||
const env = await mf.getBindings();
|
||||
console.log(Object.keys(env));
|
||||
```
|
||||
|
||||
**Verify storage:**
|
||||
```js
|
||||
const ns = await mf.getKVNamespace("TEST");
|
||||
const { keys } = await ns.list();
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
**✓ Do:**
|
||||
- Use `dispatchFetch()` for tests (no HTTP server)
|
||||
- In-memory storage for CI (omit persist options)
|
||||
- New instances per test for isolation
|
||||
- Type-safe bindings with interfaces
|
||||
- `await mf.dispose()` in cleanup
|
||||
|
||||
**✗ Avoid:**
|
||||
- HTTP server in tests
|
||||
- Shared instances without cleanup
|
||||
- Old compatibility dates (use 2026+)
|
||||
|
||||
## Migration Guides
|
||||
|
||||
### From Miniflare 2.x to 3+
|
||||
|
||||
Breaking changes in v3+:
|
||||
|
||||
| v2 | v3+ |
|
||||
|----|-----|
|
||||
| `getBindings()` sync | `getBindings()` returns Promise |
|
||||
| `ready` is void | `ready` returns `Promise<URL>` |
|
||||
| service-worker-mock | Built on workerd |
|
||||
| Different options | Restructured constructor |
|
||||
|
||||
**Example migration:**
|
||||
```js
|
||||
// v2
|
||||
const bindings = mf.getBindings();
|
||||
mf.ready; // void
|
||||
|
||||
// v3+
|
||||
const bindings = await mf.getBindings();
|
||||
const url = await mf.ready; // Promise<URL>
|
||||
```
|
||||
|
||||
### From unstable_dev to Miniflare
|
||||
|
||||
```js
|
||||
// Old (deprecated)
|
||||
import { unstable_dev } from "wrangler";
|
||||
const worker = await unstable_dev("src/index.ts");
|
||||
|
||||
// New
|
||||
import { Miniflare } from "miniflare";
|
||||
const mf = new Miniflare({ scriptPath: "src/index.ts" });
|
||||
```
|
||||
|
||||
### From Wrangler Dev
|
||||
|
||||
Miniflare doesn't auto-read `wrangler.toml`:
|
||||
|
||||
```js
|
||||
// Translate manually:
|
||||
new Miniflare({
|
||||
scriptPath: "dist/worker.js",
|
||||
compatibilityDate: "2026-01-01",
|
||||
kvNamespaces: ["KV"],
|
||||
bindings: { API_KEY: process.env.API_KEY },
|
||||
});
|
||||
```
|
||||
|
||||
## Resource Limits
|
||||
|
||||
| Limit | Value | Notes |
|
||||
|-------|-------|-------|
|
||||
| CPU time | 30s default | Configurable via `scriptTimeout` |
|
||||
| Storage | Filesystem | Performance varies by disk |
|
||||
| Memory | System dependent | No artificial limits |
|
||||
| Request.cf | Cached/mocked | Not live edge data |
|
||||
|
||||
See [patterns.md](./patterns.md) for testing examples.
|
||||
@@ -0,0 +1,181 @@
|
||||
# Testing Patterns
|
||||
|
||||
## Choosing a Testing Approach
|
||||
|
||||
| Approach | Use Case | Speed | Setup | Runtime |
|
||||
|----------|----------|-------|-------|---------|
|
||||
| **getPlatformProxy** | Unit tests, logic testing | Fast | Low | Miniflare |
|
||||
| **Miniflare API** | Integration tests, full control | Medium | Medium | Miniflare |
|
||||
| **vitest-pool-workers** | Vitest runner integration | Medium | Medium | workerd |
|
||||
|
||||
**Quick guide:**
|
||||
- Unit tests → getPlatformProxy
|
||||
- Integration tests → Miniflare API
|
||||
- Vitest workflows → vitest-pool-workers
|
||||
|
||||
## getPlatformProxy
|
||||
|
||||
Lightweight unit testing - provides bindings without full Worker runtime.
|
||||
|
||||
```js
|
||||
// vitest.config.js
|
||||
export default { test: { environment: "node" } };
|
||||
```
|
||||
|
||||
```js
|
||||
import { env } from "cloudflare:test";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("Business logic", () => {
|
||||
it("processes data with KV", async () => {
|
||||
await env.KV.put("test", "value");
|
||||
expect(await env.KV.get("test")).toBe("value");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Pros:** Fast, simple
|
||||
**Cons:** No full runtime, can't test fetch handler
|
||||
|
||||
## vitest-pool-workers
|
||||
|
||||
Full Workers runtime in Vitest. Reads `wrangler.toml`.
|
||||
|
||||
```bash
|
||||
npm i -D @cloudflare/vitest-pool-workers
|
||||
```
|
||||
|
||||
```js
|
||||
// vitest.config.js
|
||||
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
|
||||
|
||||
export default defineWorkersConfig({
|
||||
test: {
|
||||
poolOptions: { workers: { wrangler: { configPath: "./wrangler.toml" } } },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
import { env, SELF } from "cloudflare:test";
|
||||
import { it, expect } from "vitest";
|
||||
|
||||
it("handles fetch", async () => {
|
||||
const res = await SELF.fetch("http://example.com/");
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
**Pros:** Full runtime, uses wrangler.toml
|
||||
**Cons:** Requires Wrangler config
|
||||
|
||||
## Miniflare API (node:test)
|
||||
|
||||
```js
|
||||
import assert from "node:assert";
|
||||
import test, { after, before } from "node:test";
|
||||
import { Miniflare } from "miniflare";
|
||||
|
||||
let mf;
|
||||
before(() => {
|
||||
mf = new Miniflare({ scriptPath: "src/index.js", kvNamespaces: ["TEST_KV"] });
|
||||
});
|
||||
|
||||
test("fetch", async () => {
|
||||
const res = await mf.dispatchFetch("http://localhost/");
|
||||
assert.strictEqual(await res.text(), "Hello");
|
||||
});
|
||||
|
||||
after(() => mf.dispose());
|
||||
```
|
||||
|
||||
## Testing Durable Objects & Events
|
||||
|
||||
```js
|
||||
// Durable Objects
|
||||
const ns = await mf.getDurableObjectNamespace("COUNTER");
|
||||
const stub = ns.get(ns.idFromName("test-counter"));
|
||||
await stub.fetch("http://localhost/increment");
|
||||
|
||||
// Direct storage
|
||||
const storage = await mf.getDurableObjectStorage(ns.idFromName("test-counter"));
|
||||
const count = await storage.get("count");
|
||||
|
||||
// Queue
|
||||
const worker = await mf.getWorker();
|
||||
await worker.queue("my-queue", [
|
||||
{ id: "msg1", timestamp: new Date(), body: { userId: 123 }, attempts: 1 },
|
||||
]);
|
||||
|
||||
// Scheduled
|
||||
await worker.scheduled({ cron: "0 0 * * *" });
|
||||
```
|
||||
|
||||
## Test Isolation & Mocking
|
||||
|
||||
```js
|
||||
// Per-test isolation
|
||||
beforeEach(() => { mf = new Miniflare({ kvNamespaces: ["TEST"] }); });
|
||||
afterEach(() => mf.dispose());
|
||||
|
||||
// Mock external APIs
|
||||
new Miniflare({
|
||||
workers: [
|
||||
{ name: "main", serviceBindings: { API: "mock-api" }, script: `...` },
|
||||
{ name: "mock-api", script: `export default { async fetch() { return Response.json({mock: true}); } }` },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Type Safety
|
||||
|
||||
```ts
|
||||
import type { KVNamespace } from "@cloudflare/workers-types";
|
||||
|
||||
interface Env {
|
||||
KV: KVNamespace;
|
||||
API_KEY: string;
|
||||
}
|
||||
|
||||
const env = await mf.getBindings<Env>();
|
||||
await env.KV.put("key", "value"); // Typed!
|
||||
|
||||
export default {
|
||||
async fetch(req: Request, env: Env) {
|
||||
return new Response(await env.KV.get("key"));
|
||||
}
|
||||
} satisfies ExportedHandler<Env>;
|
||||
```
|
||||
|
||||
## WebSocket Testing
|
||||
|
||||
```js
|
||||
const res = await mf.dispatchFetch("http://localhost/ws", {
|
||||
headers: { Upgrade: "websocket" },
|
||||
});
|
||||
assert.strictEqual(res.status, 101);
|
||||
```
|
||||
|
||||
## Migration from unstable_dev
|
||||
|
||||
```js
|
||||
// Old (deprecated)
|
||||
import { unstable_dev } from "wrangler";
|
||||
const worker = await unstable_dev("src/index.ts");
|
||||
|
||||
// New
|
||||
import { Miniflare } from "miniflare";
|
||||
const mf = new Miniflare({ scriptPath: "src/index.ts" });
|
||||
```
|
||||
|
||||
## CI/CD Tips
|
||||
|
||||
```js
|
||||
// In-memory storage (faster)
|
||||
new Miniflare({ kvNamespaces: ["TEST"] }); // No persist = in-memory
|
||||
|
||||
// Use dispatchFetch (no port conflicts)
|
||||
await mf.dispatchFetch("http://localhost/");
|
||||
```
|
||||
|
||||
See [gotchas.md](./gotchas.md) for troubleshooting.
|
||||
Reference in New Issue
Block a user