Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/gentle-ravens-apply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"wrangler": minor
---

Add D1 migration setup to `createTestHarness()` Worker handles

Tests using `createTestHarness()` can now apply local D1 migrations before running requests:

```ts
const worker = server.getWorker();

beforeEach(async () => {
await worker.applyD1Migrations("DATABASE");
});
```
7 changes: 7 additions & 0 deletions .changeset/tidy-pandas-migrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@cloudflare/vite-plugin": patch
---

Preserve D1 migration paths in generated Worker configs

When a Worker config with a D1 binding is built by the Vite plugin, the generated `wrangler.json` now points `migrations_dir` back to the source migration directory. This lets tools that read the generated config, such as `createTestHarness()`, find the same D1 migrations as the source Worker config.
27 changes: 14 additions & 13 deletions fixtures/create-test-harness-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ For the testing patterns demonstrated here, refer to the [`createTestHarness()`

The app has two Workers:

| Worker | Route | Role |
| ------------ | ---------------------- | ----------------------------------------------------------------------------- |
| `web-worker` | `example.com/*` | Handles user-facing routes and calls the API Worker over a service binding. |
| `api-worker` | `api.example.com/v1/*` | Fetches upstream user data, caches it in KV, and generates scheduled reports. |
| Worker | Route | Role |
| ------------ | ---------------------- | -------------------------------------------------------------------------------- |
| `web-worker` | `example.com/*` | Handles user-facing routes and calls the API Worker over a service binding. |
| `api-worker` | `api.example.com/v1/*` | Fetches upstream user data, caches it in KV, and stores scheduled reports in D1. |

## What this fixture covers

- Setup [Vitest](https://vitest.dev/) against Workers developed with Wrangler.
- Setup [Playwright](https://playwright.dev/) against Workers built by the Cloudflare Vite plugin.
- Route dispatch across multiple Workers.
- Direct Worker calls with `server.getWorker(name)`.
- D1 migration setup with `worker.applyD1Migrations()`.
- Scheduled handler dispatch.
- Outbound request mocking with MSW.
- Local storage reset between tests.
Expand All @@ -42,15 +43,15 @@ pnpm --filter @fixture/create-test-harness-example test:playwright

## Files

| File | Purpose |
| ---------------------------------------------------------- | ------------------------------------------- |
| [`tests/vitest.test.ts`](tests/vitest.test.ts) | Example for testing with Vitest. |
| [`tests/playwright.test.ts`](tests/playwright.test.ts) | Example for testing with Playwright. |
| [`vite.config.ts`](vite.config.ts) | Builds Vite-generated Worker configs. |
| [`workers/web/index.ts`](workers/web/index.ts) | User-facing Worker. |
| [`workers/web/wrangler.jsonc`](workers/web/wrangler.jsonc) | Web Worker config. |
| [`workers/api/index.ts`](workers/api/index.ts) | API Worker with KV and scheduled job logic. |
| [`workers/api/wrangler.jsonc`](workers/api/wrangler.jsonc) | API Worker config. |
| File | Purpose |
| ---------------------------------------------------------- | ------------------------------------------------ |
| [`tests/vitest.test.ts`](tests/vitest.test.ts) | Example for testing with Vitest. |
| [`tests/playwright.test.ts`](tests/playwright.test.ts) | Example for testing with Playwright. |
| [`vite.config.ts`](vite.config.ts) | Builds Vite-generated Worker configs. |
| [`workers/web/index.ts`](workers/web/index.ts) | User-facing Worker. |
| [`workers/web/wrangler.jsonc`](workers/web/wrangler.jsonc) | Web Worker config. |
| [`workers/api/index.ts`](workers/api/index.ts) | API Worker with KV, D1, and scheduled job logic. |
| [`workers/api/wrangler.jsonc`](workers/api/wrangler.jsonc) | API Worker config. |

## Related docs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const test = base.extend<TestFixtures, WorkerFixtures>({

reset: [
async ({ network, server }, use, testInfo) => {
await server.getWorker("api-worker").applyD1Migrations("DATABASE");

await use();

if (testInfo.status !== testInfo.expectedStatus) {
Expand Down
13 changes: 12 additions & 1 deletion fixtures/create-test-harness-example/tests/vitest.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, test } from "vitest";
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
test,
} from "vitest";
import { createTestHarness } from "wrangler";

// Point each worker to the Wrangler config you want to test.
Expand All @@ -25,6 +32,10 @@ describe("createTestHarness: Vitest setup", () => {
await server.listen();
});

beforeEach(async () => {
await apiWorker.applyD1Migrations("DATABASE");
});

afterAll(async () => {
network.close();
await server.close();
Expand Down
24 changes: 18 additions & 6 deletions fixtures/create-test-harness-example/workers/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { WorkerEntrypoint } from "cloudflare:workers";

type Env = {
STORE: KVNamespace;
DATABASE: D1Database;
};

type User = {
Expand Down Expand Up @@ -47,9 +48,11 @@ export default class ApiWorker extends WorkerEntrypoint<Env> {
const list = await this.env.STORE.list({ prefix: "user/" });
const userIds = list.keys.map((key) => key.name.slice("user/".length));

await this.env.STORE.put(`daily-report/${date}`, JSON.stringify(userIds), {
expirationTtl: 60 * 60,
});
await this.env.DATABASE.prepare(
"INSERT OR REPLACE INTO daily_reports (date, user_ids) VALUES (?, ?)"
)
.bind(date, JSON.stringify(userIds))
.run();
console.info(`Generated daily report for ${date}`);

for (const key of list.keys) {
Expand Down Expand Up @@ -78,8 +81,17 @@ export default class ApiWorker extends WorkerEntrypoint<Env> {
}

async getDailyReport(date: string) {
return await this.env.STORE.get<string[]>(`daily-report/${date}`, {
type: "json",
});
const report = await this.env.DATABASE.prepare(
"SELECT user_ids FROM daily_reports WHERE date = ?"
)
.bind(date)
.first<{ user_ids: string }>();

if (report === null) {
return null;
}

const userIds: string[] = JSON.parse(report.user_ids);
return userIds;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE daily_reports (
date TEXT PRIMARY KEY,
user_ids TEXT NOT NULL
);
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@
"compatibility_date": "2026-06-01",
"routes": ["api.example.com/v1/*"],
"kv_namespaces": [{ "binding": "STORE", "id": "shared-store" }],
"d1_databases": [
{
"binding": "DATABASE",
"database_name": "report-database",
"database_id": "fake-database-id",
},
],
}
60 changes: 60 additions & 0 deletions packages/vite-plugin-cloudflare/src/plugins/output-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,41 @@ export const outputConfigPlugin = createPlugin("output-config", (ctx) => {
this.environment.name ===
ctx.resolvedPluginConfig.entryWorkerEnvironmentName;

const sourceConfigDirectory = path.dirname(
inputWorkerConfig.configPath ?? ctx.resolvedViteConfig.root
);
const outputDirectory = path.resolve(
ctx.resolvedViteConfig.root,
this.environment.config.build.outDir
);

outputConfig = {
...inputWorkerConfig,
d1_databases: inputWorkerConfig.d1_databases.map((database) => {
const sourceMigrationsDir = database.migrations_dir ?? "migrations";
const sourceMigrationsPath = path.resolve(
sourceConfigDirectory,
sourceMigrationsDir
);

if (!fs.existsSync(sourceMigrationsPath)) {
return database;
}

const outputMigrationsDir = vite.normalizePath(
path.relative(outputDirectory, sourceMigrationsPath) || "."
);

return {
...database,
migrations_dir: outputMigrationsDir,
migrations_pattern: rewriteMigrationsPattern(
database.migrations_pattern,
sourceMigrationsDir,
outputMigrationsDir
),
};
}),
main: entryChunk.fileName,
no_bundle: true,
rules: [{ type: "ESModule", globs: ["**/*.js", "**/*.mjs"] }],
Expand Down Expand Up @@ -153,6 +186,33 @@ export const outputConfigPlugin = createPlugin("output-config", (ctx) => {
};
});

function rewriteMigrationsPattern(
migrationsPattern: string | undefined,
fromMigrationsDir: string,
toMigrationsDir: string
): string | undefined {
if (migrationsPattern === undefined) {
return undefined;
}

const normalizedDir = path.posix.normalize(
vite.normalizePath(fromMigrationsDir)
);
const normalizedPattern = path.posix.normalize(
vite.normalizePath(migrationsPattern)
);
const suffix =
normalizedDir === "."
? normalizedPattern
: path.posix.relative(normalizedDir, normalizedPattern);

return vite.normalizePath(
path.posix.normalize(
toMigrationsDir === "." ? suffix : `${toMigrationsDir}/${suffix}`
)
);
}

function readAssetsIgnoreFile(assetsIgnorePath: string): string {
const content = fs.existsSync(assetsIgnorePath)
? fs.readFileSync(assetsIgnorePath, "utf-8")
Expand Down
80 changes: 80 additions & 0 deletions packages/wrangler/e2e/createTestHarness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,86 @@ describe("createTestHarness", () => {
await expect(adminResponse.text()).resolves.toBe("2");
});

it("applies D1 migrations to a worker binding", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "d1-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"d1_databases": [
{
"binding": "DATABASE",
"database_name": "test-database",
"database_id": "00000000-0000-0000-0000-000000000001",
"migrations_dir": "migrations",
"migrations_pattern": "migrations/*/migration.sql",
"migrations_table": "custom_migrations"
}
]
}
`,
"src/index.ts": dedent`
export default {
async fetch(request, env) {
const key = new URL(request.url).pathname.slice(1);
const row = await env.DATABASE.prepare("SELECT value FROM settings WHERE key = ?")
.bind(key)
.first();

if (row === null) {
return new Response("missing", { status: 404 });
}

return new Response(row.value);
}
};
`,
"migrations/0001_settings/migration.sql": dedent`
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
`,
"migrations/0002_seed/migration.sql": dedent`
INSERT INTO settings (key, value) VALUES ('greeting', 'Hello D1');
`,
});

const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);

await server.listen();

const worker = server.getWorker<{ DATABASE: D1Database }>();
await worker.applyD1Migrations("DATABASE");

const response = await worker.fetch("/greeting");
await expect(response.text()).resolves.toBe("Hello D1");

await worker.applyD1Migrations("DATABASE");
const secondResponse = await worker.fetch("/greeting");
await expect(secondResponse.text()).resolves.toBe("Hello D1");

server.debug();
const debugOutput = normalizeDebugOutput(logs.getAndClearOut());
expect(debugOutput).toContain(
"[server] [d1-worker] d1 migrations - DATABASE - applied 0001_settings/migration.sql"
);
expect(debugOutput).toContain(
"[server] [d1-worker] d1 migrations - DATABASE - applied 0002_seed/migration.sql"
);
expect(debugOutput).toContain(
"[server] [d1-worker] d1 migrations - DATABASE - completed (2 applied)"
);
expect(debugOutput).toContain(
"[server] [d1-worker] d1 migrations - DATABASE - completed (no migrations to apply)"
);
});

it("supports service bindings between workers", async ({ expect }) => {
await helper.seed({
"wrangler.primary.jsonc": dedent`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ function getDevCompatibilityDate(

export class ConfigController extends Controller {
latestInput?: StartDevWorkerInput;
latestWranglerConfig?: Config;
latestConfig?: StartDevWorkerOptions;
#printCurrentBindings?: (registry: WorkerRegistry | null) => void;

Expand Down Expand Up @@ -718,6 +719,7 @@ export class ConfigController extends Controller {
if (signal.aborted) {
return;
}
this.latestWranglerConfig = fileConfig;
this.latestConfig = resolvedConfig;
this.#printCurrentBindings = printCurrentBindings;
this.emitConfigUpdateEvent(resolvedConfig);
Expand Down
Loading
Loading