diff --git a/.changeset/curly-rice-travel.md b/.changeset/curly-rice-travel.md new file mode 100644 index 000000000000..05a806c0117f --- /dev/null +++ b/.changeset/curly-rice-travel.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +Fix wrangler pages deployment (list|tail) environment filtering. diff --git a/.changeset/cyan-geese-smell.md b/.changeset/cyan-geese-smell.md new file mode 100644 index 000000000000..1d3b0fbdbab8 --- /dev/null +++ b/.changeset/cyan-geese-smell.md @@ -0,0 +1,6 @@ +--- +"@cloudflare/workflows-shared": minor +"miniflare": minor +--- + +Add proper engine persistance in .wrangler and fix multiple workflows in miniflare diff --git a/.changeset/sixty-news-relax.md b/.changeset/sixty-news-relax.md new file mode 100644 index 000000000000..6944105069c3 --- /dev/null +++ b/.changeset/sixty-news-relax.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Rename `directory` to `projectRoot` and ensure it's relative to the `wrangler.toml`. This fixes a regression which meant that `.wrangler` temporary folders were inadvertently generated relative to `process.cwd()` rather than the location of the `wrangler.toml` file. It also renames `directory` to `projectRoot`, which affects the `unstable_startWorker() interface. diff --git a/fixtures/workflow-multiple/package.json b/fixtures/workflow-multiple/package.json new file mode 100644 index 000000000000..ca7184f54cc4 --- /dev/null +++ b/fixtures/workflow-multiple/package.json @@ -0,0 +1,17 @@ +{ + "name": "my-workflow-multiple", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev", + "test:ci": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20241106.0", + "undici": "catalog:default", + "wrangler": "workspace:*" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/fixtures/workflow-multiple/src/index.ts b/fixtures/workflow-multiple/src/index.ts new file mode 100644 index 000000000000..3498d07e2f24 --- /dev/null +++ b/fixtures/workflow-multiple/src/index.ts @@ -0,0 +1,90 @@ +import { + WorkerEntrypoint, + WorkflowEntrypoint, + WorkflowEvent, + WorkflowStep, +} from "cloudflare:workers"; + +type Params = { + name: string; +}; + +export class Demo extends WorkflowEntrypoint<{}, Params> { + async run(event: WorkflowEvent, step: WorkflowStep) { + const { timestamp, payload } = event; + + await step.sleep("Wait", "1 second"); + + const result = await step.do("First step", async function () { + return { + output: "First step result", + }; + }); + + await step.sleep("Wait", "1 second"); + + const result2 = await step.do("Second step", async function () { + return { + output: "workflow1", + }; + }); + + return [result, result2, timestamp, payload, "workflow1"]; + } +} + +export class Demo2 extends WorkflowEntrypoint<{}, Params> { + async run(event: WorkflowEvent, step: WorkflowStep) { + const { timestamp, payload } = event; + + await step.sleep("Wait", "1 second"); + + const result = await step.do("First step", async function () { + return { + output: "First step result", + }; + }); + + await step.sleep("Wait", "1 second"); + + const result2 = await step.do("Second step", async function () { + return { + output: "workflow2", + }; + }); + + return [result, result2, timestamp, payload, "workflow2"]; + } +} + +type Env = { + WORKFLOW: Workflow; + WORKFLOW2: Workflow; +}; + +export default class extends WorkerEntrypoint { + async fetch(req: Request) { + const url = new URL(req.url); + const id = url.searchParams.get("id"); + const workflowName = url.searchParams.get("workflowName"); + + if (url.pathname === "/favicon.ico") { + return new Response(null, { status: 404 }); + } + let workflowToUse = + workflowName == "2" ? this.env.WORKFLOW2 : this.env.WORKFLOW; + + let handle: WorkflowInstance; + if (url.pathname === "/create") { + if (id === null) { + handle = await workflowToUse.create(); + } else { + handle = await workflowToUse.create({ id }); + } + } else { + handle = await workflowToUse.get(id); + } + + return Response.json({ status: await handle.status(), id: handle.id }); + } +} diff --git a/fixtures/workflow-multiple/tests/index.test.ts b/fixtures/workflow-multiple/tests/index.test.ts new file mode 100644 index 000000000000..29268cd0ea29 --- /dev/null +++ b/fixtures/workflow-multiple/tests/index.test.ts @@ -0,0 +1,127 @@ +import { rm } from "fs/promises"; +import { resolve } from "path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it, vi } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Workflows", () => { + let ip: string, + port: number, + stop: (() => Promise) | undefined, + getOutput: () => string; + + beforeAll(async () => { + // delete previous run contents because of persistence + await rm(resolve(__dirname, "..") + "/.wrangler", { + force: true, + recursive: true, + }); + ({ ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, ".."), + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + async function fetchJson(url: string) { + const response = await fetch(url, { + headers: { + "MF-Disable-Pretty-Error": "1", + }, + }); + const text = await response.text(); + + try { + return JSON.parse(text); + } catch (err) { + throw new Error(`Couldn't parse JSON:\n\n${text}`); + } + } + + it("creates two instances with same id in two different workflows", async ({ + expect, + }) => { + const createResult = { + id: "test", + status: { + status: "running", + output: [], + }, + }; + + await Promise.all([ + expect( + fetchJson(`http://${ip}:${port}/create?workflowName=1&id=test`) + ).resolves.toStrictEqual(createResult), + expect( + fetchJson(`http://${ip}:${port}/create?workflowName=2&id=test`) + ).resolves.toStrictEqual(createResult), + ]); + + const firstResult = { + id: "test", + status: { + status: "running", + output: [{ output: "First step result" }], + }, + }; + await Promise.all([ + vi.waitFor( + async () => { + await expect( + fetchJson(`http://${ip}:${port}/status?workflowName=1&id=test`) + ).resolves.toStrictEqual(firstResult); + }, + { timeout: 5000 } + ), + vi.waitFor( + async () => { + await expect( + fetchJson(`http://${ip}:${port}/status?workflowName=2&id=test`) + ).resolves.toStrictEqual(firstResult); + }, + { timeout: 5000 } + ), + ]); + + await Promise.all([ + await vi.waitFor( + async () => { + await expect( + fetchJson(`http://${ip}:${port}/status?workflowName=1&id=test`) + ).resolves.toStrictEqual({ + id: "test", + status: { + status: "complete", + output: [ + { output: "First step result" }, + { output: "workflow1" }, + ], + }, + }); + }, + { timeout: 5000 } + ), + await vi.waitFor( + async () => { + await expect( + fetchJson(`http://${ip}:${port}/status?workflowName=2&id=test`) + ).resolves.toStrictEqual({ + id: "test", + status: { + status: "complete", + output: [ + { output: "First step result" }, + { output: "workflow2" }, + ], + }, + }); + }, + { timeout: 5000 } + ), + ]); + }); +}); diff --git a/fixtures/workflow-multiple/tests/tsconfig.json b/fixtures/workflow-multiple/tests/tsconfig.json new file mode 100644 index 000000000000..d2ce7f144694 --- /dev/null +++ b/fixtures/workflow-multiple/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts", "../../../node-types.d.ts"] +} diff --git a/fixtures/workflow-multiple/tsconfig.json b/fixtures/workflow-multiple/tsconfig.json new file mode 100644 index 000000000000..856398634a5e --- /dev/null +++ b/fixtures/workflow-multiple/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/workflow-multiple/vitest.config.mts b/fixtures/workflow-multiple/vitest.config.mts new file mode 100644 index 000000000000..846cddc41995 --- /dev/null +++ b/fixtures/workflow-multiple/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/workflow-multiple/wrangler.toml b/fixtures/workflow-multiple/wrangler.toml new file mode 100644 index 000000000000..c984cab94231 --- /dev/null +++ b/fixtures/workflow-multiple/wrangler.toml @@ -0,0 +1,14 @@ +#:schema node_modules/wrangler/config-schema.json +name = "my-workflow-demo" +main = "src/index.ts" +compatibility_date = "2024-10-22" + +[[workflows]] +binding = "WORKFLOW" +name = "my-workflow" +class_name = "Demo" + +[[workflows]] +binding = "WORKFLOW2" +name = "my-workflow-2" +class_name = "Demo2" \ No newline at end of file diff --git a/fixtures/workflow/tests/index.test.ts b/fixtures/workflow/tests/index.test.ts index ce5a11e4983e..592feeef7c0d 100644 --- a/fixtures/workflow/tests/index.test.ts +++ b/fixtures/workflow/tests/index.test.ts @@ -1,3 +1,4 @@ +import { rm } from "fs/promises"; import { resolve } from "path"; import { fetch } from "undici"; import { afterAll, beforeAll, describe, it, vi } from "vitest"; @@ -10,14 +11,14 @@ describe("Workflows", () => { getOutput: () => string; beforeAll(async () => { + // delete previous run contents because of persistence + await rm(resolve(__dirname, "..") + "/.wrangler", { + force: true, + recursive: true, + }); ({ ip, port, stop, getOutput } = await runWranglerDev( resolve(__dirname, ".."), - [ - "--port=0", - "--inspector-port=0", - "--upstream-protocol=https", - "--host=prod.example.org", - ] + ["--port=0", "--inspector-port=0"] )); }); diff --git a/fixtures/workflow/worker-configuration.d.ts b/fixtures/workflow/worker-configuration.d.ts deleted file mode 100644 index ad79d683e0fb..000000000000 --- a/fixtures/workflow/worker-configuration.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Generated by Wrangler by running `wrangler types` - -interface Env { - WORKFLOW: Workflow; -} diff --git a/packages/create-cloudflare/e2e-tests/cli.test.ts b/packages/create-cloudflare/e2e-tests/cli.test.ts index 094d86829bc3..7efce879f3b4 100644 --- a/packages/create-cloudflare/e2e-tests/cli.test.ts +++ b/packages/create-cloudflare/e2e-tests/cli.test.ts @@ -199,7 +199,7 @@ describe.skipIf(experimental || frameworkToTest || isQuarantineMode())( const { output } = await runC3( [ project.path, - "--template=https://github.com/cloudflare/templates/worker-router", + "--template=https://github.com/cloudflare/workers-graphql-server", "--no-deploy", "--git=false", ], @@ -208,10 +208,10 @@ describe.skipIf(experimental || frameworkToTest || isQuarantineMode())( ); expect(output).toContain( - `repository https://github.com/cloudflare/templates/worker-router`, + `repository https://github.com/cloudflare/workers-graphql-server`, ); expect(output).toContain( - `Cloning template from: https://github.com/cloudflare/templates/worker-router`, + `Cloning template from: https://github.com/cloudflare/workers-graphql-server`, ); expect(output).toContain(`template cloned and validated`); }, diff --git a/packages/miniflare/src/plugins/workflows/index.ts b/packages/miniflare/src/plugins/workflows/index.ts index edd08d2e0be1..4d4a53a10225 100644 --- a/packages/miniflare/src/plugins/workflows/index.ts +++ b/packages/miniflare/src/plugins/workflows/index.ts @@ -62,15 +62,20 @@ export const WORKFLOWS_PLUGIN: Plugin< sharedOptions.workflowsPersist ); await fs.mkdir(persistPath, { recursive: true }); - const storageService: Service = { - name: WORKFLOWS_STORAGE_SERVICE_NAME, + // each workflow should get its own storage service + const storageServices: Service[] = Object.entries( + options.workflows ?? {} + ).map(([_, workflow]) => ({ + name: `${WORKFLOWS_STORAGE_SERVICE_NAME}-${workflow.name}`, disk: { path: persistPath, writable: true }, - }; + })); // this creates one miniflare service per workflow that the user's script has. we should dedupe engine definition later const services = Object.entries(options.workflows ?? {}).map( ([_bindingName, workflow]) => { - const uniqueKey = `miniflare-workflows`; + // NOTE(lduarte): the engine unique namespace key must be unique per workflow definition + // otherwise workerd will crash because there's two equal DO namespaces + const uniqueKey = `miniflare-workflows-${workflow.name}`; const workflowsBinding: Service = { name: `${WORKFLOWS_PLUGIN_NAME}:${workflow.name}`, @@ -90,8 +95,9 @@ export const WORKFLOWS_PLUGIN: Plugin< preventEviction: true, }, ], - // this might conflict between workflows - durableObjectStorage: { localDisk: WORKFLOWS_STORAGE_SERVICE_NAME }, + durableObjectStorage: { + localDisk: `${WORKFLOWS_STORAGE_SERVICE_NAME}-${workflow.name}`, + }, bindings: [ { name: "ENGINE", @@ -116,7 +122,7 @@ export const WORKFLOWS_PLUGIN: Plugin< return []; } - return [storageService, ...services]; + return [...storageServices, ...services]; }, getPersistPath({ workflowsPersist }, tmpPath) { diff --git a/packages/miniflare/test/plugins/workflows/index.spec.ts b/packages/miniflare/test/plugins/workflows/index.spec.ts new file mode 100644 index 000000000000..ac19a418e868 --- /dev/null +++ b/packages/miniflare/test/plugins/workflows/index.spec.ts @@ -0,0 +1,72 @@ +import * as fs from "fs/promises"; +import { scheduler } from "timers/promises"; +import test from "ava"; +import { Miniflare, MiniflareOptions } from "miniflare"; +import { useTmp } from "../../test-shared"; + +const WORKFLOW_SCRIPT = () => ` +import { WorkflowEntrypoint } from "cloudflare:workers"; +export class MyWorkflow extends WorkflowEntrypoint { + async run(event, step) { + await step.do("i'm a step?", async () => "yes you are") + + return "I'm a output string" + } + } + export default { + async fetch(request, env, ctx) { + const workflow = await env.MY_WORKFLOW.create({id: "i'm an id"}) + + return new Response(JSON.stringify(await workflow.status())) + }, + };`; + +test("persists Workflow data on file-system between runs", async (t) => { + const tmp = await useTmp(t); + const opts: MiniflareOptions = { + name: "worker", + compatibilityDate: "2024-11-20", + modules: true, + script: WORKFLOW_SCRIPT(), + workflows: { + MY_WORKFLOW: { + className: "MyWorkflow", + name: "MY_WORKFLOW", + }, + }, + workflowsPersist: tmp, + }; + let mf = new Miniflare(opts); + t.teardown(() => mf.dispose()); + + let res = await mf.dispatchFetch("http://localhost"); + t.is(await res.text(), '{"status":"running","output":[]}'); + + // there's no waitUntil in ava haha + const begin = performance.now(); + let success = false; + let test = ""; + while (performance.now() - begin < 2000) { + const res = await mf.dispatchFetch("http://localhost"); + console.log(test); + test = await res.text(); + if (test === '{"status":"complete","output":["yes you are"]}') { + success = true; + break; + } + await scheduler.wait(50); + } + t.true(success, `Condition was not met in 2000ms - output is ${test}`); + + // check if files were commited + const names = await fs.readdir(tmp); + t.deepEqual(names, ["miniflare-workflows-MY_WORKFLOW"]); + + // restart miniflare + await mf.dispose(); + mf = new Miniflare(opts); + + // state should be persisted now + res = await mf.dispatchFetch("http://localhost"); + t.is(await res.text(), '{"status":"complete","output":["yes you are"]}'); +}); diff --git a/packages/workflows-shared/src/engine.ts b/packages/workflows-shared/src/engine.ts index a72f3f5fe68e..4a80f6cdbdc1 100644 --- a/packages/workflows-shared/src/engine.ts +++ b/packages/workflows-shared/src/engine.ts @@ -53,9 +53,10 @@ export type DatabaseInstance = { ended_on: string | null; }; +const ENGINE_STATUS_KEY = "ENGINE_STATUS"; + export class Engine extends DurableObject { logs: Array = []; - status: InstanceStatus = InstanceStatus.Queued; isRunning: boolean = false; accountId: number | undefined; @@ -66,21 +67,32 @@ export class Engine extends DurableObject { constructor(state: DurableObjectState, env: Env) { super(state, env); - void this.ctx.blockConcurrencyWhile(async () => { this.ctx.storage.transactionSync(() => { - this.ctx.storage.sql.exec(` - CREATE TABLE IF NOT EXISTS priority_queue ( - id INTEGER PRIMARY KEY NOT NULL, - created_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - target_timestamp INTEGER NOT NULL, - action INTEGER NOT NULL, -- should only be 0 or 1 (1 for added, 0 for deleted), - entryType INTEGER NOT NULL, - hash TEXT NOT NULL, - CHECK (action IN (0, 1)), -- guararentee that action can only be 0 or 1 - UNIQUE (action, entryType, hash) - ) - `); + try { + this.ctx.storage.sql.exec(` + CREATE TABLE IF NOT EXISTS priority_queue ( + id INTEGER PRIMARY KEY NOT NULL, + created_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + target_timestamp INTEGER NOT NULL, + action INTEGER NOT NULL, -- should only be 0 or 1 (1 for added, 0 for deleted), + entryType INTEGER NOT NULL, + hash TEXT NOT NULL, + CHECK (action IN (0, 1)), -- guararentee that action can only be 0 or 1 + UNIQUE (action, entryType, hash) + ); + CREATE TABLE IF NOT EXISTS states ( + id INTEGER PRIMARY KEY NOT NULL, + groupKey TEXT, + target TEXT, + metadata TEXT, + event INTEGER NOT NULL + ) + `); + } catch (e) { + console.error(e); + throw e; + } }); }); @@ -96,12 +108,13 @@ export class Engine extends DurableObject { target: string | null = null, metadata: Record ) { - this.logs.push({ + this.ctx.storage.sql.exec( + "INSERT INTO states (event, groupKey, target, metadata) VALUES (?, ?, ?, ?)", event, group, target, - metadata, - }); + JSON.stringify(metadata) + ); } readLogsFromStep(_cacheKey: string): RawInstanceLog[] { @@ -109,9 +122,19 @@ export class Engine extends DurableObject { } readLogs(): InstanceLogsResponse { + const logs = [ + ...this.ctx.storage.sql.exec>( + "SELECT event, groupKey, target, metadata FROM states" + ), + ]; + return { // @ts-expect-error TODO: Fix this - logs: this.logs, + logs: logs.map((log) => ({ + ...log, + metadata: JSON.parse(log.metadata as string), + group: log.groupKey, + })), }; } @@ -119,7 +142,13 @@ export class Engine extends DurableObject { _accountId: number, _instanceId: string ): Promise { - return this.status; + const res = await this.ctx.storage.get(ENGINE_STATUS_KEY); + + // NOTE(lduarte): if status don't exist, means that engine is running for the first time, so we assume queued + if (res === undefined) { + return InstanceStatus.Queued; + } + return res; } async setStatus( @@ -127,7 +156,7 @@ export class Engine extends DurableObject { instanceId: string, status: InstanceStatus ): Promise { - this.status = status; + await this.ctx.storage.put(ENGINE_STATUS_KEY, status); } async abort(_reason: string) { diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts index 7a478e3be5cd..7b096e4ce720 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts @@ -40,7 +40,7 @@ function configDefaults( const persist = path.join(process.cwd(), ".wrangler/persist"); return { entrypoint: "NOT_REAL", - directory: "NOT_REAL", + projectRoot: "NOT_REAL", build: unusable(), legacy: {}, dev: { persist }, @@ -68,7 +68,7 @@ describe("BundleController", () => { legacy: {}, name: "worker", entrypoint: path.resolve("src/index.ts"), - directory: path.resolve("src"), + projectRoot: path.resolve("src"), build: { additionalModules: [], processEntrypoint: false, @@ -141,7 +141,7 @@ describe("BundleController", () => { legacy: {}, name: "worker", entrypoint: path.resolve("src/index.ts"), - directory: path.resolve("src"), + projectRoot: path.resolve("src"), build: { additionalModules: [], processEntrypoint: false, @@ -207,7 +207,7 @@ describe("BundleController", () => { legacy: {}, name: "worker", entrypoint: path.resolve("out.ts"), - directory: path.resolve("."), + projectRoot: path.resolve("."), build: { additionalModules: [], processEntrypoint: false, @@ -287,7 +287,7 @@ describe("BundleController", () => { legacy: {}, name: "worker", entrypoint: path.resolve("src/index.ts"), - directory: path.resolve("src"), + projectRoot: path.resolve("src"), build: { additionalModules: [], processEntrypoint: false, @@ -345,7 +345,7 @@ describe("BundleController", () => { legacy: {}, name: "worker", entrypoint: path.resolve("src/index.ts"), - directory: path.resolve("src"), + projectRoot: path.resolve("src"), build: { additionalModules: [], @@ -391,7 +391,7 @@ describe("BundleController", () => { const configCustom: Partial = { name: "worker", entrypoint: path.resolve("out.ts"), - directory: process.cwd(), + projectRoot: process.cwd(), build: { additionalModules: [], processEntrypoint: false, @@ -464,7 +464,7 @@ describe("BundleController", () => { const configCustom: Partial = { name: "worker", entrypoint: path.resolve("out.ts"), - directory: process.cwd(), + projectRoot: process.cwd(), build: { additionalModules: [], @@ -513,7 +513,7 @@ describe("BundleController", () => { legacy: {}, name: "worker", entrypoint: path.resolve("src/index.ts"), - directory: path.resolve("src"), + projectRoot: path.resolve("src"), build: { additionalModules: [], diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/ConfigController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/ConfigController.test.ts index 8ebd3d775429..58b6e603f488 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/ConfigController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/ConfigController.test.ts @@ -51,7 +51,7 @@ describe("ConfigController", () => { moduleRoot: path.join(process.cwd(), "src"), moduleRules: [], }, - directory: process.cwd(), + projectRoot: process.cwd(), entrypoint: path.join(process.cwd(), "src/index.ts"), }, }); @@ -87,7 +87,7 @@ base_dir = \"./some/base_dir\"`, moduleRoot: path.join(process.cwd(), "./some/base_dir"), moduleRules: [], }, - directory: process.cwd(), + projectRoot: process.cwd(), entrypoint: path.join(process.cwd(), "./some/base_dir/nested/index.js"), }, }); @@ -114,7 +114,7 @@ base_dir = \"./some/base_dir\"`, type: "configUpdate", config: { entrypoint: path.join(process.cwd(), "src/index.ts"), - directory: process.cwd(), + projectRoot: process.cwd(), build: { additionalModules: [], define: {}, @@ -138,7 +138,7 @@ base_dir = \"./some/base_dir\"`, type: "configUpdate", config: { entrypoint: path.join(process.cwd(), "src/index.ts"), - directory: process.cwd(), + projectRoot: process.cwd(), build: { additionalModules: [], define: {}, @@ -168,7 +168,7 @@ base_dir = \"./some/base_dir\"`, type: "configUpdate", config: { entrypoint: path.join(process.cwd(), "src/index.ts"), - directory: process.cwd(), + projectRoot: process.cwd(), build: { alias: { foo: "bar", diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts index 8b1530475978..444797925c81 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts @@ -88,7 +88,7 @@ function makeEsbuildBundle(testBundle: TestBundle): Bundle { entrypointSource: "", entry: { file: "index.mjs", - directory: "/virtual/", + projectRoot: "/virtual/", format: "modules", moduleRoot: "/virtual", name: undefined, @@ -127,7 +127,7 @@ function configDefaults( ): StartDevWorkerOptions { return { entrypoint: "NOT_REAL", - directory: "NOT_REAL", + projectRoot: "NOT_REAL", build: unusable(), legacy: {}, dev: { persist: "./persist" }, @@ -232,7 +232,7 @@ describe("LocalRuntimeController", () => { `, entry: { file: "esm/index.mjs", - directory: "/virtual/", + projectRoot: "/virtual/", format: "modules", moduleRoot: "/virtual", name: undefined, @@ -346,7 +346,7 @@ describe("LocalRuntimeController", () => { path: "/virtual/index.js", entry: { file: "index.js", - directory: "/virtual/", + projectRoot: "/virtual/", format: "service-worker", moduleRoot: "/virtual", name: undefined, diff --git a/packages/wrangler/src/__tests__/find-additional-modules.test.ts b/packages/wrangler/src/__tests__/find-additional-modules.test.ts index e871002f96aa..dc9df03f2670 100644 --- a/packages/wrangler/src/__tests__/find-additional-modules.test.ts +++ b/packages/wrangler/src/__tests__/find-additional-modules.test.ts @@ -39,7 +39,7 @@ describe("traverse module graph", () => { const modules = await findAdditionalModules( { file: path.join(process.cwd(), "./index.js"), - directory: process.cwd(), + projectRoot: process.cwd(), format: "modules", moduleRoot: process.cwd(), exports: [], @@ -75,7 +75,7 @@ describe("traverse module graph", () => { const modules = await findAdditionalModules( { file: path.join(process.cwd(), "./index.js"), - directory: process.cwd(), + projectRoot: process.cwd(), format: "modules", moduleRoot: process.cwd(), exports: [], @@ -109,7 +109,7 @@ describe("traverse module graph", () => { const modules = await findAdditionalModules( { file: path.join(process.cwd(), "./src/nested/index.js"), - directory: path.join(process.cwd(), "./src/nested"), + projectRoot: path.join(process.cwd(), "./src/nested"), format: "modules", // The default module root is dirname(file) moduleRoot: path.join(process.cwd(), "./src/nested"), @@ -144,7 +144,7 @@ describe("traverse module graph", () => { const modules = await findAdditionalModules( { file: path.join(process.cwd(), "./src/nested/index.js"), - directory: path.join(process.cwd(), "./src/nested"), + projectRoot: path.join(process.cwd(), "./src/nested"), format: "modules", // The default module root is dirname(file) moduleRoot: path.join(process.cwd(), "./src"), @@ -179,7 +179,7 @@ describe("traverse module graph", () => { const modules = await findAdditionalModules( { file: path.join(process.cwd(), "./src/nested/index.js"), - directory: path.join(process.cwd(), "./src/nested"), + projectRoot: path.join(process.cwd(), "./src/nested"), format: "modules", // The default module root is dirname(file) moduleRoot: path.join(process.cwd(), "./src"), @@ -214,7 +214,7 @@ describe("traverse module graph", () => { const modules = await findAdditionalModules( { file: path.join(process.cwd(), "./src/index.js"), - directory: path.join(process.cwd(), "./src"), + projectRoot: path.join(process.cwd(), "./src"), format: "modules", // The default module root is dirname(file) moduleRoot: path.join(process.cwd(), "./src"), @@ -249,7 +249,7 @@ describe("traverse module graph", () => { findAdditionalModules( { file: path.join(process.cwd(), "./src/index.js"), - directory: path.join(process.cwd(), "./src"), + projectRoot: path.join(process.cwd(), "./src"), format: "modules", // The default module root is dirname(file) moduleRoot: path.join(process.cwd(), "./src"), diff --git a/packages/wrangler/src/__tests__/get-entry.test.ts b/packages/wrangler/src/__tests__/get-entry.test.ts new file mode 100644 index 000000000000..aa938d5054dc --- /dev/null +++ b/packages/wrangler/src/__tests__/get-entry.test.ts @@ -0,0 +1,143 @@ +import path from "path"; +import dedent from "ts-dedent"; +import { defaultWranglerConfig } from "../config/config"; +import { getEntry } from "../deployment-bundle/entry"; +import { mockConsoleMethods } from "./helpers/mock-console"; +import { runInTempDir } from "./helpers/run-in-tmp"; +import { seed } from "./helpers/seed"; +import type { Entry } from "../deployment-bundle/entry"; + +function normalize(entry: Entry): Entry { + const tmpDir = process.cwd(); + const tmpDirName = path.basename(tmpDir); + + return Object.fromEntries( + Object.entries(entry).map(([k, v]) => [ + k, + typeof v === "string" + ? v + .replaceAll("\\", "/") + .replace(new RegExp(`(.*${tmpDirName})`), `/tmp/dir`) + : v, + ]) + ) as Entry; +} + +describe("getEntry()", () => { + runInTempDir(); + mockConsoleMethods(); + + it("--script index.ts", async () => { + await seed({ + "index.ts": dedent/* javascript */ ` + export default { + fetch() { + + } + } + `, + }); + const entry = await getEntry( + { script: "index.ts" }, + defaultWranglerConfig, + "deploy" + ); + expect(normalize(entry)).toMatchObject({ + projectRoot: "/tmp/dir", + file: "/tmp/dir/index.ts", + moduleRoot: "/tmp/dir", + }); + }); + + it("--script src/index.ts", async () => { + await seed({ + "src/index.ts": dedent/* javascript */ ` + export default { + fetch() { + + } + } + `, + }); + const entry = await getEntry( + { script: "src/index.ts" }, + defaultWranglerConfig, + "deploy" + ); + expect(normalize(entry)).toMatchObject({ + projectRoot: "/tmp/dir", + file: "/tmp/dir/src/index.ts", + moduleRoot: "/tmp/dir/src", + }); + }); + + it("main = index.ts", async () => { + await seed({ + "index.ts": dedent/* javascript */ ` + export default { + fetch() { + + } + } + `, + }); + const entry = await getEntry( + {}, + { ...defaultWranglerConfig, main: "index.ts" }, + "deploy" + ); + expect(normalize(entry)).toMatchObject({ + projectRoot: "/tmp/dir", + file: "/tmp/dir/index.ts", + moduleRoot: "/tmp/dir", + }); + }); + + it("main = src/index.ts", async () => { + await seed({ + "src/index.ts": dedent/* javascript */ ` + export default { + fetch() { + + } + } + `, + }); + const entry = await getEntry( + {}, + { ...defaultWranglerConfig, main: "src/index.ts" }, + "deploy" + ); + expect(normalize(entry)).toMatchObject({ + projectRoot: "/tmp/dir", + file: "/tmp/dir/src/index.ts", + moduleRoot: "/tmp/dir/src", + }); + }); + + it("main = src/index.ts w/ configPath", async () => { + await seed({ + "other-worker/src/index.ts": dedent/* javascript */ ` + export default { + fetch() { + + } + } + `, + }); + const entry = await getEntry( + {}, + { + ...defaultWranglerConfig, + main: "src/index.ts", + configPath: "other-worker/wrangler.toml", + }, + "deploy" + ); + expect(normalize(entry)).toMatchObject({ + projectRoot: "/tmp/dir/other-worker", + file: "/tmp/dir/other-worker/src/index.ts", + moduleRoot: "/tmp/dir/other-worker/src", + }); + }); +}); diff --git a/packages/wrangler/src/__tests__/navigator-user-agent.test.ts b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts index 1f3f3288e659..6b66a311a9ab 100644 --- a/packages/wrangler/src/__tests__/navigator-user-agent.test.ts +++ b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts @@ -104,7 +104,7 @@ describe("defineNavigatorUserAgent is respected", () => { await bundleWorker( { file: path.resolve("src/index.js"), - directory: process.cwd(), + projectRoot: process.cwd(), format: "modules", moduleRoot: path.dirname(path.resolve("src/index.js")), exports: [], @@ -167,7 +167,7 @@ describe("defineNavigatorUserAgent is respected", () => { await bundleWorker( { file: path.resolve("src/index.js"), - directory: process.cwd(), + projectRoot: process.cwd(), format: "modules", moduleRoot: path.dirname(path.resolve("src/index.js")), exports: [], diff --git a/packages/wrangler/src/__tests__/pages/deployment-list.test.ts b/packages/wrangler/src/__tests__/pages/deployment-list.test.ts index 828e2ad00ca2..8d86f743288a 100644 --- a/packages/wrangler/src/__tests__/pages/deployment-list.test.ts +++ b/packages/wrangler/src/__tests__/pages/deployment-list.test.ts @@ -47,20 +47,128 @@ describe("pages deployment list", () => { expect(requests.count).toBe(1); }); + + it("should pass no environment", async () => { + const deployments: Deployment[] = [ + { + id: "87bbc8fe-16be-45cd-81e0-63d722e82cdf", + url: "https://87bbc8fe.images.pages.dev", + environment: "preview", + created_on: "2021-11-17T14:52:26.133835Z", + latest_stage: { + ended_on: "2021-11-17T14:52:26.133835Z", + status: "success", + }, + deployment_trigger: { + metadata: { + branch: "main", + commit_hash: "c7649364c4cb32ad4f65b530b9424e8be5bec9d6", + }, + }, + project_name: "images", + }, + ]; + + const requests = mockDeploymentListRequest(deployments); + await runWrangler("pages deployment list --project-name=images"); + expect(requests.count).toBe(1); + expect( + requests.queryParams[0].find(([key, _]) => { + return key === "env"; + }) + ).toBeUndefined(); + }); + + it("should pass production environment with flag", async () => { + const deployments: Deployment[] = [ + { + id: "87bbc8fe-16be-45cd-81e0-63d722e82cdf", + url: "https://87bbc8fe.images.pages.dev", + environment: "preview", + created_on: "2021-11-17T14:52:26.133835Z", + latest_stage: { + ended_on: "2021-11-17T14:52:26.133835Z", + status: "success", + }, + deployment_trigger: { + metadata: { + branch: "main", + commit_hash: "c7649364c4cb32ad4f65b530b9424e8be5bec9d6", + }, + }, + project_name: "images", + }, + ]; + + const requests = mockDeploymentListRequest(deployments); + await runWrangler( + "pages deployment list --project-name=images --environment=production" + ); + expect(requests.count).toBe(1); + expect( + requests.queryParams[0].find(([key, _]) => { + return key === "env"; + }) + ).toStrictEqual(["env", "production"]); + }); + + it("should pass preview environment with flag", async () => { + const deployments: Deployment[] = [ + { + id: "87bbc8fe-16be-45cd-81e0-63d722e82cdf", + url: "https://87bbc8fe.images.pages.dev", + environment: "preview", + created_on: "2021-11-17T14:52:26.133835Z", + latest_stage: { + ended_on: "2021-11-17T14:52:26.133835Z", + status: "success", + }, + deployment_trigger: { + metadata: { + branch: "main", + commit_hash: "c7649364c4cb32ad4f65b530b9424e8be5bec9d6", + }, + }, + project_name: "images", + }, + ]; + + const requests = mockDeploymentListRequest(deployments); + await runWrangler( + "pages deployment list --project-name=images --environment=preview" + ); + expect(requests.count).toBe(1); + expect( + requests.queryParams[0].find(([key, _]) => { + return key === "env"; + }) + ).toStrictEqual(["env", "preview"]); + }); }); /* -------------------------------------------------- */ /* Helper Functions */ /* -------------------------------------------------- */ -function mockDeploymentListRequest(deployments: unknown[]) { - const requests = { count: 0 }; +/** + * A logger used to check how many times a mock API has been hit. + * Useful as a helper in our testing to check if wrangler is making + * the correct API calls without actually sending any web traffic. + */ +type RequestLogger = { + count: number; + queryParams: [string, string][][]; +}; + +function mockDeploymentListRequest(deployments: unknown[]): RequestLogger { + const requests: RequestLogger = { count: 0, queryParams: [] }; msw.use( http.get( "*/accounts/:accountId/pages/projects/:project/deployments", - ({ params }) => { + ({ request, params }) => { requests.count++; - + const url = new URL(request.url); + requests.queryParams.push(Array.from(url.searchParams.entries())); expect(params.project).toEqual("images"); expect(params.accountId).toEqual("some-account-id"); diff --git a/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts b/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts index f293c051ac33..344792b252f7 100644 --- a/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts +++ b/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts @@ -166,6 +166,63 @@ describe("pages deployment tail", () => { ); await api.closeHelper(); }); + + it("passes default environment to deployments list", async () => { + api = mockTailAPIs(); + expect(api.requests.creation.length).toStrictEqual(0); + + await runWrangler( + "pages deployment tail --project-name mock-project mock-deployment-id" + ); + + await expect(api.ws.connected).resolves.toBeTruthy(); + console.log(api.requests.deployments.queryParams[0]); + expect(api.requests.deployments.count).toStrictEqual(1); + expect( + api.requests.deployments.queryParams[0].find(([key, _]) => { + return key === "env"; + }) + ).toStrictEqual(["env", "production"]); + await api.closeHelper(); + }); + + it("passes production environment to deployments list", async () => { + api = mockTailAPIs(); + expect(api.requests.creation.length).toStrictEqual(0); + + await runWrangler( + "pages deployment tail --project-name mock-project mock-deployment-id --environment production" + ); + + await expect(api.ws.connected).resolves.toBeTruthy(); + console.log(api.requests.deployments.queryParams[0]); + expect(api.requests.deployments.count).toStrictEqual(1); + expect( + api.requests.deployments.queryParams[0].find(([key, _]) => { + return key === "env"; + }) + ).toStrictEqual(["env", "production"]); + await api.closeHelper(); + }); + + it("passes preview environment to deployments list", async () => { + api = mockTailAPIs(); + expect(api.requests.creation.length).toStrictEqual(0); + + await runWrangler( + "pages deployment tail --project-name mock-project mock-deployment-id --environment preview" + ); + + await expect(api.ws.connected).resolves.toBeTruthy(); + console.log(api.requests.deployments.queryParams[0]); + expect(api.requests.deployments.count).toStrictEqual(1); + expect( + api.requests.deployments.queryParams[0].find(([key, _]) => { + return key === "env"; + }) + ).toStrictEqual(["env", "preview"]); + await api.closeHelper(); + }); }); describe("filtering", () => { @@ -783,7 +840,7 @@ function deserializeToJson(message: WebSocket.RawData): string { */ type MockAPI = { requests: { - deployments: RequestCounter; + deployments: RequestLogger; creation: RequestInit[]; deletion: RequestCounter; }; @@ -792,17 +849,29 @@ type MockAPI = { closeHelper: () => Promise; }; +/** + * A logger used to check how many times a mock API has been hit. + * Useful as a helper in our testing to check if wrangler is making + * the correct API calls without actually sending any web traffic. + */ +type RequestLogger = { + count: number; + queryParams: [string, string][][]; +}; + /** * Mock out the API hit during Tail creation * * @returns a `RequestCounter` for counting how many times the API is hit */ -function mockListDeployments(): RequestCounter { - const requests: RequestCounter = { count: 0 }; +function mockListDeployments(): RequestLogger { + const requests: RequestLogger = { count: 0, queryParams: [] }; msw.use( http.get( `*/accounts/:accountId/pages/projects/:projectName/deployments`, - () => { + ({ request }) => { + const url = new URL(request.url); + requests.queryParams.push(Array.from(url.searchParams.entries())); requests.count++; return HttpResponse.json( { @@ -839,15 +908,6 @@ function mockListDeployments(): RequestCounter { return requests; } -/** - * A counter used to check how many times a mock API has been hit. - * Useful as a helper in our testing to check if wrangler is making - * the correct API calls without actually sending any web traffic - */ -type RequestCounter = { - count: number; -}; - /** * Mock out the API hit during Tail creation * @@ -911,6 +971,15 @@ const mockEmailEventTo = "to@example.com"; */ const mockEmailEventSize = 45416; +/** + * A counter used to check how many times a mock API has been hit. + * Useful as a helper in our testing to check if wrangler is making + * the correct API calls without actually sending any web traffic + */ +type RequestCounter = { + count: number; +}; + /** * Mock out the API hit during Tail deletion * @@ -950,7 +1019,7 @@ function mockTailAPIs(): MockAPI { requests: { deletion: { count: 0 }, creation: [], - deployments: { count: 0 }, + deployments: { count: 0, queryParams: [] }, }, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ws: null!, // will be set in the `beforeEach()`. diff --git a/packages/wrangler/src/api/startDevWorker/BundlerController.ts b/packages/wrangler/src/api/startDevWorker/BundlerController.ts index bf633397473f..d3b7f91f7784 100644 --- a/packages/wrangler/src/api/startDevWorker/BundlerController.ts +++ b/packages/wrangler/src/api/startDevWorker/BundlerController.ts @@ -51,7 +51,7 @@ 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.relative(config.projectRoot, config.entrypoint) || "."; logger.log(`The file ${filePath} changed, restarting build...`); this.emitBundleStartEvent(config); try { @@ -74,7 +74,7 @@ export class BundlerController extends Controller { const entry: Entry = { file: config.entrypoint, - directory: config.directory, + projectRoot: config.projectRoot, format: config.build.format, moduleRoot: config.build.moduleRoot, exports: config.build.exports, @@ -131,7 +131,7 @@ export class BundlerController extends Controller { // This could potentially cause issues as we no longer have identical behaviour between dev and deploy? targetConsumer: "dev", local: !config.dev?.remote, - projectRoot: config.directory, + projectRoot: config.projectRoot, defineNavigatorUserAgent: isNavigatorDefined( config.compatibilityDate, config.compatibilityFlags @@ -229,7 +229,7 @@ export class BundlerController extends Controller { assert(this.#tmpDir); const entry: Entry = { file: config.entrypoint, - directory: config.directory, + projectRoot: config.projectRoot, format: config.build.format, moduleRoot: config.build.moduleRoot, exports: config.build.exports, @@ -264,7 +264,7 @@ export class BundlerController extends Controller { // startDevWorker only applies to "dev" targetConsumer: "dev", testScheduled: Boolean(config.dev?.testScheduled), - projectRoot: config.directory, + projectRoot: config.projectRoot, onStart: () => { this.emitBundleStartEvent(config); }, @@ -325,7 +325,7 @@ export class BundlerController extends Controller { onConfigUpdate(event: ConfigUpdateEvent) { this.#tmpDir?.remove(); try { - this.#tmpDir = getWranglerTmpDir(event.config.directory, "dev"); + this.#tmpDir = getWranglerTmpDir(event.config.projectRoot, "dev"); } catch (e) { logger.error( "Failed to create temporary directory to store built files." diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index eaffac4e2435..2c6097d950b4 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -256,7 +256,7 @@ async function resolveConfig( compatibilityDate: getDevCompatibilityDate(config, input.compatibilityDate), compatibilityFlags: input.compatibilityFlags ?? config.compatibility_flags, entrypoint: entry.file, - directory: entry.directory, + projectRoot: entry.projectRoot, bindings, migrations: input.migrations ?? config.migrations, sendMetrics: input.sendMetrics ?? config.send_metrics, diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index fba44f65bc62..ba045f56f370 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -174,7 +174,7 @@ export interface StartDevWorkerInput { export type StartDevWorkerOptions = Omit & { /** A worker's directory. Usually where the wrangler.toml file is located */ - directory: string; + projectRoot: string; build: StartDevWorkerInput["build"] & { nodejsCompatMode: NodeJSCompatMode; format: CfScriptFormat; diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts index 16fc175fca6e..000c3bcf97c5 100644 --- a/packages/wrangler/src/deploy/index.ts +++ b/packages/wrangler/src/deploy/index.ts @@ -329,7 +329,7 @@ export async function deployHandler(args: DeployArgs) { await verifyWorkerMatchesCITag( accountId, name, - path.relative(entry.directory, config.configPath ?? "wrangler.toml") + path.relative(entry.projectRoot, config.configPath ?? "wrangler.toml") ); } const { sourceMapSize, versionId, workerTag, targets } = await deploy({ diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index dd7d16715568..f367363f2a69 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -373,7 +373,7 @@ export async function bundleWorker( path: require.resolve(aliasPath, { // From the esbuild alias docs: "Note that when an import path is substituted using an alias, the resulting import path is resolved in the working directory instead of in the directory containing the source file with the import path." // https://esbuild.github.io/api/#alias:~:text=Note%20that%20when%20an%20import%20path%20is%20substituted%20using%20an%20alias%2C%20the%20resulting%20import%20path%20is%20resolved%20in%20the%20working%20directory%20instead%20of%20in%20the%20directory%20containing%20the%20source%20file%20with%20the%20import%20path. - paths: [entry.directory], + paths: [entry.projectRoot], }), }; } @@ -385,7 +385,7 @@ export async function bundleWorker( // Don't use entryFile here as the file may have been changed when applying the middleware entryPoints: [entry.file], bundle, - absWorkingDir: entry.directory, + absWorkingDir: entry.projectRoot, outdir: destination, keepNames: true, entryNames: entryName || path.parse(entryFile).name, @@ -526,7 +526,7 @@ export async function bundleWorker( )[0]; const resolvedEntryPointPath = path.resolve( - entry.directory, + entry.projectRoot, entryPoint.relativePath ); @@ -548,7 +548,7 @@ export async function bundleWorker( sourceMapPath, sourceMapMetadata: { tmpDir: tmpDir.path, - entryDirectory: entry.directory, + entryDirectory: entry.projectRoot, }, }; } diff --git a/packages/wrangler/src/deployment-bundle/entry.ts b/packages/wrangler/src/deployment-bundle/entry.ts index 1be44b13f210..bcc04895423c 100644 --- a/packages/wrangler/src/deployment-bundle/entry.ts +++ b/packages/wrangler/src/deployment-bundle/entry.ts @@ -22,7 +22,7 @@ export type Entry = { /** A worker's entrypoint */ file: string; /** A worker's directory. Usually where the wrangler.toml file is located */ - directory: string; + projectRoot: string; /** Is this a module worker or a service worker? */ format: CfScriptFormat; /** The directory that contains all of a `--no-bundle` worker's modules. Usually `${directory}/src`. Defaults to path.dirname(file) */ @@ -50,10 +50,11 @@ export async function getEntry( config: Config, command: "dev" | "deploy" | "versions upload" | "types" ): Promise { - const directory = process.cwd(); const entryPoint = config.site?.["entry-point"]; - let paths: { absolutePath: string; relativePath: string } | undefined; + let paths: + | { absolutePath: string; relativePath: string; projectRoot?: string } + | undefined; if (args.script) { paths = resolveEntryWithScript(args.script); @@ -81,9 +82,10 @@ export async function getEntry( } await runCustomBuild(paths.absolutePath, paths.relativePath, config.build); + const projectRoot = paths.projectRoot ?? process.cwd(); const { format, exports } = await guessWorkerFormat( paths.absolutePath, - directory, + projectRoot, args.format ?? config.build?.upload?.format, config.tsconfig ); @@ -117,7 +119,7 @@ export async function getEntry( return { file: paths.absolutePath, - directory, + projectRoot, format, moduleRoot: args.moduleRoot ?? config.base_dir ?? path.dirname(paths.absolutePath), diff --git a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts index 1e0ffe9cf68c..21f9c585a921 100644 --- a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts +++ b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts @@ -72,7 +72,7 @@ export async function findAdditionalModules( let pythonRequirements = ""; try { pythonRequirements = await readFile( - path.resolve(entry.directory, "requirements.txt"), + path.resolve(entry.projectRoot, "requirements.txt"), "utf-8" ); } catch (e) { diff --git a/packages/wrangler/src/deployment-bundle/resolve-entry.ts b/packages/wrangler/src/deployment-bundle/resolve-entry.ts index 5a9efb535194..2da92e262709 100644 --- a/packages/wrangler/src/deployment-bundle/resolve-entry.ts +++ b/packages/wrangler/src/deployment-bundle/resolve-entry.ts @@ -16,11 +16,12 @@ export function resolveEntryWithMain( ): { absolutePath: string; relativePath: string; + projectRoot: string; } { - const directory = path.resolve(path.dirname(configPath ?? ".")); - const file = path.resolve(directory, main); - const relativePath = path.relative(directory, file) || "."; - return { absolutePath: file, relativePath }; + const projectRoot = path.resolve(path.dirname(configPath ?? ".")); + const file = path.resolve(projectRoot, main); + const relativePath = path.relative(projectRoot, file) || "."; + return { absolutePath: file, relativePath, projectRoot }; } export function resolveEntryWithEntryPoint( @@ -29,13 +30,14 @@ export function resolveEntryWithEntryPoint( ): { absolutePath: string; relativePath: string; + projectRoot: string; } { - const directory = path.resolve(path.dirname(configPath ?? ".")); + const projectRoot = path.resolve(path.dirname(configPath ?? ".")); const file = path.extname(entryPoint) ? path.resolve(entryPoint) : path.resolve(entryPoint, "index.js"); - const relativePath = path.relative(directory, file) || "."; - return { absolutePath: file, relativePath }; + const relativePath = path.relative(projectRoot, file) || "."; + return { absolutePath: file, relativePath, projectRoot }; } export function resolveEntryWithAssets(): { diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index c93018ee3062..0e2bdad78008 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -193,7 +193,7 @@ export function runBuild( // Check whether we need to watch a Python requirements.txt file. const watchPythonRequirements = getBundleType(entry.format, entry.file) === "python" - ? path.resolve(entry.directory, "requirements.txt") + ? path.resolve(entry.projectRoot, "requirements.txt") : undefined; if (watchPythonRequirements) { diff --git a/packages/wrangler/src/pages/deployment-tails.ts b/packages/wrangler/src/pages/deployment-tails.ts index 96e2a9646302..9e0e9c0cdc57 100644 --- a/packages/wrangler/src/pages/deployment-tails.ts +++ b/packages/wrangler/src/pages/deployment-tails.ts @@ -163,7 +163,9 @@ export async function Handler({ } const deployments: Array = await fetchResult( - `/accounts/${accountId}/pages/projects/${projectName}/deployments` + `/accounts/${accountId}/pages/projects/${projectName}/deployments`, + {}, + new URLSearchParams({ env: environment }) ); const envDeployments = deployments.filter( diff --git a/packages/wrangler/src/pages/deployments.ts b/packages/wrangler/src/pages/deployments.ts index 2de7612ab9f2..6547d8ae1970 100644 --- a/packages/wrangler/src/pages/deployments.ts +++ b/packages/wrangler/src/pages/deployments.ts @@ -23,10 +23,15 @@ export function ListOptions(yargs: CommonYargsArgv) { description: "The name of the project you would like to list deployments for", }, + environment: { + type: "string", + choices: ["production", "preview"], + description: "Environment type to list deployments for", + }, }); } -export async function ListHandler({ projectName }: ListArgs) { +export async function ListHandler({ projectName, environment }: ListArgs) { const config = getConfigCache(PAGES_CONFIG_CACHE_FILENAME); const accountId = await requireAuth(config); @@ -42,7 +47,11 @@ export async function ListHandler({ projectName }: ListArgs) { } const deployments: Array = await fetchResult( - `/accounts/${accountId}/pages/projects/${projectName}/deployments` + `/accounts/${accountId}/pages/projects/${projectName}/deployments`, + {}, + environment + ? new URLSearchParams({ env: environment }) + : new URLSearchParams({}) ); const titleCase = (word: string) => diff --git a/packages/wrangler/src/pages/functions/buildPlugin.ts b/packages/wrangler/src/pages/functions/buildPlugin.ts index 04bef665e0d6..2ac907f29ae0 100644 --- a/packages/wrangler/src/pages/functions/buildPlugin.ts +++ b/packages/wrangler/src/pages/functions/buildPlugin.ts @@ -29,7 +29,7 @@ export function buildPluginFromFunctions({ }: Options) { const entry: Entry = { file: resolve(getBasePath(), "templates/pages-template-plugin.ts"), - directory: functionsDirectory, + projectRoot: functionsDirectory, format: "modules", moduleRoot: functionsDirectory, exports: [], diff --git a/packages/wrangler/src/pages/functions/buildWorker.ts b/packages/wrangler/src/pages/functions/buildWorker.ts index 0320ba09ead9..95c9e4cc4b5b 100644 --- a/packages/wrangler/src/pages/functions/buildWorker.ts +++ b/packages/wrangler/src/pages/functions/buildWorker.ts @@ -55,7 +55,7 @@ export function buildWorkerFromFunctions({ }: Options) { const entry: Entry = { file: resolve(getBasePath(), "templates/pages-template-worker.ts"), - directory: functionsDirectory, + projectRoot: functionsDirectory, format: "modules", moduleRoot: functionsDirectory, exports: [], @@ -151,7 +151,7 @@ export function buildRawWorker({ }: RawOptions) { const entry: Entry = { file: workerScriptPath, - directory: resolve(directory), + projectRoot: resolve(directory), format: "modules", moduleRoot: resolve(directory), exports: [], @@ -240,7 +240,7 @@ export async function produceWorkerBundleForWorkerJSDirectory({ const additionalModules = await findAdditionalModules( { file: entrypoint, - directory: resolve(workerJSDirectory), + projectRoot: resolve(workerJSDirectory), format: "modules", moduleRoot: resolve(workerJSDirectory), exports: [], diff --git a/packages/wrangler/src/versions/index.ts b/packages/wrangler/src/versions/index.ts index cec8c4793333..fb3009d22e30 100644 --- a/packages/wrangler/src/versions/index.ts +++ b/packages/wrangler/src/versions/index.ts @@ -267,7 +267,7 @@ async function versionsUploadHandler( await verifyWorkerMatchesCITag( accountId, name, - path.relative(entry.directory, config.configPath ?? "wrangler.toml") + path.relative(entry.projectRoot, config.configPath ?? "wrangler.toml") ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73acc46b8003..86cb00ea9c0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -741,6 +741,18 @@ importers: specifier: workspace:* version: link:../../packages/wrangler + fixtures/workflow-multiple: + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20241106.0 + version: 4.20241106.0 + undici: + specifier: catalog:default + version: 5.28.4 + wrangler: + specifier: workspace:* + version: link:../../packages/wrangler + packages/chrome-devtools-patches: devDependencies: patch-package: