update skills

This commit is contained in:
2026-03-17 16:53:22 -07:00
parent 0b0783ef8e
commit f9a530667e
389 changed files with 54512 additions and 1 deletions

View 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

View 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.

View File

@@ -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"],
});
```

View 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.

View File

@@ -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.