70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
960 lines
25 KiB
TypeScript
960 lines
25 KiB
TypeScript
import events from "node:events";
|
|
import path from "node:path";
|
|
import getPort from "get-port";
|
|
import dedent from "ts-dedent";
|
|
import undici from "undici";
|
|
import { assert, beforeEach, describe, it, onTestFinished, vi } from "vitest";
|
|
import WebSocket from "ws";
|
|
import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id";
|
|
import {
|
|
importWrangler,
|
|
WranglerE2ETestHelper,
|
|
} from "./helpers/e2e-wrangler-test";
|
|
import { waitFor, waitForLong } from "./helpers/wait-for";
|
|
import type { DevToolsEvent } from "../src/api";
|
|
|
|
const OPTIONS = [
|
|
{ remote: false },
|
|
...(CLOUDFLARE_ACCOUNT_ID ? [{ remote: true }] : []),
|
|
];
|
|
|
|
const { unstable_startWorker: startWorker } = await importWrangler();
|
|
|
|
function waitForMessageContaining<T>(ws: WebSocket, value: string): Promise<T> {
|
|
return new Promise((resolve) => {
|
|
ws.addEventListener("message", (event) => {
|
|
assert(typeof event.data === "string");
|
|
if (event.data.includes(value)) {
|
|
resolve(JSON.parse(event.data));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function collectMessagesContaining<T>(
|
|
ws: WebSocket,
|
|
value: string,
|
|
collection: T[] = []
|
|
) {
|
|
ws.addEventListener("message", (event) => {
|
|
assert(typeof event.data === "string");
|
|
if (event.data.includes(value)) {
|
|
collection.push(JSON.parse(event.data));
|
|
}
|
|
});
|
|
|
|
return collection;
|
|
}
|
|
|
|
describe("DevEnv", { sequential: true }, () => {
|
|
let helper: WranglerE2ETestHelper;
|
|
|
|
beforeEach(() => {
|
|
helper = new WranglerE2ETestHelper();
|
|
});
|
|
|
|
describe.each(OPTIONS)("(remote: $remote)", ({ remote }) => {
|
|
it("ProxyWorker buffers requests while runtime reloads", async ({
|
|
expect,
|
|
}) => {
|
|
const script = dedent`
|
|
export default {
|
|
fetch() {
|
|
return new Response("body:1");
|
|
}
|
|
}
|
|
`;
|
|
|
|
await helper.seed({
|
|
"src/index.ts": script,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
|
|
dev: {
|
|
remote,
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
let res = await worker.fetch("http://dummy");
|
|
await expect(res.text()).resolves.toBe("body:1");
|
|
|
|
await helper.seed({
|
|
"src/index.ts": script.replace("body:1", "body:2"),
|
|
});
|
|
|
|
await waitForLong(async () => {
|
|
res = await worker.fetch("http://dummy");
|
|
expect(await res.text()).toBe("body:2");
|
|
});
|
|
});
|
|
|
|
it.skipIf(remote)(
|
|
"InspectorProxyWorker discovery endpoints + devtools websocket connection",
|
|
async ({ expect }) => {
|
|
const script = dedent`
|
|
export default {
|
|
fetch() {
|
|
console.log('Inside mock user worker');
|
|
|
|
return new Response("body:1");
|
|
}
|
|
}
|
|
`;
|
|
|
|
await helper.seed({
|
|
"src/index.ts": script,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
|
|
dev: {
|
|
remote,
|
|
server: { port: 0 },
|
|
inspector: { port: 0 },
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const inspectorUrl = await worker.inspectorUrl;
|
|
assert(inspectorUrl, "missing inspectorUrl");
|
|
const res = await undici.fetch(`http://${inspectorUrl.host}/json`);
|
|
|
|
await expect(res.json()).resolves.toBeInstanceOf(Array);
|
|
|
|
assert(inspectorUrl, "missing inspectorUrl");
|
|
const ws = new WebSocket(inspectorUrl.href);
|
|
const openPromise = events.once(ws, "open");
|
|
|
|
const consoleApiMessages: DevToolsEvent<"Runtime.consoleAPICalled">[] =
|
|
collectMessagesContaining(ws, "Runtime.consoleAPICalled");
|
|
const executionContextCreatedPromise = waitForMessageContaining(
|
|
ws,
|
|
"Runtime.executionContextCreated"
|
|
);
|
|
|
|
await openPromise;
|
|
await worker.fetch("http://dummy");
|
|
|
|
await expect(executionContextCreatedPromise).resolves.toMatchObject({
|
|
method: "Runtime.executionContextCreated",
|
|
params: {
|
|
context: { id: expect.any(Number) },
|
|
},
|
|
});
|
|
await waitFor(() => {
|
|
expect(consoleApiMessages).toContainMatchingObject({
|
|
method: "Runtime.consoleAPICalled",
|
|
params: expect.objectContaining({
|
|
args: [{ type: "string", value: "Inside mock user worker" }],
|
|
}),
|
|
});
|
|
});
|
|
|
|
// Ensure execution contexts cleared on reload
|
|
const executionContextClearedPromise = waitForMessageContaining(
|
|
ws,
|
|
"Runtime.executionContextsCleared"
|
|
);
|
|
await helper.seed({
|
|
"src/index.ts": script.replace("body:1", "body:2"),
|
|
});
|
|
|
|
await executionContextClearedPromise;
|
|
}
|
|
);
|
|
|
|
it.skipIf(remote)(
|
|
"InspectorProxyWorker rejects unauthorised requests",
|
|
async ({ expect }) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch() {
|
|
return new Response("body:1");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
|
|
dev: {
|
|
remote,
|
|
server: { port: 0 },
|
|
inspector: { port: 0 },
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const inspectorUrl = await worker.inspectorUrl;
|
|
assert(inspectorUrl);
|
|
|
|
assert(inspectorUrl, "missing inspectorUrl");
|
|
let ws = new WebSocket(inspectorUrl.href, {
|
|
setHost: false,
|
|
headers: { Host: "example.com" },
|
|
});
|
|
|
|
let openPromise = events.once(ws, "open");
|
|
await expect(openPromise).rejects.toThrow("Unexpected server response");
|
|
|
|
// Check validates `Origin` header
|
|
assert(inspectorUrl, "missing inspectorUrl");
|
|
ws = new WebSocket(inspectorUrl.href, {
|
|
origin: "https://example.com",
|
|
});
|
|
openPromise = events.once(ws, "open");
|
|
await expect(openPromise).rejects.toThrow("Unexpected server response");
|
|
ws.close();
|
|
}
|
|
);
|
|
|
|
// Regression test for https://github.com/cloudflare/workers-sdk/issues/5297
|
|
// The runtime inspector can send messages larger than 1MB limit websocket message permitted by UserWorkers.
|
|
// In the real-world, this is encountered when debugging large source files (source maps)
|
|
// or inspecting a variable that serializes to a large string.
|
|
// Connecting devtools directly to the inspector would work fine, but we proxy the inspector messages
|
|
// through a worker (InspectorProxyWorker) which hits the limit (without the fix, compatibilityFlags:["increase_websocket_message_size"])
|
|
// By logging a large string we can verify that the inspector messages are being proxied successfully.
|
|
it.skipIf(remote)(
|
|
"InspectorProxyWorker can proxy messages > 1MB",
|
|
async ({ expect }) => {
|
|
vi.spyOn(console, "info").mockImplementation(() => {});
|
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
|
|
const LARGE_STRING = "This is a large string" + "z".repeat(2 ** 20);
|
|
|
|
const script = dedent`
|
|
export default {
|
|
fetch() {
|
|
console.log("${LARGE_STRING}");
|
|
|
|
return new Response("body:1");
|
|
}
|
|
}
|
|
`;
|
|
|
|
await helper.seed({
|
|
"src/index.ts": script,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
|
|
dev: {
|
|
remote,
|
|
server: { port: 0 },
|
|
inspector: { port: 0 },
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const inspectorUrl = await worker.inspectorUrl;
|
|
assert(inspectorUrl, "missing inspectorUrl");
|
|
const ws = new WebSocket(inspectorUrl.href);
|
|
|
|
const consoleApiMessages: DevToolsEvent<"Runtime.consoleAPICalled">[] =
|
|
collectMessagesContaining(ws, "Runtime.consoleAPICalled");
|
|
|
|
await worker.fetch("http://dummy");
|
|
|
|
await waitFor(() => {
|
|
expect(consoleApiMessages).toContainMatchingObject({
|
|
method: "Runtime.consoleAPICalled",
|
|
params: expect.objectContaining({
|
|
args: [{ type: "string", value: LARGE_STRING }],
|
|
}),
|
|
});
|
|
});
|
|
}
|
|
);
|
|
|
|
it("config.dev.{server,inspector} changes, restart the server instance", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch() {
|
|
return new Response("body:1");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
|
|
dev: {
|
|
remote,
|
|
server: { port: await getPort() },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
let res = await worker.fetch("http://dummy");
|
|
await expect(res.text()).resolves.toBe("body:1");
|
|
|
|
const oldPort = worker.config.dev?.server?.port;
|
|
let undiciRes = await undici.fetch(`http://127.0.0.1:${oldPort}`);
|
|
await expect(undiciRes.text()).resolves.toBe("body:1");
|
|
|
|
await worker.patchConfig({
|
|
dev: {
|
|
...worker.config.dev,
|
|
remote,
|
|
server: { port: await getPort() /* new port */ },
|
|
inspector: false,
|
|
},
|
|
});
|
|
const newPort = worker.config.dev?.server?.port;
|
|
|
|
res = await worker.fetch("http://dummy");
|
|
await expect(res.text()).resolves.toBe("body:1");
|
|
|
|
undiciRes = await undici.fetch(`http://127.0.0.1:${newPort}`);
|
|
await expect(undiciRes.text()).resolves.toBe("body:1");
|
|
|
|
await expect(undici.fetch(`http://127.0.0.1:${oldPort}`)).rejects.toThrow(
|
|
"fetch failed"
|
|
);
|
|
});
|
|
|
|
it("liveReload", async ({ expect }) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch() {
|
|
return new Response("body:1", {
|
|
headers: { 'Content-Type': 'text/html' }
|
|
});
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
|
|
dev: {
|
|
remote,
|
|
liveReload: true,
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const scriptRegex =
|
|
/<script defer type="application\/javascript">([\s\S]*)<\/script>/gm;
|
|
|
|
// test liveReload: true inserts live-reload <script> tag when the response Content-Type is html
|
|
let res = await worker.fetch("http://dummy");
|
|
let resText = await res.text();
|
|
expect(resText).toEqual(expect.stringContaining("body:1"));
|
|
expect(resText).toMatch(scriptRegex);
|
|
expect(resText.replace(scriptRegex, "").trim()).toEqual("body:1"); // test, without the <script> tag, the response is as authored
|
|
expect(resText.match(scriptRegex)?.[0]).toBe(dedent`
|
|
<script defer type="application/javascript">
|
|
(function() {
|
|
var ws;
|
|
function recover() {
|
|
ws = null;
|
|
setTimeout(initLiveReload, 100);
|
|
}
|
|
function initLiveReload() {
|
|
if (ws) return;
|
|
var origin = (location.protocol === "http:" ? "ws://" : "wss://") + location.host;
|
|
ws = new WebSocket(origin + "/cdn-cgi/live-reload", "WRANGLER_PROXYWORKER_LIVE_RELOAD_PROTOCOL");
|
|
ws.onclose = recover;
|
|
ws.onerror = recover;
|
|
ws.onmessage = location.reload.bind(location);
|
|
}
|
|
initLiveReload();
|
|
})();
|
|
</script>
|
|
`);
|
|
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch() {
|
|
return new Response("body:2");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
// In remote mode the edge needs time to propagate the new bundle,
|
|
// so use the longer polling timeout.
|
|
await waitForLong(async () => {
|
|
// test liveReload does nothing when the response Content-Type is not html
|
|
res = await worker.fetch("http://dummy");
|
|
resText = await res.text();
|
|
expect(resText).toBe("body:2");
|
|
});
|
|
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch() {
|
|
return new Response("body:3", {
|
|
headers: { 'Content-Type': 'text/html' }
|
|
});
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
await worker.patchConfig({
|
|
dev: {
|
|
...worker.config.dev,
|
|
liveReload: false,
|
|
},
|
|
});
|
|
|
|
await waitForLong(async () => {
|
|
// test liveReload: false does nothing even when the response Content-Type is html
|
|
res = await worker.fetch("http://dummy");
|
|
resText = await res.text();
|
|
expect(resText).toBe("body:3");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("DevEnv (local-only)", () => {
|
|
it("User worker exception", async ({ expect }) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch() {
|
|
throw new Error('Boom!');
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
dev: {
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
await expect(worker.fetch("http://dummy")).rejects.toThrow("Boom!");
|
|
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch() {
|
|
throw new Error('Boom 2!');
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
await waitFor(async () => {
|
|
await expect(worker.fetch("http://dummy")).rejects.toThrow("Boom 2!");
|
|
});
|
|
|
|
// test eyeball requests receive the pretty error page
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch() {
|
|
const e = new Error('Boom 3!');
|
|
|
|
// this is how errors are serialised after they are caught by wrangler/miniflare3 middlewares
|
|
const error = { name: e.name, message: e.message, stack: e.stack };
|
|
return Response.json(error, {
|
|
status: 500,
|
|
headers: { "MF-Experimental-Error-Stack": "true" },
|
|
});
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
await waitFor(async () => {
|
|
const undiciRes = await undici.fetch(await worker.url, {
|
|
headers: { Accept: "text/html" },
|
|
});
|
|
await expect(undiciRes.text()).resolves.toEqual(
|
|
expect.stringContaining(`<span>Boom 3!</span>`) // pretty error page html snippet
|
|
);
|
|
});
|
|
|
|
// test further changes that fix the code
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch() {
|
|
return new Response("body:3");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
await waitFor(async () => {
|
|
const res = await worker.fetch("http://dummy");
|
|
await expect(res.text()).resolves.toBe("body:3");
|
|
});
|
|
});
|
|
|
|
it("origin override takes effect in the UserWorker", async ({ expect }) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch(request) {
|
|
return new Response("URL: " + request.url);
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
|
|
dev: {
|
|
origin: {
|
|
hostname: "www.google.com",
|
|
},
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
let res = await worker.fetch("http://dummy/test/path/1");
|
|
await expect(res.text()).resolves.toBe(
|
|
`URL: http://www.google.com/test/path/1`
|
|
);
|
|
|
|
await worker.patchConfig({
|
|
dev: {
|
|
...worker.config.dev,
|
|
origin: {
|
|
secure: true,
|
|
hostname: "mybank.co.uk",
|
|
},
|
|
},
|
|
});
|
|
|
|
res = await worker.fetch("http://dummy/test/path/2");
|
|
await expect(res.text()).resolves.toBe(
|
|
"URL: https://mybank.co.uk/test/path/2"
|
|
);
|
|
});
|
|
|
|
it("inflight requests are retried during UserWorker reloads", async ({
|
|
expect,
|
|
}) => {
|
|
// to simulate inflight requests failing during UserWorker reloads,
|
|
// we will use a UserWorker with a longish `await setTimeout(...)`
|
|
// so that we can guarantee the race condition is hit when workerd is eventually terminated
|
|
// this does not apply to remote workers as they are not terminated during reloads
|
|
|
|
const script = dedent`
|
|
export default {
|
|
async fetch(request) {
|
|
const url = new URL(request.url);
|
|
|
|
if (url.pathname === '/long') {
|
|
await new Promise(r => setTimeout(r, 30_000));
|
|
}
|
|
|
|
return new Response("UserWorker:1");
|
|
}
|
|
}
|
|
`;
|
|
|
|
await helper.seed({
|
|
"src/index.ts": script,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
dev: {
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
let res = await worker.fetch("http://dummy/short");
|
|
await expect(res.text()).resolves.toBe("UserWorker:1");
|
|
|
|
const inflightDuringReloads = worker.fetch("http://dummy/long"); // NOTE: no await
|
|
|
|
// this will cause workerd for UserWorker:1 to terminate (eventually, but soon)
|
|
await helper.seed({
|
|
"src/index.ts": script.replace("UserWorker:1", "UserWorker:2"),
|
|
});
|
|
|
|
await waitFor(async () => {
|
|
res = await worker.fetch("http://dummy/short");
|
|
await expect(res.text()).resolves.toBe("UserWorker:2");
|
|
});
|
|
|
|
// this will cause workerd for UserWorker:2 to terminate (eventually, but soon)
|
|
await helper.seed({
|
|
"src/index.ts": script
|
|
.replace("UserWorker:1", "UserWorker:3") // change response so it can be identified
|
|
.replace("30_000", "0"), // remove the long wait as we won't reload this UserWorker
|
|
});
|
|
|
|
res = await inflightDuringReloads;
|
|
await expect(res.text()).resolves.toBe("UserWorker:3");
|
|
});
|
|
|
|
it("vars from .env (next to config file) override vars from Wrangler config file", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch(request, env) {
|
|
return Response.json(env);
|
|
}
|
|
}
|
|
`,
|
|
"wrangler.jsonc": JSON.stringify({
|
|
vars: {
|
|
WRANGLER_ENV_VAR_0: "default-0",
|
|
WRANGLER_ENV_VAR_1: "default-1",
|
|
WRANGLER_ENV_VAR_2: "default-2",
|
|
WRANGLER_ENV_VAR_3: "default-3",
|
|
},
|
|
}),
|
|
".env": dedent`
|
|
WRANGLER_ENV_VAR_1=env-1
|
|
WRANGLER_ENV_VAR_2=env-2
|
|
`,
|
|
".env.local": dedent`
|
|
WRANGLER_ENV_VAR_2=local-2
|
|
WRANGLER_ENV_VAR_3=local-3
|
|
`,
|
|
".env.staging": dedent`
|
|
WRANGLER_ENV_VAR_3=staging-3
|
|
WRANGLER_ENV_VAR_4=staging-4
|
|
`,
|
|
".env.staging.local": dedent`
|
|
WRANGLER_ENV_VAR_4=staging-local-4
|
|
WRANGLER_ENV_VAR_5=staging-local-5
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
config: path.resolve(helper.tmpPath, "wrangler.jsonc"),
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
dev: {
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const res = await worker.fetch("http://dummy/test/path/1");
|
|
expect(await res.json()).toMatchInlineSnapshot(`
|
|
{
|
|
"WRANGLER_ENV_VAR_0": "default-0",
|
|
"WRANGLER_ENV_VAR_1": "env-1",
|
|
"WRANGLER_ENV_VAR_2": "local-2",
|
|
"WRANGLER_ENV_VAR_3": "local-3",
|
|
}
|
|
`);
|
|
});
|
|
|
|
it("vars are not loaded from .env if there is a .dev.vars file (next to config file)", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch(request, env) {
|
|
return Response.json(env);
|
|
}
|
|
}
|
|
`,
|
|
"wrangler.jsonc": JSON.stringify({
|
|
vars: {
|
|
WRANGLER_ENV_VAR_0: "default-0",
|
|
WRANGLER_ENV_VAR_1: "default-1",
|
|
WRANGLER_ENV_VAR_2: "default-2",
|
|
WRANGLER_ENV_VAR_3: "default-3",
|
|
},
|
|
}),
|
|
".env": dedent`
|
|
WRANGLER_ENV_VAR_1=env-1
|
|
WRANGLER_ENV_VAR_2=env-2
|
|
`,
|
|
".dev.vars": dedent`
|
|
WRANGLER_ENV_VAR_2=dev-vars-2
|
|
WRANGLER_ENV_VAR_3=dev-vars-3
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
config: path.resolve(helper.tmpPath, "wrangler.jsonc"),
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
dev: {
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const res = await worker.fetch("http://dummy/test/path/1");
|
|
expect(await res.json()).toMatchInlineSnapshot(`
|
|
{
|
|
"WRANGLER_ENV_VAR_0": "default-0",
|
|
"WRANGLER_ENV_VAR_1": "default-1",
|
|
"WRANGLER_ENV_VAR_2": "dev-vars-2",
|
|
"WRANGLER_ENV_VAR_3": "dev-vars-3",
|
|
}
|
|
`);
|
|
});
|
|
|
|
it("vars from inline config override vars from both .env and config file", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch(request, env) {
|
|
return Response.json(env);
|
|
}
|
|
}
|
|
`,
|
|
"wrangler.jsonc": JSON.stringify({
|
|
vars: {
|
|
WRANGLER_ENV_VAR_0: "default-0",
|
|
WRANGLER_ENV_VAR_1: "default-1",
|
|
WRANGLER_ENV_VAR_2: "default-2",
|
|
WRANGLER_ENV_VAR_3: "default-3",
|
|
},
|
|
}),
|
|
".env": dedent`
|
|
WRANGLER_ENV_VAR_1=env-1
|
|
WRANGLER_ENV_VAR_2=env-2
|
|
`,
|
|
".env.local": dedent`
|
|
WRANGLER_ENV_VAR_2=local-2
|
|
WRANGLER_ENV_VAR_3=local-3
|
|
`,
|
|
".env.staging": dedent`
|
|
WRANGLER_ENV_VAR_3=staging-3
|
|
WRANGLER_ENV_VAR_4=staging-4
|
|
`,
|
|
".env.staging.local": dedent`
|
|
WRANGLER_ENV_VAR_4=staging-local-4
|
|
WRANGLER_ENV_VAR_5=staging-local-5
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
config: path.resolve(helper.tmpPath, "wrangler.jsonc"),
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
bindings: {
|
|
WRANGLER_ENV_VAR_3: { type: "plain_text", value: "inline-3" },
|
|
WRANGLER_ENV_VAR_4: { type: "plain_text", value: "inline-4" },
|
|
},
|
|
dev: {
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const res = await worker.fetch("http://dummy/test/path/1");
|
|
expect(await res.json()).toMatchInlineSnapshot(`
|
|
{
|
|
"WRANGLER_ENV_VAR_0": "default-0",
|
|
"WRANGLER_ENV_VAR_1": "env-1",
|
|
"WRANGLER_ENV_VAR_2": "local-2",
|
|
"WRANGLER_ENV_VAR_3": "inline-3",
|
|
"WRANGLER_ENV_VAR_4": "inline-4",
|
|
}
|
|
`);
|
|
});
|
|
|
|
it("vars from .env pointed at by `envFile` override vars from Wrangler config file and .env files local to the config file", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch(request, env) {
|
|
return Response.json(env);
|
|
}
|
|
}
|
|
`,
|
|
"wrangler.jsonc": JSON.stringify({
|
|
vars: {
|
|
WRANGLER_ENV_VAR_0: "default-0",
|
|
WRANGLER_ENV_VAR_1: "default-1",
|
|
WRANGLER_ENV_VAR_2: "default-2",
|
|
WRANGLER_ENV_VAR_3: "default-3",
|
|
},
|
|
}),
|
|
".env": dedent`
|
|
WRANGLER_ENV_VAR_1=env-1
|
|
WRANGLER_ENV_VAR_2=env-2
|
|
`,
|
|
".env.local": dedent`
|
|
WRANGLER_ENV_VAR_2=local-2
|
|
WRANGLER_ENV_VAR_3=local-3
|
|
`,
|
|
"other/.env": dedent`
|
|
WRANGLER_ENV_VAR_3=other-3
|
|
WRANGLER_ENV_VAR_4=other-4
|
|
`,
|
|
"other/.env.local": dedent`
|
|
WRANGLER_ENV_VAR_4=other-local-4
|
|
WRANGLER_ENV_VAR_5=other-local-5
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
config: path.resolve(helper.tmpPath, "wrangler.jsonc"),
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
envFiles: ["other/.env", "other/.env.local"],
|
|
dev: {
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const res = await worker.fetch("http://dummy/test/path/1");
|
|
expect(await res.json()).toMatchInlineSnapshot(`
|
|
{
|
|
"WRANGLER_ENV_VAR_0": "default-0",
|
|
"WRANGLER_ENV_VAR_1": "default-1",
|
|
"WRANGLER_ENV_VAR_2": "default-2",
|
|
"WRANGLER_ENV_VAR_3": "other-3",
|
|
"WRANGLER_ENV_VAR_4": "other-local-4",
|
|
"WRANGLER_ENV_VAR_5": "other-local-5",
|
|
}
|
|
`);
|
|
});
|
|
|
|
// Regression test for https://github.com/cloudflare/workers-sdk/issues/11038
|
|
// When envFiles is explicitly provided, .dev.vars should be completely ignored
|
|
it(".dev.vars is ignored when envFiles is explicitly provided (PR #11195)", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch(request, env) {
|
|
return Response.json(env);
|
|
}
|
|
}
|
|
`,
|
|
"wrangler.jsonc": JSON.stringify({
|
|
vars: {
|
|
WRANGLER_ENV_VAR_0: "default-0",
|
|
},
|
|
}),
|
|
".dev.vars": dedent`
|
|
WRANGLER_ENV_VAR_1=dev-vars-1
|
|
WRANGLER_ENV_VAR_2=dev-vars-2
|
|
`,
|
|
"custom/.env": dedent`
|
|
WRANGLER_ENV_VAR_2=custom-2
|
|
WRANGLER_ENV_VAR_3=custom-3
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
config: path.resolve(helper.tmpPath, "wrangler.jsonc"),
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
envFiles: ["custom/.env"],
|
|
dev: {
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const res = await worker.fetch("http://dummy/test/path/1");
|
|
expect(await res.json()).toMatchInlineSnapshot(`
|
|
{
|
|
"WRANGLER_ENV_VAR_0": "default-0",
|
|
"WRANGLER_ENV_VAR_2": "custom-2",
|
|
"WRANGLER_ENV_VAR_3": "custom-3",
|
|
}
|
|
`);
|
|
});
|
|
|
|
// Regression test for https://github.com/cloudflare/workers-sdk/issues/11264
|
|
// When envFiles is an empty array, .dev.vars should still be loaded
|
|
it(".dev.vars is loaded when envFiles is empty array (PR #11278)", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch(request, env) {
|
|
return Response.json(env);
|
|
}
|
|
}
|
|
`,
|
|
"wrangler.jsonc": JSON.stringify({
|
|
vars: {
|
|
WRANGLER_ENV_VAR_0: "default-0",
|
|
},
|
|
}),
|
|
".dev.vars": dedent`
|
|
WRANGLER_ENV_VAR_1=dev-vars-1
|
|
WRANGLER_ENV_VAR_2=dev-vars-2
|
|
`,
|
|
});
|
|
|
|
const worker = await startWorker({
|
|
config: path.resolve(helper.tmpPath, "wrangler.jsonc"),
|
|
name: "test-worker",
|
|
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
|
|
envFiles: [],
|
|
dev: {
|
|
server: { port: 0 },
|
|
inspector: false,
|
|
},
|
|
});
|
|
onTestFinished(worker?.dispose);
|
|
|
|
const res = await worker.fetch("http://dummy/test/path/1");
|
|
expect(await res.json()).toMatchInlineSnapshot(`
|
|
{
|
|
"WRANGLER_ENV_VAR_0": "default-0",
|
|
"WRANGLER_ENV_VAR_1": "dev-vars-1",
|
|
"WRANGLER_ENV_VAR_2": "dev-vars-2",
|
|
}
|
|
`);
|
|
});
|
|
});
|
|
});
|