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

Filter by extension

Filter by extension

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

Add Pages detection to autoconfig flows

When running the autoconfig logic (via `wrangler setup`, `wrangler deploy --x-autoconfig`, or the programmatic autoconfig API), Wrangler now detects when a project appears to be a Pages project and handles it appropriately:

- For `wrangler deploy`, it warns the user but still allows them to proceed
- For `wrangler setup` and the programmatic autoconfig API, it throws a fatal error
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { seed } from "@cloudflare/workers-utils/test-helpers";
/* eslint-disable workers-sdk/no-vitest-import-expect -- it.each patterns */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
/* eslint-enable workers-sdk/no-vitest-import-expect */
import * as details from "../../../autoconfig/details";
import * as configCache from "../../../config-cache";
import { clearOutputFilePath } from "../../../output";
import {
getPackageManager,
NpmPackageManager,
PnpmPackageManager,
} from "../../../package-manager";
import { PAGES_CONFIG_CACHE_FILENAME } from "../../../pages/constants";
import { mockConsoleMethods } from "../../helpers/mock-console";
import { mockConfirm } from "../../helpers/mock-dialogs";
import { useMockIsTTY } from "../../helpers/mock-istty";
import { runInTempDir } from "../../helpers/run-in-tmp";
import type { Config } from "@cloudflare/workers-utils";
Expand Down Expand Up @@ -205,4 +209,107 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => {
workerName: "overridden-worker-name",
});
});

describe("Pages project detection", () => {
it("should detect Pages project when pages_build_output_dir is set in wrangler config", async () => {
await seed({
"public/index.html": `<h1>Hello World</h1>`,
});

const result = await details.getDetailsForAutoConfig({
wranglerConfig: {
configPath: "/tmp/wrangler.toml",
pages_build_output_dir: "./dist",
} as Config,
});

expect(result.configured).toBe(false);
expect(result.framework?.id).toBe("cloudflare-pages");
expect(result.framework?.name).toBe("Cloudflare Pages");
});

it("should detect Pages project when pages.json cache file exists", async () => {
const cacheFolder = join(process.cwd(), ".cache");
await seed({
"public/index.html": `<h1>Hello World</h1>`,
// Create a cache folder in the temp directory and add pages.json to it
[join(cacheFolder, PAGES_CONFIG_CACHE_FILENAME)]: JSON.stringify({
account_id: "test-account",
}),
});

// Mock getCacheFolder to return our temp cache folder
const getCacheFolderSpy = vi
.spyOn(configCache, "getCacheFolder")
.mockReturnValue(cacheFolder);

try {
const result = await details.getDetailsForAutoConfig();

expect(result.framework?.id).toBe("cloudflare-pages");
expect(result.framework?.name).toBe("Cloudflare Pages");
} finally {
getCacheFolderSpy.mockRestore();
}
});

it("should detect Pages project when functions directory exists, no framework is detected and the user confirms that it is", async () => {
await seed({
"public/index.html": `<h1>Hello World</h1>`,
"functions/hello.js": `
export function onRequest(context) {
return new Response("Hello, world!");
}
`,
});

mockConfirm({
text: "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?",
result: true,
});

const result = await details.getDetailsForAutoConfig();

expect(result.framework?.id).toBe("cloudflare-pages");
expect(result.framework?.name).toBe("Cloudflare Pages");
});

it("should not detect Pages project when the user denies that, even it the functions directory exists and no framework is detected", async () => {
await seed({
"public/index.html": `<h1>Hello World</h1>`,
"functions/hello.js": `
export function onRequest(context) {
return new Response("Hello, world!");
}
`,
});

mockConfirm({
text: "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?",
result: false,
});

const result = await details.getDetailsForAutoConfig();

expect(result.framework?.id).toBe("static");
expect(result.framework?.name).toBe("Static");
});

it("should not detect Pages project when functions directory exists but a framework is detected", async () => {
await seed({
"functions/hello.js":
"export const myFun = () => { console.log('Hello!'); };",
"package.json": JSON.stringify({
dependencies: {
astro: "5",
},
}),
});

const result = await details.getDetailsForAutoConfig();

// Should detect Astro, not Pages
expect(result.framework?.id).toBe("astro");
});
});
});
86 changes: 86 additions & 0 deletions packages/wrangler/src/__tests__/autoconfig/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,42 @@ describe("autoconfig (deploy)", () => {
expect(runSpy).not.toHaveBeenCalled();
});

