Skip to content

Commit 6b79d96

Browse files
configure C3 experimental projects to use @cloudflare/autoconfig directly
1 parent 1394867 commit 6b79d96

9 files changed

Lines changed: 513 additions & 22 deletions

File tree

.changeset/c3-autoconfig-direct.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"create-cloudflare": patch
3+
---
4+
5+
Configure experimental projects using `@cloudflare/autoconfig` directly
6+
7+
When scaffolding experimental templates, C3 now runs the autoconfig flow in-process via `@cloudflare/autoconfig` instead of shelling out to `wrangler setup`. This produces the same configuration while making the setup step faster and no longer dependent on the `wrangler` CLI.

packages/create-cloudflare/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@babel/parser": "^7.21.3",
4141
"@babel/types": "^7.21.4",
4242
"@clack/prompts": "^1.2.0",
43+
"@cloudflare/autoconfig": "workspace:*",
4344
"@cloudflare/cli-shared-helpers": "workspace:*",
4445
"@cloudflare/codemod": "workspace:*",
4546
"@cloudflare/mock-npm-registry": "workspace:*",
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { error, log, warn } from "@cloudflare/cli-shared-helpers";
2+
import { inputPrompt } from "@cloudflare/cli-shared-helpers/interactive";
3+
import { isNonInteractiveOrCI } from "@cloudflare/workers-utils";
4+
import { execaCommand } from "execa";
5+
import { beforeEach, describe, test, vi } from "vitest";
6+
import { createC3AutoConfigContext } from "../autoconfig-context";
7+
8+
vi.mock("@cloudflare/cli-shared-helpers");
9+
vi.mock("@cloudflare/cli-shared-helpers/interactive");
10+
vi.mock("@cloudflare/workers-utils");
11+
vi.mock("execa");
12+
13+
describe("createC3AutoConfigContext", () => {
14+
beforeEach(() => {
15+
vi.mocked(isNonInteractiveOrCI).mockReturnValue(false);
16+
});
17+
18+
describe("logger", () => {
19+
test("routes log and info to `log`, joining args with spaces", ({
20+
expect,
21+
}) => {
22+
const { logger } = createC3AutoConfigContext();
23+
24+
logger.log("hello", "world");
25+
logger.info("foo", 42);
26+
27+
expect(log).toHaveBeenCalledWith("hello world");
28+
expect(log).toHaveBeenCalledWith("foo 42");
29+
});
30+
31+
test("routes warn to `warn`", ({ expect }) => {
32+
const { logger } = createC3AutoConfigContext();
33+
34+
logger.warn("careful");
35+
36+
expect(warn).toHaveBeenCalledWith("careful");
37+
});
38+
39+
test("routes error to `error`", ({ expect }) => {
40+
const { logger } = createC3AutoConfigContext();
41+
42+
logger.error("boom");
43+
44+
expect(error).toHaveBeenCalledWith("boom");
45+
});
46+
47+
test("suppresses debug output", ({ expect }) => {
48+
const { logger } = createC3AutoConfigContext();
49+
50+
logger.debug("noisy");
51+
52+
expect(log).not.toHaveBeenCalled();
53+
expect(warn).not.toHaveBeenCalled();
54+
expect(error).not.toHaveBeenCalled();
55+
});
56+
});
57+
58+
describe("dialogs", () => {
59+
test("confirm delegates to inputPrompt with a confirm prompt", async ({
60+
expect,
61+
}) => {
62+
vi.mocked(inputPrompt).mockResolvedValue(true as never);
63+
const { dialogs } = createC3AutoConfigContext();
64+
65+
const result = await dialogs.confirm("Proceed?", { defaultValue: true });
66+
67+
expect(result).toBe(true);
68+
expect(inputPrompt).toHaveBeenCalledWith(
69+
expect.objectContaining({
70+
type: "confirm",
71+
question: "Proceed?",
72+
defaultValue: true,
73+
})
74+
);
75+
});
76+
77+
test("select maps choices to inputPrompt options", async ({ expect }) => {
78+
vi.mocked(inputPrompt).mockResolvedValue("react" as never);
79+
const { dialogs } = createC3AutoConfigContext();
80+
81+
const result = await dialogs.select("Framework?", {
82+
choices: [
83+
{ title: "React", value: "react" },
84+
{ title: "Vue", value: "vue" },
85+
],
86+
defaultOption: 1,
87+
});
88+
89+
expect(result).toBe("react");
90+
expect(inputPrompt).toHaveBeenCalledWith(
91+
expect.objectContaining({
92+
type: "select",
93+
question: "Framework?",
94+
options: [
95+
{ label: "React", value: "react" },
96+
{ label: "Vue", value: "vue" },
97+
],
98+
defaultValue: "vue",
99+
})
100+
);
101+
});
102+
});
103+
104+
describe("runCommand", () => {
105+
test("runs the command in a shell with the given cwd", async ({
106+
expect,
107+
}) => {
108+
vi.mocked(execaCommand).mockResolvedValue({} as never);
109+
const context = createC3AutoConfigContext();
110+
111+
await context.runCommand("npm run build", "/tmp/project", "[build]");
112+
113+
expect(execaCommand).toHaveBeenCalledWith("npm run build", {
114+
shell: true,
115+
cwd: "/tmp/project",
116+
stdio: "inherit",
117+
});
118+
});
119+
});
120+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { error, log, warn } from "@cloudflare/cli-shared-helpers";
2+
import { inputPrompt } from "@cloudflare/cli-shared-helpers/interactive";
3+
import { isNonInteractiveOrCI } from "@cloudflare/workers-utils";
4+
import { execaCommand } from "execa";
5+
import type { AutoConfigContext } from "@cloudflare/autoconfig";
6+
7+
/**
8+
* Joins the variadic arguments passed to a logger method into a single
9+
* space-separated string, matching the behaviour of `console.log`.
10+
*
11+
* @param args - The arguments passed to the logger method.
12+
* @returns A single string with each argument stringified and space-separated.
13+
*/
14+
function stringifyLogArgs(args: unknown[]): string {
15+
return args
16+
.map((arg) => (typeof arg === "string" ? arg : String(arg)))
17+
.join(" ");
18+
}
19+
20+
/**
21+
* Creates an `AutoConfigContext` that wires C3's logging, prompting, and command
22+
* execution infrastructure into the generic `@cloudflare/autoconfig` system.
23+
*
24+
* @returns A fully-configured `AutoConfigContext` for use with `@cloudflare/autoconfig`.
25+
*/
26+
export function createC3AutoConfigContext(): AutoConfigContext {
27+
return {
28+
logger: {
29+
log: (...args) => log(stringifyLogArgs(args)),
30+
info: (...args) => log(stringifyLogArgs(args)),
31+
warn: (...args) => warn(stringifyLogArgs(args)),
32+
error: (...args) => error(stringifyLogArgs(args)),
33+
// C3 has no dedicated debug channel; debug output is suppressed to keep
34+
// the scaffolding output clean.
35+
debug: () => {},
36+
},
37+
dialogs: {
38+
confirm: async (text, options) => {
39+
const nonInteractive = isNonInteractiveOrCI();
40+
const defaultValue = nonInteractive
41+
? (options?.fallbackValue ?? options?.defaultValue ?? false)
42+
: (options?.defaultValue ?? true);
43+
44+
return inputPrompt<boolean>({
45+
type: "confirm",
46+
question: text,
47+
label: "",
48+
defaultValue,
49+
acceptDefault: nonInteractive,
50+
});
51+
},
52+
prompt: async (text, options) => {
53+
return inputPrompt<string>({
54+
type: "text",
55+
question: text,
56+
label: "",
57+
defaultValue: options?.defaultValue,
58+
acceptDefault: isNonInteractiveOrCI(),
59+
validate: options?.validate
60+
? (value) => {
61+
const result = options.validate?.(value as string);
62+
// C3 only ever runs autoconfig with confirmations skipped, so the
63+
// interactive (and possibly async) validation path is never hit.
64+
// Treat anything we can't synchronously resolve as valid.
65+
if (result instanceof Promise) {
66+
return undefined;
67+
}
68+
if (result === true || result === undefined) {
69+
return undefined;
70+
}
71+
return result === false ? "Invalid value" : result;
72+
}
73+
: undefined,
74+
});
75+
},
76+
select: async (text, options) => {
77+
return inputPrompt<string>({
78+
type: "select",
79+
question: text,
80+
label: "",
81+
options: options.choices.map((choice) => ({
82+
label: choice.title,
83+
value: choice.value,
84+
})),
85+
defaultValue: options.choices[options.defaultOption ?? 0]?.value,
86+
acceptDefault: isNonInteractiveOrCI(),
87+
});
88+
},
89+
},
90+
runCommand: async (command, cwd, label) => {
91+
log(`${label} Running: ${command}`);
92+
await execaCommand(command, { shell: true, cwd, stdio: "inherit" });
93+
},
94+
isNonInteractiveOrCI,
95+
};
96+
}

packages/create-cloudflare/src/cli.ts

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { mkdirSync } from "node:fs";
33
import { dirname } from "node:path";
44
import { chdir } from "node:process";
5+
import * as autoConfig from "@cloudflare/autoconfig";
56
import {
67
cancel,
78
checkMacOSVersion,
@@ -15,18 +16,15 @@ import { maybeAppendWranglerToGitIgnore } from "@cloudflare/cli-shared-helpers/g
1516
import { isInteractive } from "@cloudflare/cli-shared-helpers/interactive";
1617
import { cliDefinition, parseArgs, processArgument } from "helpers/args";
1718
import { C3_DEFAULTS, isUpdateAvailable, runLatest } from "helpers/cli";
18-
import { runWranglerCommand } from "helpers/command";
19-
import {
20-
detectPackageManager,
21-
rectifyPmMismatch,
22-
} from "helpers/packageManagers";
19+
import { rectifyPmMismatch } from "helpers/packageManagers";
2320
import { installWrangler, npmInstall } from "helpers/packages";
2421
import {
2522
getPnpmIgnoredBuildsGuidance,
2623
isIgnoredBuildsError,
2724
writePnpmBuildApprovals,
2825
} from "helpers/pnpmBuildApprovals";
2926
import { version } from "../package.json";
27+
import { createC3AutoConfigContext } from "./autoconfig-context";
3028
import { maybeOpenBrowser, offerToDeploy, runDeploy } from "./deploy";
3129
import { printSummary, printWelcomeMessage } from "./dialog";
3230
import { gitCommit, offerGit } from "./git";
@@ -42,7 +40,10 @@ import {
4240
} from "./templates";
4341
import { validateProjectDirectory } from "./validators";
4442
import { addTypes } from "./workers";
45-
import { updateWranglerConfig } from "./wrangler/config";
43+
import {
44+
loadProjectWranglerConfig,
45+
updateWranglerConfig,
46+
} from "./wrangler/config";
4647
import type { C3Args, C3Context } from "types";
4748

4849
export const main = async (argv: string[]) => {
@@ -164,27 +165,16 @@ const create = async (ctx: C3Context) => {
164165
const configure = async (ctx: C3Context) => {
165166
startSection(
166167
`Configuring your application for Cloudflare${
167-
ctx.args.experimental ? ` via \`wrangler setup\`` : ""
168+
ctx.args.experimental ? ` via autoconfig` : ""
168169
}`,
169170
"Step 2 of 3"
170171
);
171172

172-
// This is kept even in the autoconfig case because autoconfig will ultimately end up installing Wrangler anyway
173-
// If we _didn't_ install Wrangler when using autoconfig we'd end up with a double install (one from `npx` and one from autoconfig)
174-
await installWrangler();
175-
176173
if (ctx.args.experimental) {
177-
const { npx } = detectPackageManager();
178-
179-
await runWranglerCommand([
180-
npx,
181-
"wrangler",
182-
"setup",
183-
"--yes",
184-
"--no-completion-message",
185-
"--no-install-wrangler",
186-
]);
174+
await runAutoConfig(ctx);
187175
} else {
176+
await installWrangler();
177+
188178
// Note: This _must_ be called before the configure phase since
189179
// pre-existing workers assume its presence in their configure phase
190180
await updateWranglerConfig(ctx);
@@ -208,6 +198,40 @@ const configure = async (ctx: C3Context) => {
208198
endSection(`Application configured`);
209199
};
210200

201+
/**
202+
* Configures a C3 project for Cloudflare by running the `@cloudflare/autoconfig` flow in-process.
203+
*
204+
* @param ctx - The C3 context for the project being created.
205+
*/
206+
const runAutoConfig = async (ctx: C3Context) => {
207+
await reporter.collectAsyncMetrics({
208+
eventPrefix: "c3 autoconfig",
209+
props: { args: ctx.args },
210+
async promise() {
211+
const context = createC3AutoConfigContext();
212+
213+
const details = await autoConfig.getDetailsForAutoConfig({
214+
projectPath: ctx.project.path,
215+
// Note: the started application might already include a wrangler config file
216+
wranglerConfig: loadProjectWranglerConfig(ctx.project.path),
217+
context,
218+
});
219+
220+
reporter.setEventProperty("framework", details.framework?.id);
221+
reporter.setEventProperty("configured", details.configured);
222+
223+
if (!details.configured) {
224+
await autoConfig.runAutoConfig(details, {
225+
context,
226+
skipConfirmations: true,
227+
runBuild: false,
228+
enableWranglerInstallation: true,
229+
});
230+
}
231+
},
232+
});
233+
};
234+
211235
const deploy = async (ctx: C3Context) => {
212236
if (await offerToDeploy(ctx)) {
213237
await createProject(ctx);

0 commit comments

Comments
 (0)