Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 24 additions & 0 deletions .changeset/curly-beans-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"wrangler": minor
---

Allow Wrangler projects to build a Worker once and reuse it in `createTestHarness()`

Build the Worker once:

```sh
wrangler deploy --dry-run --outdir ./worker-output
Comment thread
edmundhung marked this conversation as resolved.
```

Then reuse the emitted Worker during test harness startup and reset:

```ts
const server = createTestHarness({
workers: [
{
configPath: "./wrangler.jsonc",
outDir: "./worker-output",
},
],
});
```
119 changes: 119 additions & 0 deletions packages/wrangler/e2e/createTestHarness.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { setTimeout } from "node:timers/promises";
import { pathToFileURL } from "node:url";
import { mockConsoleMethods } from "@cloudflare/workers-utils/test-helpers";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
Expand Down Expand Up @@ -95,6 +97,123 @@ describe("createTestHarness", () => {
);
});

it("runs existing dry-run output without rebuilding it", async ({
expect,
onTestFailed,
}) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "prebuilt-worker",
"main": "src/worker.ts",
"compatibility_date": "2026-05-20",
"vars": { "ENVIRONMENT": "top-level" },
"rules": [
{ "type": "Text", "globs": ["**/*.txt"] }
],
"build": { "command": "node build.mjs" },
"env": {
"test": {
"vars": { "ENVIRONMENT": "test" }
}
}
}
`,
"build.mjs": dedent`
import { writeFileSync } from "node:fs";

writeFileSync(
"src/generated.ts",
'export default "from custom build";'
);
`,
"src/message.txt": "from emitted text module",
"src/worker.ts": dedent`
import generated from "./generated";
import message from "./message.txt";

export default {
fetch(_request, env) {
return new Response(generated + ":" + message + ":" + env.ENVIRONMENT);
}
};
`,
});

await helper.run(
"wrangler deploy --dry-run --env test --outdir worker-output"
);

const outputPath = path.join(helper.tmpPath, "worker-output", "worker.js");
const originalOutput = await readFile(outputPath);
await helper.seed({
"build.mjs": 'throw new Error("The test harness ran the custom build");',
});

const server = createTestHarness({
root: helper.tmpPath,
workers: [
{
configPath: "./wrangler.jsonc",
env: "test",
outDir: pathToFileURL(path.join(helper.tmpPath, "worker-output")),
},
],
});
onTestFinished(server.close);
onTestFailed(server.debug);

await server.listen();
const response = await server.fetch("/");
await expect(response.text()).resolves.toBe(
"from custom build:from emitted text module:test"
);
expect(await readFile(outputPath)).toEqual(originalOutput);

await server.reset();
const resetResponse = await server.fetch("/");
await expect(resetResponse.text()).resolves.toBe(
"from custom build:from emitted text module:test"
);
expect(await readFile(outputPath)).toEqual(originalOutput);
});

it("fails when the expected dry-run entrypoint is missing", async ({
expect,
}) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "missing-prebuilt-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/index.ts": dedent`
export default {
fetch() {
return new Response("source fallback");
}
};
`,
});

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

await expect(server.listen()).rejects.toThrow(
'wrangler deploy --dry-run --config "./wrangler.jsonc" --outdir "./missing-output"'
);
});

it("can be configured after creation", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
Expand Down
75 changes: 71 additions & 4 deletions packages/wrangler/src/api/test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
WorkflowIntrospectorHandle,
} from "@cloudflare/workflows-shared/src/introspection";
import { CorePaths, Headers, Request } from "miniflare";
import { readConfig } from "../config";
import {
buildMigrationQuery,
getCreateMigrationsTableQuery,
Expand Down Expand Up @@ -402,6 +403,24 @@ type WorkerInput =
* Wrangler environment to load from the config file.
*/
env?: string;
/**
* Avoids rebuilding a Worker in a Wrangler project each time the test
* harness starts or resets. Build the Worker once with
* `wrangler deploy --dry-run --outdir`, then specify the same output
* directory here.
*
* When using a named Wrangler environment, `env` must match the environment
* used to build the output. Relative paths resolve from server `root`.
*
* @example
* ```sh
* wrangler deploy --dry-run --env test --outdir ./worker-output
* ```
* ```ts
* { configPath: "./wrangler.jsonc", env: "test", outDir: "./worker-output" }
* ```
*/
outDir?: string | URL;
Comment thread
edmundhung marked this conversation as resolved.
Outdated
/**
* Test-only vars that override vars from the Wrangler config.
*/
Expand Down Expand Up @@ -517,6 +536,57 @@ export function createTestHarness(options?: TestHarnessOptions): TestHarness {
return normalizedConfig;
}

function resolveWorkerConfig(
input: WorkerInput,
root: string
): string | Config {
if ("config" in input) {
return normalizeInlineWorkerConfig(input.config, root);
}

const configPath = resolvePath(root, input.configPath);
if (input.outDir === undefined) {
return configPath;
}

const config = readConfig({ config: configPath, env: input.env });
Comment thread
edmundhung marked this conversation as resolved.
Outdated
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
const outDir = resolvePath(root, input.outDir);
let main = config.main;
if (main !== undefined) {
const outputFileName = config.no_bundle
? path.basename(main)
: `${path.parse(main).name}.js`;
main = path.join(outDir, outputFileName);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
if (!fs.existsSync(main)) {
const envOption = input.env
? ` --env ${JSON.stringify(input.env)}`
: "";
const commandConfigPath =
typeof input.configPath === "string"
? input.configPath
: path.relative(root, configPath) || ".";
const commandOutDir =
typeof input.outDir === "string"
? input.outDir
: path.relative(root, outDir) || ".";
const commandEntrypoint = path.join(commandOutDir, outputFileName);
throw new UserError(
`Could not find the prebuilt Worker entrypoint at "${commandEntrypoint}". From the test harness root, run \`wrangler deploy --dry-run --config ${JSON.stringify(commandConfigPath)}${envOption} --outdir ${JSON.stringify(commandOutDir)}\` before starting the test harness.`,
{ telemetryMessage: "test harness prebuilt entrypoint missing" }
);
}
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

return {
...config,
main,
base_dir: outDir,
no_bundle: true,
find_additional_modules: true,
build: { ...config.build, command: undefined },
};
}

function resolveWorkerInputs(
serverOptions: TestHarnessOptions
): WranglerStartDevWorkerInput[] {
Expand Down Expand Up @@ -544,10 +614,7 @@ export function createTestHarness(options?: TestHarnessOptions): TestHarness {
}

return {
config:
"config" in input
? normalizeInlineWorkerConfig(input.config, root)
: resolvePath(root, input.configPath),
config: resolveWorkerConfig(input, root),
env: "env" in input ? input.env : undefined,
bindings,
dev: {
Expand Down
Loading