Skip to content

Commit

Permalink
C3: pass arguments to underlying CLIs (#3807)
Browse files Browse the repository at this point in the history
* pass arguments to underlying CLIs

* reafactor code and add unit tests for parseArgs
  • Loading branch information
dario-piotrowicz authored Aug 24, 2023
1 parent 3db3451 commit fac199b
Show file tree
Hide file tree
Showing 14 changed files with 282 additions and 105 deletions.
15 changes: 15 additions & 0 deletions .changeset/clever-cooks-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"create-cloudflare": patch
---

adjusted arguments passing so that arguments following an extra `--` are
passed to the underlying cli (if any)

For example:

```
$ npm create cloudflare -- --framework=X -- -a -b
```

now will run the framework X's cli with the `-a` and `-b` arguments
(such arguments will be completely ignored by C3)
1 change: 1 addition & 0 deletions packages/create-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"test:e2e:npm": "npm run build && TEST_PM=npm vitest run --config ./vitest-e2e.config.ts",
"test:e2e:pnpm": "npm run build && TEST_PM=pnpm vitest run --config ./vitest-e2e.config.ts",
"test:unit": "vitest run --config ./vitest.config.ts",
"test:unit:watch": "vitest --config ./vitest.config.ts",
"watch": "node -r esbuild-register scripts/build.ts --watch"
},
"devDependencies": {
Expand Down
91 changes: 21 additions & 70 deletions packages/create-cloudflare/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,24 @@
#!/usr/bin/env node
import Haikunator from "haikunator";
import { crash, logRaw, startSection } from "helpers/cli";
import { processArgument } from "helpers/args";
import {
C3_DEFAULTS,
WRANGLER_DEFAULTS,
crash,
logRaw,
startSection,
} from "helpers/cli";
import { blue, dim } from "helpers/colors";
import { runCommand } from "helpers/command";
import {
isInteractive,
processArgument,
spinner,
spinnerFrames,
} from "helpers/interactive";
import { isInteractive, spinnerFrames, spinner } from "helpers/interactive";
import { detectPackageManager } from "helpers/packages";
import semver from "semver";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { version } from "../package.json";
import { validateProjectDirectory } from "./common";
import { runPagesGenerator } from "./pages";
import { runWorkersGenerator } from "./workers";
import { templateMap } from "./templateMap";
import type { C3Args } from "types";

export const C3_DEFAULTS = {
projectName: new Haikunator().haikunate({ tokenHex: true }),
type: "hello-world",
framework: "angular",
autoUpdate: true,
deploy: true,
git: true,
open: true,
ts: true,
};

const WRANGLER_DEFAULTS = {
...C3_DEFAULTS,
deploy: false,
};

const { npm } = detectPackageManager();

export const main = async (argv: string[]) => {
Expand Down Expand Up @@ -133,8 +117,16 @@ const printBanner = () => {
startSection(`Create an application with Cloudflare`, "Step 1 of 3");
};

export const parseArgs = async (argv: string[]): Promise<Partial<C3Args>> => {
const args = await yargs(hideBin(argv))
const parseArgs = async (argv: string[]): Promise<Partial<C3Args>> => {
const doubleDashesIdx = argv.indexOf("--");
const c3Args = argv.slice(
0,
doubleDashesIdx < 0 ? undefined : doubleDashesIdx
);
const additionalArgs =
doubleDashesIdx < 0 ? [] : argv.slice(doubleDashesIdx + 1);

const args = await yargs(hideBin(c3Args))
.scriptName("create-cloudflare")
.usage("$0 [args]")
.positional("name", {
Expand Down Expand Up @@ -194,50 +186,9 @@ export const parseArgs = async (argv: string[]): Promise<Partial<C3Args>> => {
...(args.wranglerDefaults && WRANGLER_DEFAULTS),
...(args.acceptDefaults && C3_DEFAULTS),
projectName: args._[0] as string | undefined,
additionalArgs,
...args,
};
};

type TemplateConfig = {
label: string;
handler: (args: C3Args) => Promise<void>;
hidden?: boolean;
};

const templateMap: Record<string, TemplateConfig> = {
webFramework: {
label: "Website or web app",
handler: runPagesGenerator,
},
"hello-world": {
label: `"Hello World" Worker`,
handler: runWorkersGenerator,
},
common: {
label: "Example router & proxy Worker",
handler: runWorkersGenerator,
},
scheduled: {
label: "Scheduled Worker (Cron Trigger)",
handler: runWorkersGenerator,
},
queues: {
label: "Queue consumer & producer Worker",
handler: runWorkersGenerator,
},
chatgptPlugin: {
label: `ChatGPT plugin`,
handler: (args) =>
runWorkersGenerator({
...args,
ts: true,
}),
},
"pre-existing": {
label: "Pre-existing Worker (from Dashboard)",
handler: runWorkersGenerator,
hidden: true,
},
};

main(process.argv).catch((e) => crash(e));
5 changes: 3 additions & 2 deletions packages/create-cloudflare/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { existsSync, mkdirSync, readdirSync } from "fs";
import { basename, dirname, resolve } from "path";
import { chdir } from "process";
import { getFrameworkCli } from "frameworks/index";
import { processArgument } from "helpers/args";
import {
C3_DEFAULTS,
crash,
endSection,
log,
Expand All @@ -21,12 +23,11 @@ import {
runCommands,
wranglerLogin,
} from "helpers/command";
import { inputPrompt, processArgument, spinner } from "helpers/interactive";
import { inputPrompt, spinner } from "helpers/interactive";
import { detectPackageManager } from "helpers/packages";
import { poll } from "helpers/poll";
import { version as wranglerVersion } from "wrangler/package.json";
import { version } from "../package.json";
import { C3_DEFAULTS } from "./cli";
import type { C3Args, PagesGeneratorContext } from "types";

const { name, npm } = detectPackageManager();
Expand Down
2 changes: 1 addition & 1 deletion packages/create-cloudflare/src/frameworks/next/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mkdirSync } from "fs";
import { processArgument } from "helpers/args";
import { updateStatus, warn } from "helpers/cli";
import { brandColor, dim } from "helpers/colors";
import { installPackages, runFrameworkGenerator } from "helpers/command";
Expand All @@ -10,7 +11,6 @@ import {
writeFile,
writeJSON,
} from "helpers/files";
import { processArgument } from "helpers/interactive";
import { detectPackageManager } from "helpers/packages";
import { getFrameworkCli } from "../index";
import {
Expand Down
83 changes: 83 additions & 0 deletions packages/create-cloudflare/src/helpers/__tests__/args.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, expect, test, vi } from "vitest";
import { parseArgs } from "../args";

vi.mock("yargs/helpers", () => ({ hideBin: (x: string[]) => x }));

describe("Cli", () => {
describe("parseArgs", () => {
test("parsing the first argument as the projectName", async () => {
const result = await parseArgs(["my-project"]);
expect(result.projectName).toBe("my-project");
});

test("not parsing first argument as the projectName if it is after --", async () => {
const result = await parseArgs(["--", "my-project"]);
expect(result.projectName).toBeFalsy();
});

test("parsing optional C3 arguments correctly", async () => {
const result = await parseArgs(["--framework", "angular", "--ts=true"]);
expect(result.projectName).toBeFalsy();
expect(result.framework).toEqual("angular");
expect(result.ts).toEqual(true);
expect(result.additionalArgs).toEqual([]);
});

test("parsing positional + optional C3 arguments correctly", async () => {
const result = await parseArgs([
"my-project",
"--framework",
"angular",
"--deploy",
"true",
"--git=false",
]);
expect(result.projectName).toEqual("my-project");
expect(result.framework).toEqual("angular");
expect(result.deploy).toEqual(true);
expect(result.git).toEqual(false);
expect(result.additionalArgs).toEqual([]);
});

test("parsing optional C3 arguments + additional arguments correctly", async () => {
const result = await parseArgs([
"--framework",
"react",
"--ts=true",
"--",
"positional-arg",
"--react-option",
"5",
]);
expect(result.projectName).toBeFalsy();
expect(result.framework).toEqual("react");
expect(result.ts).toEqual(true);
expect(result.additionalArgs).toEqual([
"positional-arg",
"--react-option",
"5",
]);
});

test("parsing positional + optional C3 arguments + additional arguments correctly", async () => {
const result = await parseArgs([
"my-react-project",
"--framework",
"react",
"--ts=true",
"--",
"positional-arg",
"--react-option",
"5",
]);
expect(result.projectName).toBe("my-react-project");
expect(result.framework).toEqual("react");
expect(result.ts).toEqual(true);
expect(result.additionalArgs).toEqual([
"positional-arg",
"--react-option",
"5",
]);
});
});
});
76 changes: 76 additions & 0 deletions packages/create-cloudflare/src/helpers/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { version } from "../../package.json";
import { templateMap } from "../templateMap";
import { C3_DEFAULTS, WRANGLER_DEFAULTS, logRaw } from "./cli";
import { getRenderers, inputPrompt } from "./interactive";
import type { PromptConfig } from "./interactive";
import type { C3Args } from "types";

export const parseArgs = async (argv: string[]): Promise<Partial<C3Args>> => {
const doubleDashesIdx = argv.indexOf("--");
const c3Args = argv.slice(
0,
doubleDashesIdx < 0 ? undefined : doubleDashesIdx
);
const additionalArgs =
doubleDashesIdx < 0 ? [] : argv.slice(doubleDashesIdx + 1);

const args = await yargs(hideBin(c3Args))
.scriptName("create-cloudflare")
.usage("$0 [args]")
.positional("name", { type: "string" })
.option("type", { type: "string" })
.option("framework", { type: "string" })
.option("deploy", { type: "boolean" })
.option("ts", { type: "boolean" })
.option("git", { type: "boolean" })
.option("open", {
type: "boolean",
default: true,
description:
"opens your browser after your deployment, set --no-open to disable",
})
.option("existing-script", {
type: "string",
hidden: templateMap["pre-existing"].hidden,
})
.option("accept-defaults", {
alias: "y",
type: "boolean",
})
.option("wrangler-defaults", { type: "boolean", hidden: true })
.version(version)
.help().argv;

return {
...(args.wranglerDefaults && WRANGLER_DEFAULTS),
...(args.acceptDefaults && C3_DEFAULTS),
projectName: args._[0] as string | undefined,
additionalArgs,
...args,
};
};

export const processArgument = async <T>(
args: Partial<C3Args>,
name: keyof C3Args,
promptConfig: PromptConfig
) => {
let value = args[name];
const renderSubmitted = getRenderers(promptConfig).submit;

// If the value has already been set via args, use that
if (value !== undefined) {
promptConfig.validate?.(value);

const lines = renderSubmitted({ value });
logRaw(lines.join("\n"));

return value as T;
}

value = await inputPrompt(promptConfig);

return value as T;
};
17 changes: 17 additions & 0 deletions packages/create-cloudflare/src/helpers/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { exit } from "process";
import Haikunator from "haikunator";
import open from "open";
import { brandColor, dim, gray, white, red, hidden, bgRed } from "./colors";

Expand Down Expand Up @@ -125,3 +126,19 @@ export async function openInBrowser(url: string): Promise<void> {
warn("Failed to open browser");
});
}

export const C3_DEFAULTS = {
projectName: new Haikunator().haikunate({ tokenHex: true }),
type: "hello-world",
framework: "angular",
autoUpdate: true,
deploy: true,
git: true,
open: true,
ts: true,
};

export const WRANGLER_DEFAULTS = {
...C3_DEFAULTS,
deploy: false,
};
4 changes: 4 additions & 0 deletions packages/create-cloudflare/src/helpers/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ export const runFrameworkGenerator = async (
ctx: PagesGeneratorContext,
cmd: string
) => {
if (ctx.framework?.args?.length) {
cmd = `${cmd} ${ctx.framework.args.join(" ")}`;
}

endSection(
`Continue with ${ctx.framework?.config.displayName}`,
`via \`${cmd.trim()}\``
Expand Down
Loading

0 comments on commit fac199b

Please sign in to comment.