diff --git a/packages/wrangler/e2e/dev-env.test.ts b/packages/wrangler/e2e/dev-env.test.ts index 3573a0987d30..a5bbcd893553 100644 --- a/packages/wrangler/e2e/dev-env.test.ts +++ b/packages/wrangler/e2e/dev-env.test.ts @@ -16,93 +16,75 @@ describe("switching runtimes", () => { account_id = "${CLOUDFLARE_ACCOUNT_ID}" compatibility_date = "2023-01-01" `, + "index.ts": dedent/*javascript*/ ` + export default { + async fetch(request, env) { + return new Response( + env.ORDER + ": I am " + (env.REMOTE ? "remote" : "local") + ); + }, + }; + `, "index.mjs": dedent/*javascript*/ ` - const firstRemote = process.argv[2] === "remote" - import { unstable_DevEnv as DevEnv } from "${WRANGLER_IMPORT}"; - - const devEnv = new DevEnv() - - let config = { - name: "worker", - script: "", - compatibilityFlags: ["nodejs_compat"], - compatibilityDate: "2023-10-01", - dev: { - remote: firstRemote, - auth: { - accountId: process.env.CLOUDFLARE_ACCOUNT_ID, - apiToken: process.env.CLOUDFLARE_API_TOKEN - } - } - }; - let bundle = { - type: "esm", - modules: [], - id: 0, - path: "/virtual/esm/index.mjs", - entrypointSource: "export default { fetch() { return new Response('Hello World " + (firstRemote ? 'local' : 'remote') + " runtime') } }", - entry: { - file: "esm/index.mjs", - directory: "/virtual/", - format: "modules", - moduleRoot: "/virtual", - name: undefined, - }, - dependencies: {}, - sourceMapPath: undefined, - sourceMapMetadata: undefined, - }; - - devEnv.proxy.onConfigUpdate({ - type: "configUpdate", - config, - }); - - devEnv.runtimes.forEach((runtime) => - runtime.onBundleStart({ - type: "bundleStart", - config, - }) - ); + import { setTimeout } from "timers/promises"; + import { unstable_DevEnv as DevEnv } from "${WRANGLER_IMPORT}"; - devEnv.runtimes.forEach((runtime) => - runtime.onBundleComplete({ - type: "bundleComplete", - config, - bundle, - }) - ); + const firstRemote = process.argv[2] === "remote"; - // Immediately switch runtime - config = { ...config, dev: { ...config.dev, remote: !firstRemote } }; - bundle = {...bundle, entrypointSource: "export default { fetch() { return new Response('Hello World " + (firstRemote ? 'local' : 'remote') + " runtime') } }"} + const devEnv = new DevEnv(); - devEnv.proxy.onConfigUpdate({ - type: "configUpdate", - config, - }); + let config = { + name: "worker", + entrypoint: "index.ts", + compatibilityFlags: ["nodejs_compat"], + compatibilityDate: "2023-10-01", + bindings: { + REMOTE: { + type: "json", + value: firstRemote, + }, + ORDER: { + type: "plain_text", + value: "1", + }, + }, + dev: { + remote: firstRemote, + auth: { + accountId: process.env.CLOUDFLARE_ACCOUNT_ID, + apiToken: process.env.CLOUDFLARE_API_TOKEN, + }, + }, + }; + void devEnv.config.set(config); - devEnv.runtimes.forEach((runtime) => - runtime.onBundleStart({ - type: "bundleStart", - config, - }) - ); + const { url } = await devEnv.proxy.ready.promise; + console.log(await fetch(url).then((r) => r.text())); - devEnv.runtimes.forEach((runtime) => - runtime.onBundleComplete({ - type: "bundleComplete", - config, - bundle, - }) - ); + void devEnv.config.patch({ + bindings: { + REMOTE: { + type: "json", + value: !firstRemote, + }, + ORDER: { + type: "plain_text", + value: "2", + }, + }, + dev: { + ...config.dev, + remote: !firstRemote, + }, + }); - const { proxyWorker } = await devEnv.proxy.ready.promise; - await devEnv.proxy.runtimeMessageMutex.drained(); + // Give the config some time to propagate + await setTimeout(500); - console.log(await proxyWorker.dispatchFetch("http://example.com").then(r => r.text())) + console.log(await fetch(url).then((r) => r.text())); - process.exit(0); + await devEnv.teardown(); + process.exit(0); `, "package.json": dedent` { @@ -113,22 +95,24 @@ describe("switching runtimes", () => { `, }); }); - it("can switch from local to remote, with first fetch returning remote", async () => { + it("can switch from local to remote", async () => { const stdout = execSync(`node index.mjs local`, { timeout: 20_000, encoding: "utf-8", cwd: root, stdio: "pipe", }); - expect(stdout).toContain("Hello World remote runtime"); + expect(stdout).toContain("1: I am local"); + expect(stdout).toContain("2: I am remote"); }); - it("can switch from remote to local, with first fetch returning local", async () => { + it("can switch from remote to local", async () => { const stdout = execSync(`node index.mjs remote`, { timeout: 20_000, encoding: "utf-8", cwd: root, stdio: "pipe", }); - expect(stdout).toContain("Hello World local runtime"); + expect(stdout).toContain("1: I am remote"); + expect(stdout).toContain("2: I am local"); }); }); diff --git a/packages/wrangler/e2e/dev-with-resources.test.ts b/packages/wrangler/e2e/dev-with-resources.test.ts index e4410a138b7d..f4c9047417a5 100644 --- a/packages/wrangler/e2e/dev-with-resources.test.ts +++ b/packages/wrangler/e2e/dev-with-resources.test.ts @@ -4,7 +4,7 @@ import getPort from "get-port"; import dedent from "ts-dedent"; import { Agent, fetch } from "undici"; import { beforeEach, describe, expect, it } from "vitest"; -import { WebSocket } from "ws"; +import WebSocket from "ws"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; import { generateResourceName } from "./helpers/generate-resource-name"; @@ -305,7 +305,7 @@ describe.each(RUNTIMES)("Bindings: $flags", ({ runtime, flags }) => { it("exposes KV namespace bindings", async () => { const ns = await helper.kv(isLocal); - await helper.runLongLived( + await helper.run( `wrangler kv key put ${resourceFlags} --namespace-id=${ns} existing-key existing-value` ); diff --git a/packages/wrangler/e2e/startWorker.test.ts b/packages/wrangler/e2e/startWorker.test.ts index 4ce8147a3ac8..e188444a65a0 100644 --- a/packages/wrangler/e2e/startWorker.test.ts +++ b/packages/wrangler/e2e/startWorker.test.ts @@ -5,8 +5,8 @@ import { setTimeout } from "timers/promises"; import getPort from "get-port"; import dedent from "ts-dedent"; import undici from "undici"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { WebSocket } from "ws"; +import { beforeEach, describe, expect, it } from "vitest"; +import WebSocket from "ws"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; const OPTIONS = [ @@ -53,9 +53,9 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { "src/index.ts": script, }); - const worker = devEnv.startWorker({ - entrypoint: { path: path.resolve(helper.tmpPath, "src/index.ts") }, - directory: helper.tmpPath, + const worker = await devEnv.startWorker({ + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + dev: { remote }, }); @@ -88,10 +88,10 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { "src/index.ts": script, }); - const worker = devEnv.startWorker({ + const worker = await devEnv.startWorker({ name: "test-worker", - entrypoint: { path: path.resolve(helper.tmpPath, "src/index.ts") }, - directory: helper.tmpPath, + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + dev: { remote }, }); @@ -158,10 +158,10 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { `, }); - const worker = devEnv.startWorker({ + const worker = await devEnv.startWorker({ name: "test-worker", - entrypoint: { path: path.resolve(helper.tmpPath, "src/index.ts") }, - directory: helper.tmpPath, + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + dev: { remote }, }); @@ -185,10 +185,6 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { ws.close(); }); it("User worker exception", async (t) => { - const consoleErrorSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const devEnv = new DevEnv(); t.onTestFinished(() => devEnv.teardown()); @@ -202,20 +198,14 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { `, }); - const worker = devEnv.startWorker({ + const worker = await devEnv.startWorker({ name: "test-worker", - entrypoint: { path: path.resolve(helper.tmpPath, "src/index.ts") }, - directory: helper.tmpPath, + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + dev: { remote }, }); - let res = await worker.fetch("http://dummy"); - await expect(res.text()).resolves.toMatch(/^Error: Boom!/); - - await setTimeout(100); // allow some time for the error to be logged (TODO: replace with retry/waitUntil helper) - expect(consoleErrorSpy).toBeCalledWith( - expect.stringContaining("Error: Boom!") - ); + await expect(worker.fetch("http://dummy")).rejects.toThrowError("Boom!"); await helper.seed({ "src/index.ts": dedent` @@ -228,13 +218,7 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { }); await setTimeout(300); - res = await worker.fetch("http://dummy"); - await expect(res.text()).resolves.toMatch(/^Error: Boom 2!/); - - await setTimeout(100); // allow some time for the error to be logged (TODO: replace with retry/waitUntil helper) - expect(consoleErrorSpy).toBeCalledWith( - expect.stringContaining("Error: Boom 2!") - ); + await expect(worker.fetch("http://dummy")).rejects.toThrowError("Boom 2!"); // test eyeball requests receive the pretty error page await helper.seed({ @@ -274,15 +258,11 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { }); await setTimeout(300); - res = await worker.fetch("http://dummy"); + let res = await worker.fetch("http://dummy"); await expect(res.text()).resolves.toBe("body:3"); - consoleErrorSpy.mockReset(); res = await worker.fetch("http://dummy"); await expect(res.text()).resolves.toBe("body:3"); - - await setTimeout(100); // allow some time for the error to be logged (TODO: replace with retry/waitUntil helper) - expect(consoleErrorSpy).not.toHaveBeenCalled(); }); it("config.dev.{server,inspector} changes, restart the server instance", async (t) => { const devEnv = new DevEnv(); @@ -298,10 +278,10 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { `, }); - const worker = devEnv.startWorker({ + const worker = await devEnv.startWorker({ name: "test-worker", - entrypoint: { path: path.resolve(helper.tmpPath, "src/index.ts") }, - directory: helper.tmpPath, + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + dev: { remote, server: { port: await getPort() }, @@ -316,7 +296,7 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { let undiciRes = await undici.fetch(`http://127.0.0.1:${oldPort}`); await expect(undiciRes.text()).resolves.toBe("body:1"); - worker.patchConfig({ + await worker.patchConfig({ dev: { ...worker.config.dev, remote, @@ -352,10 +332,10 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { `, }); - const worker = devEnv.startWorker({ + const worker = await devEnv.startWorker({ name: "test-worker", - entrypoint: { path: path.resolve(helper.tmpPath, "src/index.ts") }, - directory: helper.tmpPath, + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + dev: { remote, liveReload: true, @@ -400,7 +380,7 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { } `, }); - worker.patchConfig({ + await worker.patchConfig({ dev: { ...worker.config.dev, liveReload: false, @@ -427,10 +407,10 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { `, }); - const worker = devEnv.startWorker({ + const worker = await devEnv.startWorker({ name: "test-worker", - entrypoint: { path: path.resolve(helper.tmpPath, "src/index.ts") }, - directory: helper.tmpPath, + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + dev: { remote, origin: { @@ -444,7 +424,7 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { `URL: http://www.google.com/test/path/1` ); - worker.patchConfig({ + await worker.patchConfig({ dev: { ...worker.config.dev, origin: { @@ -486,10 +466,10 @@ describe.each(OPTIONS)("DevEnv", ({ remote }) => { "src/index.ts": script, }); - const worker = devEnv.startWorker({ + const worker = await devEnv.startWorker({ name: "test-worker", - entrypoint: { path: path.resolve(helper.tmpPath, "src/index.ts") }, - directory: helper.tmpPath, + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + dev: { remote, origin: { diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts index 7d388504a6a2..1820de7ff6f7 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts @@ -1,15 +1,12 @@ import { once } from "events"; -import { mkdir, writeFile } from "fs/promises"; import path from "path"; import dedent from "ts-dedent"; import { test as base, describe } from "vitest"; import { BundlerController } from "../../../api/startDevWorker/BundlerController"; import { runInTempDir } from "../../helpers/run-in-tmp"; -import type { - BundleCompleteEvent, - BundleStartEvent, - StartDevWorkerOptions, -} from "../../../api"; +import { seed } from "../../helpers/seed"; +import { unusable } from "../../helpers/unusable"; +import type { BundleCompleteEvent, StartDevWorkerOptions } from "../../../api"; // Find the bundled result of a particular source file function findSourceFile(source: string, name: string): string { @@ -17,15 +14,6 @@ function findSourceFile(source: string, name: string): string { const endIndex = source.indexOf("\n//", startIndex); return source.slice(startIndex, endIndex); } -// Seeds the `root` directory on the file system with some data. Use in -// combination with `dedent` for petty formatting of seeded contents. -export async function seed(files: Record) { - for (const [name, contents] of Object.entries(files)) { - const filePath = path.resolve(name); - await mkdir(path.dirname(filePath), { recursive: true }); - await writeFile(filePath, contents); - } -} const test = base.extend<{ controller: BundlerController }>({ // eslint-disable-next-line no-empty-pattern @@ -45,13 +33,19 @@ async function waitForBundleComplete( return event; } -async function _waitForBundleStart( - controller: BundlerController -): Promise { - const [event] = await once(controller, "bundleStart"); - return event; +function configDefaults( + config: Partial +): StartDevWorkerOptions { + const persist = path.join(process.cwd(), ".wrangler/persist"); + return { + entrypoint: "NOT_REAL", + directory: "NOT_REAL", + build: unusable(), + legacy: {}, + dev: { persist }, + ...config, + }; } - describe("happy path bundle + watch", () => { runInTempDir(); test("single ts source file", async ({ controller }) => { @@ -65,11 +59,15 @@ describe("happy path bundle + watch", () => { } satisfies ExportedHandler `, }); - const config: StartDevWorkerOptions = { + const config: Partial = { + legacy: {}, name: "worker", - entrypoint: { path: path.resolve("src/index.ts") }, + entrypoint: path.resolve("src/index.ts"), directory: path.resolve("src"), build: { + additionalModules: [], + processEntrypoint: false, + nodejsCompatMode: null, bundle: true, moduleRules: [], custom: {}, @@ -81,7 +79,7 @@ describe("happy path bundle + watch", () => { await controller.onConfigUpdate({ type: "configUpdate", - config, + config: configDefaults(config), }); let ev = await waitForBundleComplete(controller); @@ -132,11 +130,15 @@ describe("happy path bundle + watch", () => { export default "someone" `, }); - const config: StartDevWorkerOptions = { + const config: Partial = { + legacy: {}, name: "worker", - entrypoint: { path: path.resolve("src/index.ts") }, + entrypoint: path.resolve("src/index.ts"), directory: path.resolve("src"), build: { + additionalModules: [], + processEntrypoint: false, + nodejsCompatMode: null, bundle: true, moduleRules: [], custom: {}, @@ -148,7 +150,7 @@ describe("happy path bundle + watch", () => { await controller.onConfigUpdate({ type: "configUpdate", - config, + config: configDefaults(config), }); let ev = await waitForBundleComplete(controller); @@ -193,11 +195,15 @@ describe("happy path bundle + watch", () => { } satisfies ExportedHandler `, }); - const config: StartDevWorkerOptions = { + const config: Partial = { + legacy: {}, name: "worker", - entrypoint: { path: path.resolve("out.ts") }, + entrypoint: path.resolve("out.ts"), directory: path.resolve("."), build: { + additionalModules: [], + processEntrypoint: false, + nodejsCompatMode: null, bundle: true, moduleRules: [], custom: { @@ -212,7 +218,7 @@ describe("happy path bundle + watch", () => { await controller.onConfigUpdate({ type: "configUpdate", - config, + config: configDefaults(config), }); let ev = await waitForBundleComplete(controller); @@ -263,12 +269,16 @@ describe("switching", () => { } satisfies ExportedHandler `, }); - const config: StartDevWorkerOptions = { + const config: Partial = { + legacy: {}, name: "worker", - entrypoint: { path: path.resolve("src/index.ts") }, + entrypoint: path.resolve("src/index.ts"), directory: path.resolve("src"), build: { + additionalModules: [], + processEntrypoint: false, + nodejsCompatMode: null, bundle: true, moduleRules: [], custom: {}, @@ -280,7 +290,7 @@ describe("switching", () => { await controller.onConfigUpdate({ type: "configUpdate", - config, + config: configDefaults(config), }); const ev = await waitForBundleComplete(controller); @@ -305,11 +315,14 @@ describe("switching", () => { } satisfies ExportedHandler `, }); - const configCustom = { + const configCustom: Partial = { name: "worker", - entrypoint: { path: path.resolve("out.ts") }, + entrypoint: path.resolve("out.ts"), directory: process.cwd(), build: { + additionalModules: [], + processEntrypoint: false, + nodejsCompatMode: null, bundle: true, moduleRules: [], custom: { @@ -320,11 +333,12 @@ describe("switching", () => { format: "modules", moduleRoot: process.cwd(), }, - } satisfies StartDevWorkerOptions; + legacy: {}, + }; await controller.onConfigUpdate({ type: "configUpdate", - config: configCustom, + config: configDefaults(configCustom), }); let evCustom = await waitForBundleComplete(controller); @@ -373,12 +387,15 @@ describe("switching", () => { } satisfies ExportedHandler `, }); - const configCustom = { + const configCustom: Partial = { name: "worker", - entrypoint: { path: path.resolve("out.ts") }, + entrypoint: path.resolve("out.ts"), directory: process.cwd(), build: { + additionalModules: [], + processEntrypoint: false, + nodejsCompatMode: null, bundle: true, moduleRules: [], custom: { @@ -389,11 +406,11 @@ describe("switching", () => { format: "modules", moduleRoot: process.cwd(), }, - } satisfies StartDevWorkerOptions; + }; await controller.onConfigUpdate({ type: "configUpdate", - config: configCustom, + config: configDefaults(configCustom), }); const evCustom = await waitForBundleComplete(controller); @@ -417,12 +434,16 @@ describe("switching", () => { } satisfies ExportedHandler `, }); - const config: StartDevWorkerOptions = { + const config: Partial = { + legacy: {}, name: "worker", - entrypoint: { path: path.resolve("src/index.ts") }, + entrypoint: path.resolve("src/index.ts"), directory: path.resolve("src"), build: { + additionalModules: [], + processEntrypoint: false, + nodejsCompatMode: null, bundle: true, moduleRules: [], custom: {}, @@ -434,7 +455,7 @@ describe("switching", () => { await controller.onConfigUpdate({ type: "configUpdate", - config, + config: configDefaults(config), }); let ev = await waitForBundleComplete(controller); diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/ConfigController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/ConfigController.test.ts index 4f36a61203ce..3ede75567544 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/ConfigController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/ConfigController.test.ts @@ -1,7 +1,11 @@ import events from "node:events"; +import path from "node:path"; +import dedent from "ts-dedent"; import { describe, it } from "vitest"; import { ConfigController } from "../../../api/startDevWorker/ConfigController"; -import type { ConfigUpdateEvent, StartDevWorkerOptions } from "../../../api"; +import { runInTempDir } from "../../helpers/run-in-tmp"; +import { seed } from "../../helpers/seed"; +import type { ConfigUpdateEvent, StartDevWorkerInput } from "../../../api"; async function waitForConfigUpdate( controller: ConfigController @@ -11,15 +15,24 @@ async function waitForConfigUpdate( } describe("ConfigController", () => { + runInTempDir(); it("should emit configUpdate events with defaults applied", async () => { const controller = new ConfigController(); const event = waitForConfigUpdate(controller); - const config: StartDevWorkerOptions = { - entrypoint: { path: "src/index.ts" }, - directory: "./", + await seed({ + "src/index.ts": dedent/* javascript */ ` + export default { + fetch(request, env, ctx) { + return new Response("hello world") + } + } satisfies ExportedHandler + `, + }); + const config: StartDevWorkerInput = { + entrypoint: "src/index.ts", }; - controller.set(config); + await controller.set(config); await expect(event).resolves.toMatchObject({ type: "configUpdate", @@ -28,13 +41,11 @@ describe("ConfigController", () => { additionalModules: [], define: {}, format: "modules", - moduleRoot: "./", + moduleRoot: path.join(process.cwd(), "src"), moduleRules: [], }, - directory: "./", - entrypoint: { - path: "src/index.ts", - }, + directory: process.cwd(), + entrypoint: path.join(process.cwd(), "src/index.ts"), }, }); }); @@ -42,30 +53,38 @@ describe("ConfigController", () => { it("should shallow merge patched config", async () => { const controller = new ConfigController(); const event1 = waitForConfigUpdate(controller); - const config: StartDevWorkerOptions = { - entrypoint: { path: "src/index.ts" }, - directory: "./", + await seed({ + "src/index.ts": dedent/* javascript */ ` + export default { + fetch(request, env, ctx) { + return new Response("hello world") + } + } satisfies ExportedHandler + `, + }); + const config: StartDevWorkerInput = { + entrypoint: "src/index.ts", }; - controller.set(config); + await controller.set(config); await expect(event1).resolves.toMatchObject({ type: "configUpdate", config: { - entrypoint: { path: "src/index.ts" }, - directory: "./", + entrypoint: path.join(process.cwd(), "src/index.ts"), + directory: process.cwd(), build: { additionalModules: [], define: {}, format: "modules", - moduleRoot: "./", + moduleRoot: path.join(process.cwd(), "src"), moduleRules: [], }, }, }); const event2 = waitForConfigUpdate(controller); - controller.patch({ + await controller.patch({ dev: { remote: true, liveReload: true, @@ -76,13 +95,13 @@ describe("ConfigController", () => { await expect(event2).resolves.toMatchObject({ type: "configUpdate", config: { - entrypoint: { path: "src/index.ts" }, - directory: "./", + entrypoint: path.join(process.cwd(), "src/index.ts"), + directory: process.cwd(), build: { additionalModules: [], define: {}, format: "modules", - moduleRoot: "./", + moduleRoot: path.join(process.cwd(), "src"), moduleRules: [], }, dev: { @@ -94,26 +113,26 @@ describe("ConfigController", () => { }); const event3 = waitForConfigUpdate(controller); - controller.patch({ + await controller.patch({ dev: { - server: { hostname: "myexample.com" }, + origin: { hostname: "myexample.com" }, }, }); // expect `dev` field to be overwritten and all other config to remain intact await expect(event3).resolves.toMatchObject({ type: "configUpdate", config: { - entrypoint: { path: "src/index.ts" }, - directory: "./", + entrypoint: path.join(process.cwd(), "src/index.ts"), + directory: process.cwd(), build: { additionalModules: [], define: {}, format: "modules", - moduleRoot: "./", + moduleRoot: path.join(process.cwd(), "src"), moduleRules: [], }, dev: { - server: { hostname: "myexample.com" }, + origin: { hostname: "myexample.com" }, }, }, }); diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts index e7ab9f3fdaa3..2e52009cf3e7 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import events from "node:events"; import fs, { readFileSync } from "node:fs"; import net from "node:net"; @@ -7,7 +8,7 @@ import { DeferredPromise, Response } from "miniflare"; import dedent from "ts-dedent"; import { fetch } from "undici"; import { assert, describe, expect, it } from "vitest"; -import { WebSocket } from "ws"; +import WebSocket from "ws"; import { LocalRuntimeController } from "../../../api/startDevWorker/LocalRuntimeController"; import { urlFromParts } from "../../../api/startDevWorker/utils"; import { RuleTypeToModuleType } from "../../../deployment-bundle/module-collection"; @@ -16,7 +17,6 @@ import { unusable } from "../../helpers/unusable"; import type { Bundle, File, - FilePath, ReloadCompleteEvent, StartDevWorkerOptions, } from "../../../api"; @@ -119,14 +119,29 @@ function makeEsbuildBundle(testBundle: TestBundle): Bundle { return bundle; } +function configDefaults( + config: Partial +): StartDevWorkerOptions { + const tmp = useTmp(); + const persist = path.join(tmp, "persist"); + return { + entrypoint: "NOT_REAL", + directory: "NOT_REAL", + build: unusable(), + legacy: {}, + dev: { persist }, + ...config, + }; +} + describe("Core", () => { it("should start Miniflare with module worker", async () => { const controller = new LocalRuntimeController(); teardown(() => controller.teardown()); - const config: StartDevWorkerOptions = { + const config = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", compatibilityFlags: ["nodejs_compat"], compatibilityDate: "2023-10-01", }; @@ -220,8 +235,15 @@ describe("Core", () => { sourceMapPath: undefined, sourceMapMetadata: undefined, }; - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); const event = await waitForReloadComplete(controller); const url = urlFromParts(event.proxyData.userWorkerUrl); @@ -269,9 +291,9 @@ describe("Core", () => { const controller = new LocalRuntimeController(); teardown(() => controller.teardown()); - const config: StartDevWorkerOptions = { + const config = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", }; const bundle: Bundle = { type: "commonjs", @@ -326,8 +348,15 @@ describe("Core", () => { sourceMapPath: undefined, sourceMapMetadata: undefined, }; - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); const event = await waitForReloadComplete(controller); const url = urlFromParts(event.proxyData.userWorkerUrl); @@ -356,13 +385,13 @@ describe("Core", () => { teardown(() => controller.teardown()); function update(version: number) { - const config: StartDevWorkerOptions = { + const config = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", bindings: { VERSION: { type: "json", value: version }, }, - }; + } satisfies Partial; const bundle = makeEsbuildBundle(dedent/*javascript*/ ` export default { fetch(request, env, ctx) { @@ -370,8 +399,15 @@ describe("Core", () => { } } `); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); } // Start worker @@ -404,9 +440,9 @@ describe("Core", () => { const disabledDate = "2022-03-20"; const enabledDate = "2022-03-21"; - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", compatibilityDate: disabledDate, }; const bundle = makeEsbuildBundle(dedent/*javascript*/ ` @@ -415,16 +451,30 @@ describe("Core", () => { } `); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); let event = await waitForReloadComplete(controller); let res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.text()).toBe("undefined"); // Check respects compatibility date config.compatibilityDate = enabledDate; - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.text()).toBe("object"); @@ -432,8 +482,15 @@ describe("Core", () => { // Check respects compatibility flags config.compatibilityDate = disabledDate; config.compatibilityFlags = ["global_navigator"]; - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.text()).toBe("object"); @@ -442,9 +499,9 @@ describe("Core", () => { const controller = new LocalRuntimeController(); teardown(() => controller.teardown()); - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", }; const bundle = makeEsbuildBundle(dedent/*javascript*/ ` export default { @@ -454,8 +511,15 @@ describe("Core", () => { } } `); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); const event = await waitForReloadComplete(controller); const url = urlFromParts(event.proxyData.userWorkerUrl); const inspectorUrl = urlFromParts(event.proxyData.userWorkerInspectorUrl); @@ -501,9 +565,9 @@ describe("Bindings", () => { const controller = new LocalRuntimeController(); teardown(() => controller.teardown()); - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", bindings: { TEXT: { type: "plain_text", value: "text" }, OBJECT: { type: "json", value: { a: { b: 1 } } }, @@ -526,8 +590,15 @@ describe("Bindings", () => { } } `); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); const event = await waitForReloadComplete(controller); const res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); @@ -541,9 +612,9 @@ describe("Bindings", () => { const controller = new LocalRuntimeController(); teardown(() => controller.teardown()); - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", bindings: { // `wasm-module` bindings aren't allowed in modules workers WASM: { type: "wasm_module", source: { contents: WASM_ADD_MODULE } }, @@ -558,8 +629,15 @@ describe("Bindings", () => { });`, }, }); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); const event = await waitForReloadComplete(controller); const res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); @@ -571,11 +649,12 @@ describe("Bindings", () => { const controller = new LocalRuntimeController(); teardown(() => controller.teardown()); - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), - dev: { persist: { path: persist } }, + entrypoint: "NOT_REAL", + dev: { persist }, }; + const bundle = makeEsbuildBundle(`export default { async fetch(request, env, ctx) { const key = "http://localhost/"; @@ -588,8 +667,17 @@ describe("Bindings", () => { return (await caches.default.match(key)) ?? new Response("miss"); } }`); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); let event = await waitForReloadComplete(controller); let res = await fetch(urlFromParts(event.proxyData.userWorkerUrl), { @@ -598,8 +686,15 @@ describe("Bindings", () => { expect(await res.text()).toBe("cached"); // Check restarting uses persisted data - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.text()).toBe("cached"); @@ -607,8 +702,15 @@ describe("Bindings", () => { // Check deleting persistence directory removes data await controller.teardown(); fs.rmSync(persist, { recursive: true }); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.text()).toBe("miss"); @@ -619,11 +721,11 @@ describe("Bindings", () => { const controller = new LocalRuntimeController(); teardown(() => controller.teardown()); - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", bindings: { NAMESPACE: { type: "kv_namespace", id: "ns" } }, - dev: { persist: { path: persist } }, + dev: { persist }, }; const bundle = makeEsbuildBundle(`export default { async fetch(request, env, ctx) { @@ -631,8 +733,15 @@ describe("Bindings", () => { return new Response(await env.NAMESPACE.get("key")); } }`); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); let event = await waitForReloadComplete(controller); let res = await fetch(urlFromParts(event.proxyData.userWorkerUrl), { @@ -641,8 +750,15 @@ describe("Bindings", () => { expect(await res.text()).toBe("value"); // Check restarting uses persisted data - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.text()).toBe("value"); @@ -650,8 +766,15 @@ describe("Bindings", () => { // Check deleting persistence directory removes data await controller.teardown(); fs.rmSync(persist, { recursive: true }); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.text()).toBe(""); @@ -665,9 +788,9 @@ describe("Bindings", () => { fs.writeFileSync(path.join(tmp, "charts.xlsx"), "📊"); fs.writeFileSync(path.join(tmp, "secrets.txt"), "🔐"); - let config: StartDevWorkerOptions = { + let config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", legacy: { site: { bucket: tmp, include: ["*.txt"] } }, }; const bundle = makeEsbuildBundle(` @@ -685,8 +808,15 @@ describe("Bindings", () => { } }`); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); let event = await waitForReloadComplete(controller); let url = urlFromParts(event.proxyData.userWorkerUrl); let res = await fetch(new URL("/company.txt", url)); @@ -703,8 +833,15 @@ describe("Bindings", () => { site: { bucket: tmp, exclude: ["secrets.txt"] }, }, }; - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); url = urlFromParts(event.proxyData.userWorkerUrl); res = await fetch(new URL("/company.txt", url)); @@ -720,11 +857,11 @@ describe("Bindings", () => { const controller = new LocalRuntimeController(); teardown(() => controller.teardown()); - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", bindings: { BUCKET: { type: "r2_bucket", bucket_name: "bucket" } }, - dev: { persist: { path: persist } }, + dev: { persist }, }; const bundle = makeEsbuildBundle(`export default { async fetch(request, env, ctx) { @@ -733,8 +870,15 @@ describe("Bindings", () => { return new Response(object?.body); } }`); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); let event = await waitForReloadComplete(controller); let res = await fetch(urlFromParts(event.proxyData.userWorkerUrl), { @@ -743,8 +887,15 @@ describe("Bindings", () => { expect(await res.text()).toBe("value"); // Check restarting uses persisted data - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.text()).toBe("value"); @@ -752,8 +903,15 @@ describe("Bindings", () => { // Check deleting persistence directory removes data await controller.teardown(); fs.rmSync(persist, { recursive: true }); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.text()).toBe(""); @@ -764,13 +922,13 @@ describe("Bindings", () => { const controller = new LocalRuntimeController(); teardown(() => controller.teardown()); - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", bindings: { DB: { type: "d1", database_name: "db-name", database_id: "db" }, }, - dev: { persist: { path: persist } }, + dev: { persist }, }; const bundle = makeEsbuildBundle(`export default { async fetch(request, env, ctx) { @@ -782,8 +940,15 @@ describe("Bindings", () => { return Response.json(result.results); } }`); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); let event = await waitForReloadComplete(controller); let res = await fetch(urlFromParts(event.proxyData.userWorkerUrl), { @@ -792,8 +957,15 @@ describe("Bindings", () => { expect(await res.json()).toEqual([{ key: "key", value: "value" }]); // Check restarting uses persisted data - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.json()).toEqual([{ key: "key", value: "value" }]); @@ -801,8 +973,15 @@ describe("Bindings", () => { // Check deleting persistence directory removes data await controller.teardown(); fs.rmSync(persist, { recursive: true }); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); event = await waitForReloadComplete(controller); res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); expect(await res.json()).toEqual([]); @@ -814,9 +993,9 @@ describe("Bindings", () => { teardown(() => controller.teardown()); const reportPromise = new DeferredPromise(); - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", bindings: { QUEUE: { type: "queue", queue_name: "queue" }, BATCH_REPORT: { @@ -830,7 +1009,7 @@ describe("Bindings", () => { triggers: [ { type: "queue-consumer", queue: "queue", max_batch_timeout: 0 }, ], - dev: { persist: { path: persist } }, + dev: { persist }, }; const bundle = makeEsbuildBundle(`export default { async fetch(request, env, ctx) { @@ -844,8 +1023,15 @@ describe("Bindings", () => { }); } }`); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); const event = await waitForReloadComplete(controller); const res = await fetch(urlFromParts(event.proxyData.userWorkerUrl), { @@ -870,9 +1056,9 @@ describe("Bindings", () => { teardown(() => controller.teardown()); const localConnectionString = `postgres://username:password@127.0.0.1:${port}/db`; - const config: StartDevWorkerOptions = { + const config: Partial = { name: "worker", - entrypoint: unusable(), + entrypoint: "NOT_REAL", bindings: { DB: { type: "hyperdrive", id: "db", localConnectionString } }, }; const bundle = makeEsbuildBundle(`export default { @@ -884,8 +1070,15 @@ describe("Bindings", () => { return new Response(socket.readable); } }`); - controller.onBundleStart({ type: "bundleStart", config }); - controller.onBundleComplete({ type: "bundleComplete", config, bundle }); + controller.onBundleStart({ + type: "bundleStart", + config: configDefaults(config), + }); + controller.onBundleComplete({ + type: "bundleComplete", + config: configDefaults(config), + bundle, + }); const event = await waitForReloadComplete(controller); const res = await fetch(urlFromParts(event.proxyData.userWorkerUrl)); diff --git a/packages/wrangler/src/__tests__/helpers/seed.ts b/packages/wrangler/src/__tests__/helpers/seed.ts new file mode 100644 index 000000000000..d0df7dfe71b5 --- /dev/null +++ b/packages/wrangler/src/__tests__/helpers/seed.ts @@ -0,0 +1,13 @@ +// Seeds the `root` directory on the file system with some data. Use in + +import { mkdir, writeFile } from "fs/promises"; +import path from "path"; + +// combination with `dedent` for petty formatting of seeded contents. +export async function seed(files: Record) { + for (const [name, contents] of Object.entries(files)) { + const filePath = path.resolve(name); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, contents); + } +} diff --git a/packages/wrangler/src/__tests__/helpers/teardown.ts b/packages/wrangler/src/__tests__/helpers/teardown.ts index 47e935af2faf..fd8d4542a0cd 100644 --- a/packages/wrangler/src/__tests__/helpers/teardown.ts +++ b/packages/wrangler/src/__tests__/helpers/teardown.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterEach } from "vitest"; const teardownCallbacks: (() => void | Promise)[] = []; +// TODO: Switch to vitest.onTestFinished() export function teardown(callback: () => void | Promise) { // `unshift()` so teardown callbacks executed in reverse teardownCallbacks.unshift(callback); @@ -28,6 +29,11 @@ afterEach(async () => { export function useTmp() { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "wrangler-vitest-")); - teardown(() => fs.rmSync(tmp, { recursive: true, force: true })); + teardown(() => { + try { + // This sometimes fails with EBUSY on Windows + fs.rmSync(tmp, { recursive: true, force: true }); + } catch {} + }); return tmp; } diff --git a/packages/wrangler/src/api/dev.ts b/packages/wrangler/src/api/dev.ts index 503923667f8d..cdafbe8d772a 100644 --- a/packages/wrangler/src/api/dev.ts +++ b/packages/wrangler/src/api/dev.ts @@ -8,6 +8,8 @@ import type { CfModule } from "../deployment-bundle/worker"; import type { StartDevOptions } from "../dev"; import type { EnablePagesAssetsServiceBindingOptions } from "../miniflare-cli/types"; import type { ProxyData } from "./startDevWorker"; +import type { FSWatcher } from "chokidar"; +import type { Instance } from "ink"; import type { Json } from "miniflare"; import type { RequestInfo, RequestInit, Response } from "undici"; @@ -250,14 +252,20 @@ export async function unstable_dev( }; } else { //outside of test mode, rebuilds work fine, but only one instance of wrangler will work at a time - const devServer = await run( + const devServer = (await run( { - DEV_ENV: devEnv, + DEV_ENV: false, FILE_BASED_REGISTRY: fileBasedRegistry, + JSON_CONFIG_FILE: Boolean(devOptions.experimentalJsonConfig), }, () => startDev(devOptions) - ); + )) as { + devReactElement: Instance; + watcher: FSWatcher | undefined; + stop: () => Promise; + }; const { port, address, proxyData } = await readyPromise; + return { port, address, diff --git a/packages/wrangler/src/api/integrations/platform/index.ts b/packages/wrangler/src/api/integrations/platform/index.ts index 048587e5541a..674120ad655d 100644 --- a/packages/wrangler/src/api/integrations/platform/index.ts +++ b/packages/wrangler/src/api/integrations/platform/index.ts @@ -109,6 +109,7 @@ export async function getPlatformProxy< { FILE_BASED_REGISTRY: Boolean(options.experimentalRegistry), DEV_ENV: false, + JSON_CONFIG_FILE: Boolean(options.experimentalJsonConfig), }, () => getMiniflareOptionsFromConfig(rawConfig, env, options) ); diff --git a/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts b/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts index 1a695e33e5ba..19948e1f6830 100644 --- a/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts +++ b/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts @@ -83,7 +83,6 @@ function createWorkerBundleFormData( migrations: undefined, compatibility_date: config?.compatibility_date, compatibility_flags: config?.compatibility_flags, - usage_model: undefined, keepVars: undefined, keepSecrets: undefined, keepBindings: undefined, diff --git a/packages/wrangler/src/api/startDevWorker/BundlerController.ts b/packages/wrangler/src/api/startDevWorker/BundlerController.ts index 250a471e8b83..7ed05096839d 100644 --- a/packages/wrangler/src/api/startDevWorker/BundlerController.ts +++ b/packages/wrangler/src/api/startDevWorker/BundlerController.ts @@ -43,10 +43,6 @@ export class BundlerController extends Controller { #customBuildAborter = new AbortController(); async #runCustomBuild(config: StartDevWorkerOptions, filePath: string) { - assert(config.entrypoint?.path); - assert(config.directory); - assert(config.build?.format); - assert(config.build?.moduleRoot); // If a new custom build comes in, we need to cancel in-flight builds this.#customBuildAborter.abort(); this.#customBuildAborter = new AbortController(); @@ -54,11 +50,11 @@ export class BundlerController extends Controller { // Since `this.#customBuildAborter` will change as new builds are scheduled, store the specific AbortController that will be used for this build const buildAborter = this.#customBuildAborter; const relativeFile = - path.relative(config.directory, config.entrypoint.path) || "."; + path.relative(config.directory, config.entrypoint) || "."; logger.log(`The file ${filePath} changed, restarting build...`); this.emitBundleStartEvent(config); try { - await runCustomBuild(config.entrypoint.path, relativeFile, { + await runCustomBuild(config.entrypoint, relativeFile, { cwd: config.build?.custom?.workingDirectory, command: config.build?.custom?.command, }); @@ -66,29 +62,27 @@ export class BundlerController extends Controller { return; } assert(this.#tmpDir); - assert(config.build?.moduleRules, "config.build?.moduleRules"); - assert(config.build?.define, "config.build?.define"); if (!config.build?.bundle) { // if we're not bundling, let's just copy the entry to the destination directory const destinationDir = this.#tmpDir.path; writeFileSync( - path.join(destinationDir, path.basename(config.entrypoint.path)), - readFileSync(config.entrypoint.path, "utf-8") + path.join(destinationDir, path.basename(config.entrypoint)), + readFileSync(config.entrypoint, "utf-8") ); } const entry: Entry = { - file: config.entrypoint.path, + file: config.entrypoint, directory: config.directory, format: config.build.format, moduleRoot: config.build.moduleRoot, }; - const entryDirectory = path.dirname(config.entrypoint.path); + const entryDirectory = path.dirname(config.entrypoint); const moduleCollector = createModuleCollector({ wrangler1xLegacyModuleReferences: getWrangler1xLegacyModuleReferences( entryDirectory, - config.entrypoint.path + config.entrypoint ), entry, // `moduleCollector` doesn't get used when `noBundle` is set, so @@ -139,7 +133,7 @@ export class BundlerController extends Controller { return; } const entrypointPath = realpathSync( - bundleResult?.resolvedEntryPointPath ?? config.entrypoint.path + bundleResult?.resolvedEntryPointPath ?? config.entrypoint ); this.emitBundleCompleteEvent(config, { @@ -148,7 +142,7 @@ export class BundlerController extends Controller { path: entrypointPath, type: bundleResult?.bundleType ?? - getBundleType(config.build.format, config.entrypoint.path), + getBundleType(config.build.format, config.entrypoint), modules: bundleResult.modules, dependencies: bundleResult?.dependencies ?? {}, sourceMapPath: bundleResult?.sourceMapPath, @@ -175,8 +169,10 @@ export class BundlerController extends Controller { return; } - const pathsToWatch = config.build?.custom?.watch; - assert(pathsToWatch); + const pathsToWatch = config.build.custom.watch; + + // This is always present if a custom command is provided, defaulting to `./src` + assert(pathsToWatch, "config.build.custom.watch"); this.#customBuildWatcher = watch(pathsToWatch, { persistent: true, @@ -201,14 +197,8 @@ export class BundlerController extends Controller { return; } assert(this.#tmpDir); - assert(config.build?.moduleRules, "config.build?.moduleRules"); - assert(config.build?.define, "config.build?.define"); - assert(config.entrypoint?.path, "config.entrypoint?.path"); - assert(config.directory, "config.directory"); - assert(config.build.format, "config.build.format"); - assert(config.build.moduleRoot, "config.build.moduleRoot"); const entry: Entry = { - file: config.entrypoint.path, + file: config.entrypoint, directory: config.directory, format: config.build.format, moduleRoot: config.build.moduleRoot, diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index 1433cbd8d17d..fea129b961df 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -1,52 +1,377 @@ import assert from "node:assert"; +import path from "node:path"; +import { watch } from "chokidar"; +import { + DEFAULT_INSPECTOR_PORT, + DEFAULT_LOCAL_PORT, + getDevCompatibilityDate, + getRules, + getScriptName, + isLegacyEnv, +} from "../.."; +import { printBindings, readConfig } from "../../config"; +import { getEntry } from "../../deployment-bundle/entry"; +import { + getBindings, + getHostAndRoutes, + getInferredHost, + maskVars, +} from "../../dev"; +import { getLocalPersistencePath } from "../../dev/get-local-persistence-path"; +import { UserError } from "../../errors"; +import { logger } from "../../logger"; +import { getAccountId, requireApiToken } from "../../user"; +import { memoizeGetPort } from "../../utils/memoizeGetPort"; import { Controller } from "./BaseController"; +import { + convertCfWorkerInitBindingstoBindings, + extractBindingsOfType, + unwrapHook, +} from "./utils"; +import type { Config } from "../../config"; +import type { CfUnsafe } from "../../deployment-bundle/worker"; import type { ControllerEventMap } from "./BaseController"; import type { ConfigUpdateEvent } from "./events"; -import type { StartDevWorkerOptions } from "./types"; +import type { + StartDevWorkerInput, + StartDevWorkerOptions, + Trigger, +} from "./types"; export type ConfigControllerEventMap = ControllerEventMap & { configUpdate: [ConfigUpdateEvent]; }; -type Options = StartDevWorkerOptions; +const getInspectorPort = memoizeGetPort(DEFAULT_INSPECTOR_PORT, "127.0.0.1"); +const getLocalPort = memoizeGetPort(DEFAULT_LOCAL_PORT, "localhost"); + +async function resolveDevConfig( + config: Config, + input: StartDevWorkerInput +): Promise { + const localPersistencePath = getLocalPersistencePath( + input.dev?.persist, + config.configPath + ); + + const { host, routes } = await getHostAndRoutes( + { + host: input.dev?.origin?.hostname, + routes: input.triggers?.filter( + (t): t is Extract => t.type === "route" + ), + }, + config + ); + + const initialIp = input.dev?.server?.hostname ?? config.dev.ip; + + const initialIpListenCheck = initialIp === "*" ? "0.0.0.0" : initialIp; + + return { + auth: + input.dev?.auth ?? + (async () => { + return { + accountId: await getAccountId(), + apiToken: requireApiToken(), + }; + }), + remote: input.dev?.remote, + server: { + hostname: input.dev?.server?.hostname || config.dev.ip, + port: + input.dev?.server?.port ?? + config.dev.port ?? + (await getLocalPort(initialIpListenCheck)), + secure: + input.dev?.server?.secure || config.dev.local_protocol === "https", + httpsKeyPath: input.dev?.server?.httpsKeyPath, + httpsCertPath: input.dev?.server?.httpsCertPath, + }, + inspector: { + port: + input.dev?.inspector?.port ?? + config.dev.inspector_port ?? + (await getInspectorPort()), + }, + origin: { + secure: + input.dev?.origin?.secure || config.dev.upstream_protocol === "https", + hostname: host ?? getInferredHost(routes), + }, + liveReload: input.dev?.liveReload || false, + testScheduled: input.dev?.testScheduled, + // absolute resolved path + persist: localPersistencePath, + registry: input.dev?.registry, + } satisfies StartDevWorkerOptions["dev"]; +} + +async function resolveBindings( + config: Config, + input: StartDevWorkerInput +): Promise<{ bindings: StartDevWorkerOptions["bindings"]; unsafe?: CfUnsafe }> { + const bindings = getBindings(config, input.env, !input.dev?.remote, { + kv: extractBindingsOfType("kv_namespace", input.bindings), + vars: Object.fromEntries( + extractBindingsOfType("plain_text", input.bindings).map((b) => [ + b.binding, + b.value, + ]) + ), + durableObjects: extractBindingsOfType( + "durable_object_namespace", + input.bindings + ), + r2: extractBindingsOfType("r2_bucket", input.bindings), + services: extractBindingsOfType("service", input.bindings), + d1Databases: extractBindingsOfType("d1", input.bindings), + ai: extractBindingsOfType("ai", input.bindings)?.[0], + version_metadata: extractBindingsOfType( + "version_metadata", + input.bindings + )?.[0], + }); + + const maskedVars = maskVars(bindings, config); + + // now log all available bindings into the terminal + printBindings({ + ...bindings, + vars: maskedVars, + }); + + return { + bindings: { + ...input.bindings, + ...convertCfWorkerInitBindingstoBindings(bindings), + }, + unsafe: bindings.unsafe, + }; +} + +async function resolveTriggers( + config: Config, + input: StartDevWorkerInput +): Promise { + const { routes } = await getHostAndRoutes( + { + host: input.dev?.origin?.hostname, + routes: input.triggers?.filter( + (t): t is Extract => t.type === "route" + ), + }, + config + ); + + const devRoutes = + routes?.map>((r) => + typeof r === "string" + ? { + type: "route", + pattern: r, + } + : { type: "route", ...r } + ) ?? []; + const queueConsumers = + config.queues.consumers?.map>( + (c) => ({ + ...c, + type: "queue-consumer", + }) + ) ?? []; + + const crons = + config.triggers.crons?.map>((c) => ({ + cron: c, + type: "cron", + })) ?? []; + + return [...devRoutes, ...queueConsumers, ...crons]; +} + +async function resolveConfig( + config: Config, + input: StartDevWorkerInput +): Promise { + const legacySite = unwrapHook(input.legacy?.site, config); + + const legacyAssets = unwrapHook(input.legacy?.assets, config); + + const entry = await getEntry( + { + assets: Boolean(legacyAssets), + script: input.entrypoint, + moduleRoot: input.build?.moduleRoot, + }, + config, + "dev" + ); + + const nodejsCompatMode = unwrapHook(input.build?.nodejsCompatMode, config); + + const { bindings, unsafe } = await resolveBindings(config, input); + + const resolved = { + name: getScriptName({ name: input.name, env: input.env }, config), + compatibilityDate: getDevCompatibilityDate(config, input.compatibilityDate), + compatibilityFlags: input.compatibilityFlags ?? config.compatibility_flags, + entrypoint: entry.file, + directory: entry.directory, + bindings, + sendMetrics: input.sendMetrics ?? config.send_metrics, + triggers: await resolveTriggers(config, input), + env: input.env, + build: { + additionalModules: input.build?.additionalModules ?? [], + processEntrypoint: Boolean(input.build?.processEntrypoint), + bundle: input.build?.bundle ?? !config.no_bundle, + findAdditionalModules: + input.build?.findAdditionalModules ?? config.find_additional_modules, + moduleRoot: entry.moduleRoot, + moduleRules: input.build?.moduleRules ?? getRules(config), + + minify: input.build?.minify ?? config.minify, + define: { ...config.define, ...input.build?.define }, + custom: { + command: input.build?.custom?.command ?? config.build?.command, + watch: input.build?.custom?.watch ?? config.build?.watch_dir, + workingDirectory: + input.build?.custom?.workingDirectory ?? config.build?.cwd, + }, + format: entry.format, + nodejsCompatMode: nodejsCompatMode ?? null, + jsxFactory: input.build?.jsxFactory || config.jsx_factory, + jsxFragment: input.build?.jsxFragment || config.jsx_fragment, + tsconfig: input.build?.tsconfig ?? config.tsconfig, + }, + dev: await resolveDevConfig(config, input), + legacy: { + site: legacySite, + assets: legacyAssets, + enableServiceEnvironments: + input.legacy?.enableServiceEnvironments ?? !isLegacyEnv(config), + }, + unsafe: { + capnp: input.unsafe?.capnp ?? unsafe?.capnp, + metadata: input.unsafe?.metadata ?? unsafe?.metadata, + }, + } satisfies StartDevWorkerOptions; + + if (resolved.legacy.assets && resolved.legacy.site) { + throw new UserError( + "Cannot use Assets and Workers Sites in the same Worker." + ); + } + + const services = extractBindingsOfType("service", resolved.bindings); + if (services && services.length > 0) { + logger.warn( + `This worker is bound to live services: ${services + .map( + (service) => + `${service.name} (${service.service}${ + service.environment ? `@${service.environment}` : "" + }${service.entrypoint ? `#${service.entrypoint}` : ""})` + ) + .join(", ")}` + ); + } + + if (!resolved.dev?.origin?.secure && resolved.dev?.remote) { + logger.warn( + "Setting upstream-protocol to http is not currently supported for remote mode.\n" + + "If this is required in your project, please add your use case to the following issue:\n" + + "https://github.com/cloudflare/workers-sdk/issues/583." + ); + } + + // TODO(queues) support remote wrangler dev + const queues = extractBindingsOfType("queue", resolved.bindings); + if ( + resolved.dev.remote && + (queues?.length || + resolved.triggers?.some((t) => t.type === "queue-consumer")) + ) { + logger.warn( + "Queues are currently in Beta and are not supported in wrangler dev remote mode." + ); + } + return resolved; +} export class ConfigController extends Controller { - latestInput?: Options; - latestConfig?: Options; + latestInput?: StartDevWorkerInput; + latestConfig?: StartDevWorkerOptions; + + #configWatcher?: ReturnType; + #abortController?: AbortController; - public set(input: Options) { - this.#updateConfig(input); + async #ensureWatchingConfig(configPath: string | undefined) { + await this.#configWatcher?.close(); + if (configPath) { + this.#configWatcher = watch(configPath, { + persistent: true, + }).on("change", async (_event) => { + logger.log(`${path.basename(configPath)} changed...`); + assert( + this.latestInput, + "Cannot be watching config without having first set an input" + ); + void this.#updateConfig(this.latestInput); + }); + } + } + public set(input: StartDevWorkerInput) { + return this.#updateConfig(input); } - public patch(input: Partial) { + public patch(input: Partial) { assert( this.latestInput, "Cannot call updateConfig without previously calling setConfig" ); - const config: Options = { + const config: StartDevWorkerInput = { ...this.latestInput, ...input, }; - this.#updateConfig(config); + return this.#updateConfig(config); } - #updateConfig(input: Options) { - const directory = input.directory; - - this.latestConfig = { - directory, - build: { - moduleRules: [], - additionalModules: [], - define: {}, - format: "modules", - moduleRoot: directory, // TODO: this default needs to come from getEntry() once readConfig has been moved into ConfigController - ...input.build, - }, - ...input, - }; + async #updateConfig(input: StartDevWorkerInput) { + this.#abortController?.abort(); + this.#abortController = new AbortController(); + const signal = this.#abortController.signal; this.latestInput = input; - this.emitConfigUpdateEvent(this.latestConfig); + + const fileConfig = readConfig(input.config, { + env: input.env, + "dispatch-namespace": undefined, + "legacy-env": !input.legacy?.enableServiceEnvironments ?? true, + remote: input.dev?.remote, + upstreamProtocol: + input.dev?.origin?.secure === undefined + ? undefined + : input.dev?.origin?.secure + ? "https" + : "http", + localProtocol: + input.dev?.server?.secure === undefined + ? undefined + : input.dev?.server?.secure + ? "https" + : "http", + }); + void this.#ensureWatchingConfig(fileConfig.configPath); + + const resolvedConfig = await resolveConfig(fileConfig, input); + if (signal.aborted) { + return; + } + this.latestConfig = resolvedConfig; + this.emitConfigUpdateEvent(resolvedConfig); + return this.latestConfig; } // ****************** @@ -54,14 +379,14 @@ export class ConfigController extends Controller { // ****************** async teardown() { - // do nothing + await this.#configWatcher?.close(); } // ********************* // Event Dispatchers // ********************* - emitConfigUpdateEvent(config: Options) { + emitConfigUpdateEvent(config: StartDevWorkerOptions) { this.emit("configUpdate", { type: "configUpdate", config }); } } diff --git a/packages/wrangler/src/api/startDevWorker/DevEnv.ts b/packages/wrangler/src/api/startDevWorker/DevEnv.ts index 8a9840d00fed..d7cc698182d1 100644 --- a/packages/wrangler/src/api/startDevWorker/DevEnv.ts +++ b/packages/wrangler/src/api/startDevWorker/DevEnv.ts @@ -8,7 +8,7 @@ import { ProxyController } from "./ProxyController"; import { RemoteRuntimeController } from "./RemoteRuntimeController"; import type { Controller, RuntimeController } from "./BaseController"; import type { ErrorEvent } from "./events"; -import type { StartDevWorkerOptions, Worker } from "./types"; +import type { StartDevWorkerInput, Worker } from "./types"; export class DevEnv extends EventEmitter { config: ConfigController; @@ -16,10 +16,10 @@ export class DevEnv extends EventEmitter { runtimes: RuntimeController[]; proxy: ProxyController; - startWorker(options: StartDevWorkerOptions): Worker { + async startWorker(options: StartDevWorkerInput): Promise { const worker = createWorkerObject(this); - this.config.set(options); + await this.config.set(options); return worker; } @@ -93,14 +93,13 @@ export class DevEnv extends EventEmitter { // ********************* async teardown() { - this.emit("teardown"); - await Promise.all([ this.config.teardown(), this.bundler.teardown(), ...this.runtimes.map((runtime) => runtime.teardown()), this.proxy.teardown(), ]); + this.emit("teardown"); } emitErrorEvent(ev: ErrorEvent) { diff --git a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts index 616a7063d622..93a8d825253e 100644 --- a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts @@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto"; import { readFile } from "node:fs/promises"; import chalk from "chalk"; import { Miniflare, Mutex } from "miniflare"; -import { getLocalPersistencePath } from "../../dev/get-local-persistence-path"; import * as MF from "../../dev/miniflare"; import { logger } from "../../logger"; import { RuntimeController } from "./BaseController"; @@ -52,13 +51,6 @@ async function convertToConfigBundle( event.config.bindings ); - const persistence = getLocalPersistencePath( - typeof event.config.dev?.persist === "object" - ? event.config.dev?.persist.path - : undefined, - event.config.config?.path - ); - const crons = []; const queueConsumers = []; for (const trigger of event.config.triggers ?? []) { @@ -112,7 +104,7 @@ async function convertToConfigBundle( initialIp: "127.0.0.1", rules: [], inspectorPort: 0, - localPersistencePath: persistence, + localPersistencePath: event.config.dev.persist, liveReload: event.config.dev?.liveReload ?? false, crons, queueConsumers, diff --git a/packages/wrangler/src/api/startDevWorker/ProxyController.ts b/packages/wrangler/src/api/startDevWorker/ProxyController.ts index 4f3bbb854d3e..7f61d9d4bd6b 100644 --- a/packages/wrangler/src/api/startDevWorker/ProxyController.ts +++ b/packages/wrangler/src/api/startDevWorker/ProxyController.ts @@ -1,9 +1,11 @@ import assert from "node:assert"; import { randomUUID } from "node:crypto"; +import events from "node:events"; import path from "node:path"; -import { LogLevel, Miniflare, Mutex, Response, WebSocket } from "miniflare"; +import { LogLevel, Miniflare, Mutex, Response } from "miniflare"; import inspectorProxyWorkerPath from "worker:startDevWorker/InspectorProxyWorker"; import proxyWorkerPath from "worker:startDevWorker/ProxyWorker"; +import WebSocket from "ws"; import { logConsoleMessage, maybeHandleNetworkLoadResource, @@ -50,7 +52,7 @@ export class ProxyController extends Controller { public proxyWorker?: Miniflare; proxyWorkerOptions?: MiniflareOptions; - inspectorProxyWorkerWebSocket?: DeferredPromise; + private inspectorProxyWorkerWebSocket?: DeferredPromise; protected latestConfig?: StartDevWorkerOptions; protected latestBundle?: EsbuildBundle; @@ -199,13 +201,15 @@ export class ProxyController extends Controller { } } - async reconnectInspectorProxyWorker(): Promise { + private async reconnectInspectorProxyWorker(): Promise< + WebSocket | undefined + > { if (this._torndown) { return; } const existingWebSocket = await this.inspectorProxyWorkerWebSocket?.promise; - if (existingWebSocket?.readyState === WebSocket.READY_STATE_OPEN) { + if (existingWebSocket?.readyState === WebSocket.OPEN) { return existingWebSocket; } @@ -215,15 +219,16 @@ export class ProxyController extends Controller { try { assert(this.proxyWorker); - const inspectorProxyWorker = await this.proxyWorker.getWorker( + + const inspectorProxyWorkerUrl = await this.proxyWorker.unsafeGetDirectURL( "InspectorProxyWorker" ); - ({ webSocket } = await inspectorProxyWorker.fetch( - "http://dummy/cdn-cgi/InspectorProxyWorker/websocket", + webSocket = new WebSocket( + `${inspectorProxyWorkerUrl.href}/cdn-cgi/InspectorProxyWorker/websocket`, { - headers: { Authorization: this.secret, Upgrade: "websocket" }, + headers: { Authorization: this.secret }, } - )); + ); } catch (cause) { if (this._torndown) { return; @@ -257,7 +262,8 @@ export class ProxyController extends Controller { void this.reconnectInspectorProxyWorker(); }); - webSocket.accept(); + await events.once(webSocket, "open"); + this.inspectorProxyWorkerWebSocket?.resolve(webSocket); return webSocket; diff --git a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts index 2fc3b525d13d..6d80edb7b0a7 100644 --- a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts @@ -73,7 +73,6 @@ export class RemoteRuntimeController extends RuntimeController { bindings: props.bindings, compatibilityDate: props.compatibilityDate, compatibilityFlags: props.compatibilityFlags, - usageModel: props.usageModel, }); const { workerAccount, workerContext } = await getWorkerAccountAndContext( @@ -83,7 +82,6 @@ export class RemoteRuntimeController extends RuntimeController { legacyEnv: props.legacyEnv, host: props.host, routes: props.routes, - sendMetrics: props.sendMetrics, } ); if (!this.#session) { @@ -147,7 +145,6 @@ export class RemoteRuntimeController extends RuntimeController { legacyEnv: !config.legacy?.enableServiceEnvironments, // wrangler environment -- just pass it through for now host: config.dev.origin?.hostname, routes, - sendMetrics: config.sendMetrics, }); const bindings = ( @@ -175,7 +172,6 @@ export class RemoteRuntimeController extends RuntimeController { bindings: bindings, compatibilityDate: config.compatibilityDate, compatibilityFlags: config.compatibilityFlags, - usageModel: config.usageModel, routes, }); diff --git a/packages/wrangler/src/api/startDevWorker/index.ts b/packages/wrangler/src/api/startDevWorker/index.ts index f339367eef55..7b968a72f619 100644 --- a/packages/wrangler/src/api/startDevWorker/index.ts +++ b/packages/wrangler/src/api/startDevWorker/index.ts @@ -1,11 +1,13 @@ import { DevEnv } from "./DevEnv"; -import type { StartDevWorkerOptions, Worker } from "./types"; +import type { StartDevWorkerInput, Worker } from "./types"; export { DevEnv }; export * from "./types"; export * from "./events"; -export function startWorker(options: StartDevWorkerOptions): Worker { +export async function startWorker( + options: StartDevWorkerInput +): Promise { const devEnv = new DevEnv(); return devEnv.startWorker(options); diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index b228baa7bee7..9a71f4be460f 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -52,7 +52,7 @@ export interface Worker { dispose(): Promise; } -export interface StartDevWorkerOptions { +export interface StartDevWorkerInput { /** The name of the worker. */ name?: string; /** @@ -60,11 +60,10 @@ export interface StartDevWorkerOptions { * This is the `main` property of a wrangler.toml. * You can specify a file path or provide the contents directly. */ - entrypoint?: FilePath; + entrypoint?: string; /** The configuration of the worker. */ - config?: FilePath; - /** A worker's directory. Usually where the wrangler.toml file is located */ - directory?: string; + config?: string; + /** The compatibility date for the workerd runtime. */ compatibilityDate?: string; /** The compatibility flags for the workerd runtime. */ @@ -84,7 +83,6 @@ export interface StartDevWorkerOptions { * Otherwise, Wrangler will use the user's preference. */ sendMetrics?: boolean; - usageModel?: "bundled" | "unbound"; /** Options applying to the worker's build step. Applies to deploy and dev. */ build?: { @@ -115,10 +113,10 @@ export interface StartDevWorkerOptions { jsxFactory?: string; jsxFragment?: string; tsconfig?: string; - nodejsCompatMode?: NodeJSCompatMode; - moduleRoot?: string; + // HACK: Resolving the nodejs compat mode is complex and fraught with backwards-compat concerns + nodejsCompatMode?: Hook; - format?: CfScriptFormat; + moduleRoot?: string; }; /** Options applying to the worker's development preview environment. */ @@ -130,7 +128,7 @@ export interface StartDevWorkerOptions { /** Cloudflare Account credentials. Can be provided upfront or as a function which will be called only when required. */ auth?: AsyncHook; /** Whether local storage (KV, Durable Objects, R2, D1, etc) is persisted. You can also specify the directory to persist data to. */ - persist?: boolean | { path: string }; + persist?: string; /** Controls which logs are logged 🤙. */ logLevel?: LogLevel; /** Whether the worker server restarts upon source/config file changes. */ @@ -159,14 +157,37 @@ export interface StartDevWorkerOptions { testScheduled?: boolean; }; legacy?: { - site?: Config["site"]; - assets?: Config["assets"]; + site?: Hook; + assets?: Hook; enableServiceEnvironments?: boolean; }; unsafe?: Omit; } -export type HookValues = string | number | boolean | object; +export type StartDevWorkerOptions = StartDevWorkerInput & { + /** A worker's directory. Usually where the wrangler.toml file is located */ + directory: string; + build: StartDevWorkerInput["build"] & { + nodejsCompatMode: NodeJSCompatMode; + format: CfScriptFormat; + moduleRoot: string; + moduleRules: Rule[]; + define: Record; + additionalModules: CfModule[]; + + processEntrypoint: boolean; + }; + legacy: StartDevWorkerInput["legacy"] & { + assets?: Config["assets"]; + site?: Config["site"]; + }; + dev: StartDevWorkerInput["dev"] & { + persist: string; + }; + entrypoint: string; +}; + +export type HookValues = string | number | boolean | object | undefined | null; export type Hook = | T | ((...args: Args) => T); diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index 5973cfeb9b8b..69fa1fc39f15 100644 --- a/packages/wrangler/src/api/startDevWorker/utils.ts +++ b/packages/wrangler/src/api/startDevWorker/utils.ts @@ -8,6 +8,7 @@ import type { Hook, HookValues, ServiceFetch, + StartDevWorkerInput, StartDevWorkerOptions, } from "./types"; @@ -55,11 +56,11 @@ export function urlFromParts( type UnwrapHook = H extends Hook ? T : never; export function unwrapHook< - H extends AsyncHook, + H extends AsyncHook | undefined, T extends HookValues = UnwrapHook, Args extends unknown[] = [], >( - hook: H | undefined, + hook: H, ...args: Args ): H extends undefined ? UnwrapHook | undefined : UnwrapHook { return typeof hook === "function" ? hook(...args) : hook; @@ -386,8 +387,30 @@ export function extractBindingsOfType< >( type: Type, bindings: StartDevWorkerOptions["bindings"] -): Extract[] { - return Object.values(bindings ?? {}).filter( - (b): b is Extract => b.type === type - ); +): (Extract & { + binding: string; + /* ugh why durable objects :( */ name: string; +})[] { + return Object.entries(bindings ?? {}) + .filter( + (binding): binding is [string, Extract] => + binding[1].type === type + ) + .map((binding) => ({ + ...binding[1], + binding: binding[0], + name: binding[0], + })) as (Extract & { + binding: string; + /* ugh why durable objects :( */ name: string; + })[]; +} + +// DO NOT USE! +// StartDevWorkerInput and StartDevWorkerOptions are not generally assignable to each other, but they're assignable _enough_ to make the faking of events work when --x-dev-env is turned off +// Typescript needs some help to figure this out though +export function fakeResolvedInput( + input: StartDevWorkerInput +): StartDevWorkerOptions { + return input as StartDevWorkerOptions; } diff --git a/packages/wrangler/src/cli.ts b/packages/wrangler/src/cli.ts index 222574e48ddd..eea6469827db 100644 --- a/packages/wrangler/src/cli.ts +++ b/packages/wrangler/src/cli.ts @@ -1,6 +1,11 @@ import process from "process"; import { hideBin } from "yargs/helpers"; -import { unstable_dev, DevEnv as unstable_DevEnv, unstable_pages } from "./api"; +import { + unstable_dev, + DevEnv as unstable_DevEnv, + unstable_pages, + startWorker as unstable_startWorker, +} from "./api"; import { FatalError } from "./errors"; import { main } from "."; import type { UnstableDevOptions, UnstableDevWorker } from "./api"; @@ -26,7 +31,7 @@ if (typeof vitest === "undefined" && require.main === module) { * It makes it possible to import wrangler from 'wrangler', * and call wrangler.unstable_dev(). */ -export { unstable_dev, unstable_pages, unstable_DevEnv }; +export { unstable_dev, unstable_pages, unstable_DevEnv, unstable_startWorker }; export type { UnstableDevWorker, UnstableDevOptions }; export * from "./api/integrations"; diff --git a/packages/wrangler/src/config/index.ts b/packages/wrangler/src/config/index.ts index 4f0dbd38e3c6..d1da1d564b10 100644 --- a/packages/wrangler/src/config/index.ts +++ b/packages/wrangler/src/config/index.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import dotenv from "dotenv"; import { findUpSync } from "find-up"; import { FatalError, UserError } from "../errors"; +import { getFlag } from "../experimental-flags"; import { logger } from "../logger"; import { EXIT_CODE_INVALID_PAGES_CONFIG } from "../pages/errors"; import { parseJSONC, parseTOML, readFileSync } from "../parse"; @@ -26,7 +27,7 @@ export type { } from "./environment"; type ReadConfigCommandArgs = NormalizeAndValidateConfigArgs & { - experimentalJsonConfig: boolean | undefined; + experimentalJsonConfig?: boolean | undefined; }; /** @@ -52,10 +53,12 @@ export function readConfig( requirePagesConfig?: boolean, hideWarnings: boolean = false ): Config { + const isJsonConfigEnabled = + getFlag("JSON_CONFIG_FILE") ?? args.experimentalJsonConfig; let rawConfig: RawConfig = {}; if (!configPath) { - configPath = findWranglerToml(process.cwd(), args.experimentalJsonConfig); + configPath = findWranglerToml(process.cwd(), isJsonConfigEnabled); } try { @@ -98,7 +101,7 @@ export function readConfig( } if ( isPagesConfigFile && - (configPath?.endsWith("json") || args.experimentalJsonConfig) + (configPath?.endsWith("json") || isJsonConfigEnabled) ) { throw new UserError( `Pages doesn't currently support JSON formatted config \`${ diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index 46137b98f17e..b366d00a88ae 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -43,7 +43,11 @@ import type { ValidatorFn } from "./validation-helpers"; export type NormalizeAndValidateConfigArgs = { env?: string; "legacy-env"?: boolean; + // This is not relevant in dev. It's only purpose is loosening Worker name validation when deploying to a dispatch namespace "dispatch-namespace"?: string; + remote?: boolean; + localProtocol?: string; + upstreamProtocol?: string; }; const ENGLISH = new Intl.ListFormat("en-US"); @@ -457,7 +461,7 @@ function normalizeAndValidatePagesBuildOutputDir( function normalizeAndValidateDev( diagnostics: Diagnostics, rawDev: RawDevConfig, - args: Record + args: NormalizeAndValidateConfigArgs ): DevConfig { assert(typeof args === "object" && args !== null && !Array.isArray(args)); const { diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index a03020301fe0..de37f8e5f23e 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -668,7 +668,6 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m : undefined, compatibility_date: props.compatibilityDate ?? config.compatibility_date, compatibility_flags: compatibilityFlags, - usage_model: config.usage_model, keepVars, keepSecrets: keepVars, // keepVars implies keepSecrets logpush: props.logpush !== undefined ? props.logpush : config.logpush, diff --git a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts index 7dfd279ca25d..6e2adc1f13d9 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -160,7 +160,6 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData { bindings, rawBindings, migrations, - usage_model, compatibility_date, compatibility_flags, keepVars, @@ -537,7 +536,6 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData { bindings: metadataBindings, ...(compatibility_date && { compatibility_date }), ...(compatibility_flags && { compatibility_flags }), - ...(usage_model && { usage_model }), ...(migrations && { migrations }), capnp_schema: capnpSchemaOutputFile, ...(keep_bindings && { keep_bindings }), diff --git a/packages/wrangler/src/deployment-bundle/entry.ts b/packages/wrangler/src/deployment-bundle/entry.ts index 0f0ddc233e99..76928e9747bc 100644 --- a/packages/wrangler/src/deployment-bundle/entry.ts +++ b/packages/wrangler/src/deployment-bundle/entry.ts @@ -36,7 +36,7 @@ export async function getEntry( args: { script?: string; format?: CfScriptFormat | undefined; - assets?: string | undefined; + assets?: string | undefined | boolean; moduleRoot?: string; }, config: Config, diff --git a/packages/wrangler/src/deployment-bundle/worker.ts b/packages/wrangler/src/deployment-bundle/worker.ts index 9c13c61f7ece..cd2483540ab8 100644 --- a/packages/wrangler/src/deployment-bundle/worker.ts +++ b/packages/wrangler/src/deployment-bundle/worker.ts @@ -167,7 +167,7 @@ export interface CfR2Bucket { export interface CfD1Database { binding: string; database_id: string; - database_name?: string; + database_name: string; preview_database_id?: string; database_internal_env?: string; migrations_table?: string; @@ -334,7 +334,6 @@ export interface CfWorkerInit { migrations: CfDurableObjectMigrations | undefined; compatibility_date: string | undefined; compatibility_flags: string[] | undefined; - usage_model: "bundled" | "unbound" | undefined; keepVars: boolean | undefined; keepSecrets: boolean | undefined; keepBindings?: WorkerMetadata["keep_bindings"]; diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index a89f05baf4e0..1b2a1f0df080 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -4,10 +4,12 @@ import path from "node:path"; import util from "node:util"; import { isWebContainer } from "@webcontainer/env"; import { watch } from "chokidar"; -import getPort from "get-port"; import { render } from "ink"; import { DevEnv } from "./api"; -import { extractBindingsOfType } from "./api/startDevWorker/utils"; +import { + convertCfWorkerInitBindingstoBindings, + extractBindingsOfType, +} from "./api/startDevWorker/utils"; import { findWranglerToml, printBindings, readConfig } from "./config"; import { getEntry } from "./deployment-bundle/entry"; import { validateNodeCompat } from "./deployment-bundle/node-compat"; @@ -22,8 +24,17 @@ import { run } from "./experimental-flags"; import { logger } from "./logger"; import * as metrics from "./metrics"; import { getAssetPaths, getSiteAssetPaths } from "./sites"; -import { getAccountFromCache, loginOrRefreshIfRequired } from "./user"; -import { collectKeyValues } from "./utils/collectKeyValues"; +import { + getAccountFromCache, + getAccountId, + loginOrRefreshIfRequired, + requireApiToken, +} from "./user"; +import { + collectKeyValues, + collectPlainTextVars, +} from "./utils/collectKeyValues"; +import { memoizeGetPort } from "./utils/memoizeGetPort"; import { mergeWithOverride } from "./utils/mergeWithOverride"; import { getHostFromRoute, getZoneIdForPreview } from "./zones"; import { @@ -35,7 +46,13 @@ import { isLegacyEnv, printWranglerBanner, } from "./index"; -import type { ProxyData, ReadyEvent, ReloadCompleteEvent } from "./api"; +import type { + ProxyData, + ReadyEvent, + ReloadCompleteEvent, + StartDevWorkerInput, + Trigger, +} from "./api"; import type { Config, Environment } from "./config"; import type { EnvironmentNonInheritable, @@ -349,12 +366,20 @@ This is currently not supported 😭, but we think that we'll get it to work soo { DEV_ENV: args.experimentalDevEnv, FILE_BASED_REGISTRY: args.experimentalRegistry, + JSON_CONFIG_FILE: Boolean(args.experimentalJsonConfig), }, () => startDev(args) ); - watcher = devInstance.watcher; - const { waitUntilExit } = devInstance.devReactElement; - await waitUntilExit(); + if (args.experimentalDevEnv) { + assert(devInstance instanceof DevEnv); + await events.once(devInstance, "teardown"); + } else { + assert(!(devInstance instanceof DevEnv)); + + watcher = devInstance.watcher; + const { waitUntilExit } = devInstance.devReactElement; + await waitUntilExit(); + } } finally { await watcher?.close(); } @@ -451,7 +476,7 @@ async function updateDevEnvRegistry( return; } - devEnv.config.patch({ + void devEnv.config.patch({ dev: { ...devEnv.config.latestConfig?.dev, registry: boundWorkers, @@ -478,13 +503,220 @@ export async function startDev(args: StartDevOptions) { ); } + if (args.inspect) { + //devtools are enabled by default, but we still need to disable them if the caller doesn't want them + logger.warn( + "Passing --inspect is unnecessary, now you can always connect to devtools." + ); + } + if (args.experimentalPublic) { + throw new UserError( + "The --experimental-public field has been renamed to --assets" + ); + } + + if (args.public) { + throw new UserError("The --public field has been renamed to --assets"); + } + + if (args.experimentalEnableLocalPersistence) { + logger.warn( + `--experimental-enable-local-persistence is deprecated.\n` + + `Move any existing data to .wrangler/state and use --persist, or\n` + + `use --persist-to=./wrangler-local-state to keep using the old path.` + ); + } + + if (args.assets) { + logger.warn( + "The --assets argument is experimental and may change or break at any time" + ); + } + const configPath = args.config || (args.script && findWranglerToml(path.dirname(args.script))); + + const devEnv = new DevEnv(); + + if (args.experimentalDevEnv) { + // The ProxyWorker will have a stable host and port, so only listen for the first update + devEnv.proxy.once("ready", async (event: ReadyEvent) => { + if (process.send) { + const url = await event.proxyWorker.ready; + + process.send( + JSON.stringify({ + event: "DEV_SERVER_READY", + ip: url.hostname, + port: parseInt(url.port), + }) + ); + } + }); + if (!args.disableDevRegistry) { + const teardownRegistryPromise = devRegistry((registry) => + updateDevEnvRegistry(devEnv, registry) + ); + devEnv.once("teardown", async () => { + const teardownRegistry = await teardownRegistryPromise; + await teardownRegistry(devEnv.config.latestConfig?.name); + }); + devEnv.runtimes.forEach((runtime) => { + runtime.on( + "reloadComplete", + async (reloadEvent: ReloadCompleteEvent) => { + if (!reloadEvent.config.dev?.remote) { + assert(devEnv.proxy.proxyWorker); + const url = await devEnv.proxy.proxyWorker.ready; + + await maybeRegisterLocalWorker( + url, + reloadEvent.config.name, + reloadEvent.proxyData.internalDurableObjects, + reloadEvent.proxyData.entrypointAddresses + ); + } + } + ); + }); + } + await devEnv.config.set({ + name: args.name, + config: configPath, + entrypoint: args.script, + compatibilityDate: args.compatibilityDate, + compatibilityFlags: args.compatibilityFlags, + triggers: args.routes?.map>( + (r) => ({ + type: "route", + pattern: r, + }) + ), + + build: { + bundle: args.bundle !== undefined ? args.bundle : undefined, + define: collectKeyValues(args.define), + jsxFactory: args.jsxFactory, + jsxFragment: args.jsxFragment, + tsconfig: args.tsconfig, + minify: args.minify, + processEntrypoint: args.processEntrypoint, + additionalModules: args.additionalModules, + moduleRoot: args.moduleRoot, + moduleRules: args.rules, + nodejsCompatMode: (parsedConfig: Config) => + validateNodeCompat({ + legacyNodeCompat: + args.nodeCompat ?? parsedConfig.node_compat ?? false, + compatibilityFlags: + args.compatibilityFlags ?? + parsedConfig.compatibility_flags ?? + [], + noBundle: args.noBundle ?? parsedConfig.no_bundle ?? false, + }), + }, + bindings: { + ...collectPlainTextVars(args.var), + ...convertCfWorkerInitBindingstoBindings({ + kv_namespaces: args.kv, + vars: args.vars, + send_email: undefined, + wasm_modules: undefined, + text_blobs: undefined, + browser: undefined, + ai: args.ai, + version_metadata: args.version_metadata, + data_blobs: undefined, + durable_objects: { bindings: args.durableObjects ?? [] }, + queues: undefined, + r2_buckets: args.r2, + d1_databases: args.d1Databases, + vectorize: undefined, + constellation: args.constellation, + hyperdrive: undefined, + services: args.services, + analytics_engine_datasets: undefined, + dispatch_namespaces: undefined, + mtls_certificates: undefined, + logfwdr: undefined, + unsafe: undefined, + }), + }, + dev: { + auth: async () => { + return { + accountId: args.accountId ?? (await getAccountId()), + apiToken: requireApiToken(), + }; + }, + remote: !args.forceLocal && args.remote, + server: { + hostname: args.ip, + port: args.port, + secure: + args.localProtocol === undefined + ? undefined + : args.localProtocol === "https", + httpsCertPath: args.httpsCertPath, + httpsKeyPath: args.httpsKeyPath, + }, + inspector: { + port: args.inspectorPort, + }, + origin: { + hostname: args.host ?? args.localUpstream, + secure: + args.upstreamProtocol === undefined + ? undefined + : args.upstreamProtocol === "https", + }, + persist: args.persistTo, + liveReload: args.liveReload, + testScheduled: args.testScheduled, + logLevel: args.logLevel, + registry: devEnv.config.latestConfig?.dev.registry, + }, + legacy: { + site: (config) => { + const assetPaths = getResolvedAssetPaths(args, config); + + return Boolean(args.site || config.site) && assetPaths + ? { + bucket: path.join( + assetPaths.baseDirectory, + assetPaths?.assetDirectory + ), + include: assetPaths.includePatterns, + exclude: assetPaths.excludePatterns, + } + : undefined; + }, + assets: (config) => config.assets, + enableServiceEnvironments: !(args.legacyEnv ?? true), + }, + } satisfies StartDevWorkerInput); + + void metrics.sendMetricsEvent( + "run dev", + { + local: !args.remote, + usesTypeScript: /\.tsx?$/.test( + devEnv.config.latestConfig?.entrypoint as string + ), + }, + { + sendMetrics: devEnv.config.latestConfig?.sendMetrics, + offline: !args.remote, + } + ); + + return devEnv; + } const projectRoot = configPath && path.dirname(configPath); let config = readConfig(configPath, args); - if (config.configPath) { + if (config.configPath && !args.experimentalDevEnv) { watcher = watch(config.configPath, { persistent: true, }).on("change", async (_event) => { @@ -519,8 +751,7 @@ export async function startDev(args: StartDevOptions) { args.compatibilityFlags ?? config.compatibility_flags ?? [], noBundle: args.noBundle ?? config.no_bundle ?? false, }); - - await metrics.sendMetricsEvent( + void metrics.sendMetricsEvent( "run dev", { local: !args.remote, @@ -529,52 +760,6 @@ export async function startDev(args: StartDevOptions) { { sendMetrics: config.send_metrics, offline: !args.remote } ); - const devEnv = new DevEnv(); - - if (args.experimentalDevEnv) { - const teardownRegistryPromise = devRegistry((registry) => - updateDevEnvRegistry(devEnv, registry) - ); - devEnv.once("teardown", async () => { - const teardownRegistry = await teardownRegistryPromise; - await teardownRegistry(devEnv.config.latestConfig?.name); - }); - // The ProxyWorker will have a stable host and port, so only listen for the first update - devEnv.proxy.once("ready", async (event: ReadyEvent) => { - if (process.send) { - const url = await event.proxyWorker.ready; - - process.send( - JSON.stringify({ - event: "DEV_SERVER_READY", - ip: url.hostname, - port: parseInt(url.port), - }) - ); - } - }); - if (!args.disableDevRegistry) { - devEnv.runtimes.forEach((runtime) => { - runtime.on( - "reloadComplete", - async (reloadEvent: ReloadCompleteEvent) => { - if (!reloadEvent.config.dev?.remote) { - assert(devEnv.proxy.proxyWorker); - const url = await devEnv.proxy.proxyWorker.ready; - - await maybeRegisterLocalWorker( - url, - reloadEvent.config.name, - reloadEvent.proxyData.internalDurableObjects, - reloadEvent.proxyData.entrypointAddresses - ); - } - } - ); - }); - } - } - // eslint-disable-next-line no-inner-declarations async function getDevReactElement(configParam: Config) { const { assetPaths, bindings } = getBindingsAndAssetPaths( @@ -649,7 +834,6 @@ export async function startDev(args: StartDevOptions) { sendMetrics={configParam.send_metrics} testScheduled={args.testScheduled} projectRoot={projectRoot} - experimentalDevEnv={args.experimentalDevEnv} rawArgs={args} rawConfig={configParam} devEnv={devEnv} @@ -821,6 +1005,7 @@ export async function startApiDev(args: StartDevOptions) { { DEV_ENV: args.experimentalDevEnv, FILE_BASED_REGISTRY: args.experimentalRegistry, + JSON_CONFIG_FILE: Boolean(args.experimentalJsonConfig), }, () => getDevServer(config) ); @@ -836,24 +1021,15 @@ export async function startApiDev(args: StartDevOptions) { }, }; } -/** - * Get an available TCP port number. - * - * Avoiding calling `getPort()` multiple times by memoizing the first result. - */ -function memoizeGetPort(defaultPort: number, host: string) { - let portValue: number; - return async () => { - // Check a specific host to avoid probing all local addresses. - portValue = portValue ?? (await getPort({ port: defaultPort, host: host })); - return portValue; - }; -} + /** * mask anything that was overridden in .dev.vars * so that we don't log potential secrets into the terminal */ -function maskVars(bindings: CfWorkerInit["bindings"], configParam: Config) { +export function maskVars( + bindings: CfWorkerInit["bindings"], + configParam: Config +) { const maskedVars = { ...bindings.vars }; for (const key of Object.keys(maskedVars)) { if (maskedVars[key] !== configParam.vars[key]) { @@ -865,8 +1041,13 @@ function maskVars(bindings: CfWorkerInit["bindings"], configParam: Config) { return maskedVars; } -async function getHostAndRoutes( - args: Pick, +export async function getHostAndRoutes( + args: + | Pick + | { + host?: string; + routes?: Extract[]; + }, config: Pick & { dev: Pick; } @@ -874,8 +1055,21 @@ async function getHostAndRoutes( // TODO: if worker_dev = false and no routes, then error (only for dev) // Compute zone info from the `host` and `route` args and config; const host = args.host || config.dev.host; - const routes: Route[] | undefined = - args.routes || (config.route && [config.route]) || config.routes; + const routes: Route[] | undefined = ( + args.routes || + (config.route && [config.route]) || + config.routes + )?.map((r) => { + if (typeof r !== "object") { + return r; + } + if ("custom_domain" in r || "zone_id" in r || "zone_name" in r) { + return r; + } else { + // Make sure we map SDW SimpleRoute types { type: "route", pattern: string } to string + return r.pattern; + } + }); return { host, routes }; } @@ -948,33 +1142,12 @@ export async function validateDevServerSettings( ); } - if (args.inspect) { - //devtools are enabled by default, but we still need to disable them if the caller doesn't want them - logger.warn( - "Passing --inspect is unnecessary, now you can always connect to devtools." - ); - } - if (args.experimentalPublic) { - throw new UserError( - "The --experimental-public field has been renamed to --assets" - ); - } - - if (args.public) { - throw new UserError("The --public field has been renamed to --assets"); - } - if ((args.assets ?? config.assets) && (args.site ?? config.site)) { throw new UserError( "Cannot use Assets and Workers Sites in the same Worker." ); } - if (args.assets) { - logger.warn( - "The --assets argument is experimental and may change or break at any time" - ); - } const upstreamProtocol = args.upstreamProtocol ?? config.dev.upstream_protocol; if (upstreamProtocol === "http" && args.remote) { @@ -985,14 +1158,6 @@ export async function validateDevServerSettings( ); } - if (args.experimentalEnableLocalPersistence) { - logger.warn( - `--experimental-enable-local-persistence is deprecated.\n` + - `Move any existing data to .wrangler/state and use --persist, or\n` + - `use --persist-to=./wrangler-local-state to keep using the old path.` - ); - } - const localPersistencePath = getLocalPersistencePath( args.persistTo, config.configPath @@ -1017,7 +1182,7 @@ export async function validateDevServerSettings( }; } -export function getBindingsAndAssetPaths( +export function getResolvedBindings( args: StartDevOptions, configParam: Config ) { @@ -1042,6 +1207,13 @@ export function getBindingsAndAssetPaths( vars: maskedVars, }); + return bindings; +} + +export function getResolvedAssetPaths( + args: StartDevOptions, + configParam: Config +) { const assetPaths = args.assets || configParam.assets ? getAssetPaths(configParam, args.assets) @@ -1051,7 +1223,17 @@ export function getBindingsAndAssetPaths( args.siteInclude, args.siteExclude ); - return { assetPaths, bindings }; + return assetPaths; +} + +export function getBindingsAndAssetPaths( + args: StartDevOptions, + configParam: Config +) { + return { + bindings: getResolvedBindings(args, configParam), + assetPaths: getResolvedAssetPaths(args, configParam), + }; } export function getBindings( diff --git a/packages/wrangler/src/dev/dev.tsx b/packages/wrangler/src/dev/dev.tsx index 4a7b647afb63..dfc6ac212ee8 100644 --- a/packages/wrangler/src/dev/dev.tsx +++ b/packages/wrangler/src/dev/dev.tsx @@ -9,23 +9,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useErrorHandler, withErrorBoundary } from "react-error-boundary"; import onExit from "signal-exit"; import { fetch } from "undici"; -import { - getDevCompatibilityDate, - getRules, - getScriptName, - isLegacyEnv, -} from ".."; import { convertCfWorkerInitBindingstoBindings, createDeferred, + fakeResolvedInput, } from "../api/startDevWorker/utils"; -import { validateNodeCompat } from "../deployment-bundle/node-compat"; import { runCustomBuild } from "../deployment-bundle/run-custom-build"; -import { - getBindingsAndAssetPaths, - getInferredHost, - validateDevServerSettings, -} from "../dev"; import { getBoundRegisteredWorkers, getRegisteredWorkers, @@ -48,6 +37,7 @@ import type { DevEnv, ProxyData, ReloadCompleteEvent, + StartDevWorkerInput, StartDevWorkerOptions, Trigger, } from "../api"; @@ -264,147 +254,11 @@ export type DevProps = { sendMetrics: boolean | undefined; testScheduled: boolean | undefined; projectRoot: string | undefined; - experimentalDevEnv: boolean; rawConfig: Config; rawArgs: StartDevOptions; devEnv: DevEnv; }; -// TODO: use in finalisation or followup stage of SDW -async function _toSDW( - args: StartDevOptions, - accountId: Promise, - workerDefinitions: WorkerRegistry, - config: Config -): Promise { - const { - entry, - upstreamProtocol, - host, - routes, - getLocalPort, - getInspectorPort, - cliDefines, - localPersistencePath, - processEntrypoint, - additionalModules, - } = await validateDevServerSettings(args, config); - - const nodejsCompatMode = validateNodeCompat({ - legacyNodeCompat: args.nodeCompat ?? config.node_compat ?? false, - compatibilityFlags: - args.compatibilityFlags ?? config.compatibility_flags ?? [], - noBundle: args.noBundle ?? config.no_bundle ?? false, - }); - - const { assetPaths, bindings } = getBindingsAndAssetPaths(args, config); - - const devRoutes = - routes?.map>((r) => - typeof r === "string" - ? { - type: "route", - pattern: r, - } - : { type: "route", ...r } - ) ?? []; - const queueConsumers = - config.queues.consumers?.map>( - (c) => ({ - ...c, - type: "queue-consumer", - }) - ) ?? []; - - const crons = - config.triggers.crons?.map>((c) => ({ - cron: c, - type: "cron", - })) ?? []; - - return { - name: getScriptName({ name: args.name, env: args.env }, config), - compatibilityDate: getDevCompatibilityDate(config, args.compatibilityDate), - compatibilityFlags: args.compatibilityFlags || config.compatibility_flags, - entrypoint: { path: entry.file }, - directory: entry.directory, - bindings: convertCfWorkerInitBindingstoBindings(bindings), - - triggers: [...devRoutes, ...queueConsumers, ...crons], - env: args.env, - sendMetrics: config.send_metrics, - build: { - additionalModules: additionalModules, - processEntrypoint: processEntrypoint, - bundle: !(args.bundle ?? !config.no_bundle), - findAdditionalModules: config.find_additional_modules, - moduleRoot: entry.moduleRoot, - moduleRules: args.rules ?? getRules(config), - - minify: args.minify ?? config.minify, - define: { ...config.define, ...cliDefines }, - custom: { - command: (config.build || {}).command, - watch: (config.build || {}).watch_dir, - workingDirectory: (config.build || {}).cwd, - }, - format: entry.format, - nodejsCompatMode: nodejsCompatMode, - jsxFactory: args.jsxFactory || config.jsx_factory, - jsxFragment: args.jsxFragment || config.jsx_fragment, - tsconfig: args.tsconfig ?? config.tsconfig, - }, - dev: { - auth: async () => { - return { - accountId: await accountId, - apiToken: requireApiToken(), - }; - }, - remote: !args.local, - server: { - hostname: args.ip || config.dev.ip, - port: args.port ?? config.dev.port ?? (await getLocalPort()), - secure: (args.localProtocol || config.dev.local_protocol) === "https", - httpsKeyPath: args.httpsKeyPath, - httpsCertPath: args.httpsCertPath, - }, - inspector: { - port: - args.inspectorPort ?? - config.dev.inspector_port ?? - (await getInspectorPort()), - }, - origin: { - secure: upstreamProtocol === "https", - hostname: args.localUpstream ?? host ?? getInferredHost(routes), - }, - liveReload: args.liveReload || false, - testScheduled: args.testScheduled, - registry: workerDefinitions, - persist: { path: localPersistencePath }, - }, - legacy: { - site: - Boolean(args.site || config.site) && assetPaths - ? { - bucket: path.join( - assetPaths.baseDirectory, - assetPaths?.assetDirectory - ), - include: assetPaths.includePatterns, - exclude: assetPaths.excludePatterns, - } - : undefined, - assets: config.assets, - enableServiceEnvironments: !isLegacyEnv(config), - }, - unsafe: { - capnp: bindings.unsafe?.capnp, - metadata: bindings.unsafe?.metadata, - }, - } satisfies StartDevWorkerOptions; -} export function DevImplementation(props: DevProps): JSX.Element { validateDevProps(props); @@ -520,17 +374,14 @@ function DevSession(props: DevSessionProps) { }; }, [devEnv]); - const workerDefinitions = props.experimentalDevEnv - ? undefined - : // eslint-disable-next-line react-hooks/rules-of-hooks - useDevRegistry( - props.name, - props.bindings.services, - props.bindings.durable_objects, - props.local ? "local" : "remote" - ); + const workerDefinitions = useDevRegistry( + props.name, + props.bindings.services, + props.bindings.durable_objects, + props.local ? "local" : "remote" + ); - const startDevWorkerOptions: StartDevWorkerOptions = useMemo(() => { + const startDevWorkerOptions: StartDevWorkerInput = useMemo(() => { const routes = props.routes?.map>((r) => typeof r === "string" @@ -557,13 +408,12 @@ function DevSession(props: DevSessionProps) { name: props.name ?? "worker", compatibilityDate: props.compatibilityDate, compatibilityFlags: props.compatibilityFlags, - entrypoint: { path: props.entry.file }, + entrypoint: props.entry.file, directory: props.entry.directory, bindings: convertCfWorkerInitBindingstoBindings(props.bindings), triggers: [...routes, ...queueConsumers, ...crons], env: props.env, - sendMetrics: props.sendMetrics, build: { additionalModules: props.additionalModules, processEntrypoint: props.processEntrypoint, @@ -580,7 +430,7 @@ function DevSession(props: DevSessionProps) { jsxFactory: props.jsxFactory, jsxFragment: props.jsxFragment, tsconfig: props.tsconfig, - nodejsCompatMode: props.nodejsCompatMode, + nodejsCompatMode: props.nodejsCompatMode ?? null, format: props.entry.format, moduleRoot: props.entry.moduleRoot, }, @@ -608,6 +458,7 @@ function DevSession(props: DevSessionProps) { }, liveReload: props.liveReload, testScheduled: props.testScheduled, + persist: "", }, legacy: { site: @@ -646,7 +497,6 @@ function DevSession(props: DevSessionProps) { props.additionalModules, props.env, props.legacyEnv, - props.sendMetrics, props.noBundle, props.findAdditionalModules, props.minify, @@ -672,24 +522,20 @@ function DevSession(props: DevSessionProps) { ]); const onBundleStart = useCallback(() => { - if (!props.experimentalDevEnv) { - devEnv.proxy.onBundleStart({ - type: "bundleStart", - config: startDevWorkerOptions, - }); - } - }, [devEnv, startDevWorkerOptions, props.experimentalDevEnv]); + devEnv.proxy.onBundleStart({ + type: "bundleStart", + config: fakeResolvedInput(startDevWorkerOptions), + }); + }, [devEnv, startDevWorkerOptions]); const onBundleComplete = useCallback( (bundle: EsbuildBundle) => { - if (!props.experimentalDevEnv) { - devEnv.proxy.onReloadStart({ - type: "reloadStart", - config: startDevWorkerOptions, - bundle, - }); - } + devEnv.proxy.onReloadStart({ + type: "reloadStart", + config: fakeResolvedInput(startDevWorkerOptions), + bundle, + }); }, - [devEnv, startDevWorkerOptions, props.experimentalDevEnv] + [devEnv, startDevWorkerOptions] ); const esbuildStartTimeoutRef = useRef>(); const latestReloadCompleteEvent = useRef(); @@ -727,64 +573,47 @@ function DevSession(props: DevSessionProps) { const directory = useTmpDir(props.projectRoot); useEffect(() => { - if (!props.experimentalDevEnv) { - // temp: fake these events by calling the handler directly - devEnv.proxy.onConfigUpdate({ - type: "configUpdate", - config: startDevWorkerOptions, - }); - } else { - // Ensure we preserve `getRegisteredWorker`, which is set with `DevEnv.config.patch()` elsewhere - devEnv.config.set({ - ...startDevWorkerOptions, - dev: { - ...startDevWorkerOptions.dev, - registry: devEnv.config.latestConfig?.dev?.registry, - }, - }); - } - }, [devEnv, startDevWorkerOptions, props.experimentalDevEnv]); - - if (!props.experimentalDevEnv) { - // eslint-disable-next-line react-hooks/rules-of-hooks - useCustomBuild(props.entry, props.build, onBundleStart, onCustomBuildEnd); - } - - const bundle = props.experimentalDevEnv - ? undefined - : // eslint-disable-next-line react-hooks/rules-of-hooks - useEsbuild({ - entry: props.entry, - destination: directory, - jsxFactory: props.jsxFactory, - processEntrypoint: props.processEntrypoint, - additionalModules: props.additionalModules, - rules: props.rules, - jsxFragment: props.jsxFragment, - serveAssetsFromWorker: Boolean( - props.assetPaths && !props.isWorkersSite && props.local - ), - tsconfig: props.tsconfig, - minify: props.minify, - nodejsCompatMode: props.nodejsCompatMode, - define: props.define, - alias: props.alias, - noBundle: props.noBundle, - findAdditionalModules: props.findAdditionalModules, - assets: props.assetsConfig, - durableObjects: props.bindings.durable_objects || { bindings: [] }, - local: props.local, - // Enable the bundling to know whether we are using dev or deploy - targetConsumer: "dev", - testScheduled: props.testScheduled ?? false, - projectRoot: props.projectRoot, - onStart: onEsbuildStart, - onComplete: onBundleComplete, - defineNavigatorUserAgent: isNavigatorDefined( - props.compatibilityDate, - props.compatibilityFlags - ), - }); + // temp: fake these events by calling the handler directly + devEnv.proxy.onConfigUpdate({ + type: "configUpdate", + config: fakeResolvedInput(startDevWorkerOptions), + }); + }, [devEnv, startDevWorkerOptions]); + + useCustomBuild(props.entry, props.build, onBundleStart, onCustomBuildEnd); + + const bundle = useEsbuild({ + entry: props.entry, + destination: directory, + jsxFactory: props.jsxFactory, + processEntrypoint: props.processEntrypoint, + additionalModules: props.additionalModules, + rules: props.rules, + jsxFragment: props.jsxFragment, + serveAssetsFromWorker: Boolean( + props.assetPaths && !props.isWorkersSite && props.local + ), + tsconfig: props.tsconfig, + minify: props.minify, + nodejsCompatMode: props.nodejsCompatMode, + define: props.define, + alias: props.alias, + noBundle: props.noBundle, + findAdditionalModules: props.findAdditionalModules, + assets: props.assetsConfig, + durableObjects: props.bindings.durable_objects || { bindings: [] }, + local: props.local, + // Enable the bundling to know whether we are using dev or deploy + targetConsumer: "dev", + testScheduled: props.testScheduled ?? false, + projectRoot: props.projectRoot, + onStart: onEsbuildStart, + onComplete: onBundleComplete, + defineNavigatorUserAgent: isNavigatorDefined( + props.compatibilityDate, + props.compatibilityFlags + ), + }); // TODO(queues) support remote wrangler dev if ( @@ -831,7 +660,7 @@ function DevSession(props: DevSessionProps) { if (bundle) { latestReloadCompleteEvent.current = { type: "reloadComplete", - config: startDevWorkerOptions, + config: fakeResolvedInput(startDevWorkerOptions), bundle, proxyData, }; @@ -874,7 +703,6 @@ function DevSession(props: DevSessionProps) { enablePagesAssetsServiceBinding={props.enablePagesAssetsServiceBinding} sourceMapPath={bundle?.sourceMapPath} services={props.bindings.services} - experimentalDevEnv={props.experimentalDevEnv} /> ) : ( ); } diff --git a/packages/wrangler/src/dev/local.tsx b/packages/wrangler/src/dev/local.tsx index 8e3a3e1596e2..08f451bcd43c 100644 --- a/packages/wrangler/src/dev/local.tsx +++ b/packages/wrangler/src/dev/local.tsx @@ -53,7 +53,6 @@ export interface LocalProps { testScheduled?: boolean; sourceMapPath: string | undefined; services: Config["services"] | undefined; - experimentalDevEnv: boolean; } // TODO(soon): we should be able to remove this function when we fully migrate @@ -162,11 +161,7 @@ export function Local(props: LocalProps) { } }, [props.bindings.durable_objects?.bindings]); - if (!props.experimentalDevEnv) { - // this condition WILL be static and therefore safe to wrap around a hook - // eslint-disable-next-line react-hooks/rules-of-hooks - useLocalWorker(props); - } + useLocalWorker(props); return null; } diff --git a/packages/wrangler/src/dev/remote.tsx b/packages/wrangler/src/dev/remote.tsx index 80083b8bdd14..dee3bd285c06 100644 --- a/packages/wrangler/src/dev/remote.tsx +++ b/packages/wrangler/src/dev/remote.tsx @@ -128,37 +128,32 @@ interface RemoteProps { sendMetrics: boolean | undefined; setAccountId: (accountId: string) => void; - experimentalDevEnv: boolean; } export function Remote(props: RemoteProps) { const accountChoicesRef = useRef>(); const [accountChoices, setAccountChoices] = useState(); - if (!props.experimentalDevEnv) { - // this condition WILL be static and therefore safe to wrap around a hook - // eslint-disable-next-line react-hooks/rules-of-hooks - useWorker({ - name: props.name, - bundle: props.bundle, - format: props.format, - modules: props.bundle ? props.bundle.modules : [], - accountId: props.accountId, - bindings: props.bindings, - assetPaths: props.assetPaths, - isWorkersSite: props.isWorkersSite, - compatibilityDate: props.compatibilityDate, - compatibilityFlags: props.compatibilityFlags, - usageModel: props.usageModel, - env: props.env, - legacyEnv: props.legacyEnv, - host: props.host, - routes: props.routes, - onReady: props.onReady, - sendMetrics: props.sendMetrics, - port: props.port, - }); - } + useWorker({ + name: props.name, + bundle: props.bundle, + format: props.format, + modules: props.bundle ? props.bundle.modules : [], + accountId: props.accountId, + bindings: props.bindings, + assetPaths: props.assetPaths, + isWorkersSite: props.isWorkersSite, + compatibilityDate: props.compatibilityDate, + compatibilityFlags: props.compatibilityFlags, + usageModel: props.usageModel, + env: props.env, + legacyEnv: props.legacyEnv, + host: props.host, + routes: props.routes, + onReady: props.onReady, + sendMetrics: props.sendMetrics, + port: props.port, + }); const errorHandler = useErrorHandler(); @@ -319,7 +314,6 @@ export function useWorker( bindings: props.bindings, compatibilityDate: props.compatibilityDate, compatibilityFlags: props.compatibilityFlags, - usageModel: props.usageModel, }); const { workerAccount, workerContext } = await getWorkerAccountAndContext( @@ -438,7 +432,9 @@ export function useWorker( return token; } -export async function startRemoteServer(props: RemoteProps) { +export async function startRemoteServer( + props: RemoteProps & { experimentalDevEnv: boolean } +) { let accountId = props.accountId; if (accountId === undefined) { const accountChoices = await getAccountChoices(); @@ -567,7 +563,6 @@ export async function getRemotePreviewToken(props: RemoteProps) { bindings: props.bindings, compatibilityDate: props.compatibilityDate, compatibilityFlags: props.compatibilityFlags, - usageModel: props.usageModel, }); const workerPreviewToken = await createWorkerPreview( init, @@ -608,7 +603,6 @@ export async function createRemoteWorkerInit(props: { bindings: CfWorkerInit["bindings"]; compatibilityDate: string | undefined; compatibilityFlags: string[] | undefined; - usageModel: "bundled" | "unbound" | undefined; }) { const { entrypointSource: content, modules } = withSourceURLs( props.bundle.path, @@ -674,7 +668,6 @@ export async function createRemoteWorkerInit(props: { migrations: undefined, // no migrations in dev compatibility_date: props.compatibilityDate, compatibility_flags: props.compatibilityFlags, - usage_model: props.usageModel, keepVars: true, keepSecrets: true, logpush: false, diff --git a/packages/wrangler/src/dev/start-server.ts b/packages/wrangler/src/dev/start-server.ts index 1b0e4061bb69..16e5001bac24 100644 --- a/packages/wrangler/src/dev/start-server.ts +++ b/packages/wrangler/src/dev/start-server.ts @@ -3,6 +3,7 @@ import * as path from "node:path"; import * as util from "node:util"; import chalk from "chalk"; import onExit from "signal-exit"; +import { fakeResolvedInput } from "../api/startDevWorker/utils"; import { bundleWorker } from "../deployment-bundle/bundle"; import { getBundleType } from "../deployment-bundle/bundle-type"; import { dedupeModulesByName } from "../deployment-bundle/dedupe-modules"; @@ -30,7 +31,7 @@ import { localPropsToConfigBundle, maybeRegisterLocalWorker } from "./local"; import { DEFAULT_WORKER_NAME, MiniflareServer } from "./miniflare"; import { startRemoteServer } from "./remote"; import { validateDevProps } from "./validate-dev-props"; -import type { ProxyData, StartDevWorkerOptions } from "../api"; +import type { ProxyData, StartDevWorkerInput } from "../api"; import type { Config } from "../config"; import type { DurableObjectBindings } from "../config/environment"; import type { Entry } from "../deployment-bundle/entry"; @@ -45,6 +46,7 @@ export async function startDevServer( props: DevProps & { local: boolean; disableDevRegistry: boolean; + experimentalDevEnv: boolean; } ) { let workerDefinitions: WorkerRegistry = {}; @@ -90,10 +92,9 @@ export async function startDevServer( } const devEnv = props.devEnv; - const startDevWorkerOptions: StartDevWorkerOptions = { + const startDevWorkerOptions: StartDevWorkerInput = { name: props.name ?? "worker", - entrypoint: { path: props.entry.file }, - directory: props.entry.directory, + entrypoint: props.entry.file, dev: { server: { hostname: props.initialIp, @@ -132,8 +133,9 @@ export async function startDevServer( }, }, build: { - format: props.entry.format, + // format: props.entry.format, moduleRoot: props.entry.moduleRoot, + nodejsCompatMode: null, }, }; @@ -141,11 +143,11 @@ export async function startDevServer( if (!props.experimentalDevEnv) { devEnv.proxy.onConfigUpdate({ type: "configUpdate", - config: startDevWorkerOptions, + config: fakeResolvedInput(startDevWorkerOptions), }); devEnv.proxy.onBundleStart({ type: "bundleStart", - config: startDevWorkerOptions, + config: fakeResolvedInput(startDevWorkerOptions), }); } @@ -195,7 +197,7 @@ export async function startDevServer( // temp: fake these events by calling the handler directly devEnv.proxy.onReloadStart({ type: "reloadStart", - config: startDevWorkerOptions, + config: fakeResolvedInput(startDevWorkerOptions), bundle, }); @@ -243,7 +245,7 @@ export async function startDevServer( if (!props.experimentalDevEnv) { devEnv.proxy.onReloadComplete({ type: "reloadComplete", - config: startDevWorkerOptions, + config: fakeResolvedInput(startDevWorkerOptions), bundle, proxyData, }); @@ -303,7 +305,7 @@ export async function startDevServer( if (!props.experimentalDevEnv) { devEnv.proxy.onReloadComplete({ type: "reloadComplete", - config: startDevWorkerOptions, + config: fakeResolvedInput(startDevWorkerOptions), bundle, proxyData, }); @@ -443,7 +445,7 @@ async function runEsbuild({ } export async function startLocalServer( - props: LocalProps + props: LocalProps & { experimentalDevEnv: boolean } ): Promise<{ stop: () => Promise }> { if (!props.bundle || !props.format) { return { async stop() {} }; diff --git a/packages/wrangler/src/experimental-flags.ts b/packages/wrangler/src/experimental-flags.ts index 319f288d385f..b565f4de00cc 100644 --- a/packages/wrangler/src/experimental-flags.ts +++ b/packages/wrangler/src/experimental-flags.ts @@ -5,6 +5,7 @@ type ExperimentalFlags = { // TODO: use this DEV_ENV: boolean; FILE_BASED_REGISTRY: boolean; + JSON_CONFIG_FILE: boolean; }; const flags = new AsyncLocalStorage(); diff --git a/packages/wrangler/src/secret/index.ts b/packages/wrangler/src/secret/index.ts index 07fa3abb53f1..b6231facbb02 100644 --- a/packages/wrangler/src/secret/index.ts +++ b/packages/wrangler/src/secret/index.ts @@ -104,7 +104,6 @@ async function createDraftWorker({ migrations: undefined, compatibility_date: undefined, compatibility_flags: undefined, - usage_model: undefined, keepVars: false, // this doesn't matter since it's a new script anyway keepSecrets: false, // this doesn't matter since it's a new script anyway logpush: false, diff --git a/packages/wrangler/src/tail/index.ts b/packages/wrangler/src/tail/index.ts index bbbb05cca939..35954feed1ff 100644 --- a/packages/wrangler/src/tail/index.ts +++ b/packages/wrangler/src/tail/index.ts @@ -22,7 +22,7 @@ import type { StrictYargsOptionsToInterface, } from "../yargs-types"; import type { TailCLIFilters } from "./createTail"; -import type { RawData } from "ws"; +import type WebSocket from "ws"; export function tailOptions(yargs: CommonYargsArgv) { return yargs @@ -147,7 +147,7 @@ export async function tailHandler(args: TailArgs) { ); } - const printLog: (data: RawData) => void = + const printLog: (data: WebSocket.RawData) => void = args.format === "pretty" ? prettyPrintLogs : jsonPrintLogs; tail.on("message", printLog); diff --git a/packages/wrangler/src/utils/collectKeyValues.ts b/packages/wrangler/src/utils/collectKeyValues.ts index 3086f982f2fa..b1fa2fcf4c9c 100644 --- a/packages/wrangler/src/utils/collectKeyValues.ts +++ b/packages/wrangler/src/utils/collectKeyValues.ts @@ -1,3 +1,5 @@ +import type { Binding } from "../api"; + /** * a function that takes an array of strings in `key:value` format * (typically from yargs or config) and returns back @@ -12,3 +14,18 @@ export function collectKeyValues(array?: string[]) { }, {}) || {} ); } + +export function collectPlainTextVars( + array?: string[] +): Record> { + return ( + array?.reduce>>( + (recordsToCollect, v) => { + const [key, ...value] = v.split(":"); + recordsToCollect[key] = { type: "plain_text", value: value.join(":") }; + return recordsToCollect; + }, + {} + ) || {} + ); +} diff --git a/packages/wrangler/src/utils/memoizeGetPort.ts b/packages/wrangler/src/utils/memoizeGetPort.ts new file mode 100644 index 000000000000..b716f3428f95 --- /dev/null +++ b/packages/wrangler/src/utils/memoizeGetPort.ts @@ -0,0 +1,20 @@ +import getPort from "get-port"; + +/** + * Get an available TCP port number. + * + * Avoiding calling `getPort()` multiple times by memoizing the first result. + */ +export function memoizeGetPort(defaultPort: number, host: string) { + let portValue: number | undefined; + let cachedHost = host; + return async (forHost: string = host) => { + if (forHost !== cachedHost) { + portValue = undefined; + cachedHost = forHost; + } + // Check a specific host to avoid probing all local addresses. + portValue = portValue ?? (await getPort({ port: defaultPort, host: host })); + return portValue; + }; +} diff --git a/packages/wrangler/src/versions/secrets/index.ts b/packages/wrangler/src/versions/secrets/index.ts index e1c785256d0f..c5cf3fbcaf3d 100644 --- a/packages/wrangler/src/versions/secrets/index.ts +++ b/packages/wrangler/src/versions/secrets/index.ts @@ -193,8 +193,6 @@ export async function copyWorkerVersionWithNewSecrets({ compatibility_date: versionInfo.resources.script_runtime.compatibility_date, compatibility_flags: versionInfo.resources.script_runtime.compatibility_flags, - usage_model: versionInfo.resources.script_runtime - .usage_model as CfWorkerInit["usage_model"], keepVars: false, // we're re-uploading everything keepSecrets: false, // handled in keepBindings keepBindings, diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index 4d06151b3585..72bbc78ccb77 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -380,7 +380,6 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m : undefined, compatibility_date: props.compatibilityDate ?? config.compatibility_date, compatibility_flags: compatibilityFlags, - usage_model: config.usage_model, keepVars: false, // the wrangler.toml should be the source-of-truth for vars keepSecrets: true, // until wrangler.toml specifies secret bindings, we need to inherit from the previous Worker Version logpush: undefined, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b48bfb7986e4..50e015865acd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1041,7 +1041,7 @@ importers: version: 2.0.2 '@types/ws': specifier: ^8.5.7 - version: 8.5.9 + version: 8.5.10 '@typescript-eslint/eslint-plugin': specifier: ^6.9.0 version: 6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2) @@ -3684,6 +3684,9 @@ packages: '@types/wrap-ansi@3.0.0': resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + '@types/ws@8.5.10': + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + '@types/ws@8.5.9': resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} @@ -10821,6 +10824,10 @@ snapshots: '@types/wrap-ansi@3.0.0': {} + '@types/ws@8.5.10': + dependencies: + '@types/node': 20.8.3 + '@types/ws@8.5.9': dependencies: '@types/node': 20.8.3