it("should warn and prompt when Pages project is detected", async () => {
vi.spyOn(details, "getDetailsForAutoConfig").mockImplementationOnce(() =>
Promise.resolve({
configured: false,
projectPath: process.cwd(),
workerName: "my-worker",
framework: {
id: "cloudflare-pages",
name: "Cloudflare Pages",
autoConfigSupported: false,
configure: async () => ({ wranglerConfig: {} }),
isConfigured: () => false,
},
outputDir: "public",
})
);
const runSpy = vi.spyOn(run, "runAutoConfig");

// User declines to proceed
mockConfirm({
text: "Are you sure that you want to proceed?",
result: false,
});

// Should not throw - just return early
await runWrangler("deploy --x-autoconfig");

// Should show warning about Pages project
expect(std.warn).toContain(
"It seems that you have run `wrangler deploy` on a Pages project"
);

// Should NOT run autoconfig since it's a Pages project
expect(runSpy).not.toHaveBeenCalled();
});

describe("runAutoConfig()", () => {
let installSpy: MockInstance;
beforeEach(() => {
Expand Down Expand Up @@ -392,5 +428,55 @@ describe("autoconfig (deploy)", () => {
`[AssertionError: The Output Directory is unexpectedly missing]`
);
});

it("errors with Pages-specific message when framework is cf-pages", async () => {
mockConfirm({
text: "Do you want to modify these settings?",
result: false,
});

await expect(
run.runAutoConfig({
projectPath: process.cwd(),
configured: false,
framework: {
id: "cloudflare-pages",
name: "Cloudflare Pages",
autoConfigSupported: false,
configure: async () => ({ wranglerConfig: {} }),
isConfigured: () => false,
},
workerName: "my-worker",
outputDir: "dist",
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The target project seems to be using Cloudflare Pages. Automatically migrating from a Pages project to a Workers one is not yet supported.]`
);
});

it("errors with generic message when unsupported framework is not cf-pages", async () => {
mockConfirm({
text: "Do you want to modify these settings?",
result: false,
});

await expect(
run.runAutoConfig({
projectPath: process.cwd(),
configured: false,
framework: {
id: "some-unsupported",
name: "Some Unsupported Framework",
autoConfigSupported: false,
configure: async () => ({ wranglerConfig: {} }),
isConfigured: () => false,
},
workerName: "my-worker",
outputDir: "dist",
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The detected framework ("Some Unsupported Framework") cannot be automatically configured.]`
);
});
});
});
74 changes: 74 additions & 0 deletions packages/wrangler/src/__tests__/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,80 @@ describe("deploy", () => {
);
});

it("should attempt to run the autoconfig flow when pages_build_output_dir and (--x-autoconfig is used)", async () => {
writeWranglerConfig({
pages_build_output_dir: "public",
name: "test-name",
});

const getDetailsForAutoConfigSpy = vi
.spyOn(await import("../autoconfig/details"), "getDetailsForAutoConfig")
.mockResolvedValue({
configured: false,
projectPath: process.cwd(),
workerName: "test-name",
framework: {
id: "cloudflare-pages",
name: "Cloudflare Pages",
autoConfigSupported: false,
configure: async () => ({ wranglerConfig: {} }),
isConfigured: () => false,
},
outputDir: "public",
});

mockConfirm({
text: "Are you sure that you want to proceed?",
options: { defaultValue: false },
result: false,
});

await runWrangler("deploy --x-autoconfig");

expect(getDetailsForAutoConfigSpy).toHaveBeenCalled();

expect(std.warn).toContain(
"It seems that you have run `wrangler deploy` on a Pages project"
);
});

it("in non-interactive mode, attempts to deploy a Pages project when --x-autoconfig is used", async () => {
setIsTTY(false);
writeWranglerConfig({
pages_build_output_dir: "public",
name: "test-name",
});

const getDetailsForAutoConfigSpy = vi
.spyOn(await import("../autoconfig/details"), "getDetailsForAutoConfig")
.mockResolvedValue({
configured: false,
projectPath: process.cwd(),
workerName: "test-name",
framework: {
id: "cloudflare-pages",
name: "Cloudflare Pages",
autoConfigSupported: false,
configure: async () => ({ wranglerConfig: {} }),
isConfigured: () => false,
},
outputDir: "public",
});

// The command will fail later due to missing entry-point, but we can still verify
// that the deployment of the (Pages) project was attempted
await expect(runWrangler("deploy --x-autoconfig")).rejects.toThrow();

expect(getDetailsForAutoConfigSpy).toHaveBeenCalled();

expect(std.warn).toContain(
"It seems that you have run `wrangler deploy` on a Pages project"
);
expect(std.out).toContain(
"Using fallback value in non-interactive context: yes"
);
});

describe("output additional script information", () => {
it("for first party workers, it should print worker information at log level", async () => {
setIsTTY(false);
Expand Down
Loading
Loading