Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C3: Automatically use the latest version of c3 if not running latest #3803

Merged
merged 7 commits into from
Aug 24, 2023
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
5 changes: 5 additions & 0 deletions .changeset/nervous-insects-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-cloudflare": minor
---

C3: Checks for a newer version of create-cloudflare and uses it if available. This behavior can be suppressed with the --no-auto-update flag.
7 changes: 5 additions & 2 deletions packages/create-cloudflare/e2e-tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tmpdir } from "os";
import { join } from "path";
import { spawn } from "cross-spawn";
import { spinnerFrames } from "helpers/interactive";
import type { SpinnerStyle } from "helpers/interactive";

export const C3_E2E_PREFIX = "c3-e2e-";

Expand Down Expand Up @@ -122,8 +123,10 @@ export const condenseOutput = (lines: string[]) => {

const filterLine = (line: string) => {
// Remove all lines with spinners
for (const frame of spinnerFrames) {
if (line.includes(frame)) return false;
for (const spinnerType of Object.keys(spinnerFrames)) {
for (const frame of spinnerFrames[spinnerType as SpinnerStyle]) {
if (line.includes(frame)) return false;
}
}

// Remove empty lines
Expand Down
101 changes: 90 additions & 11 deletions packages/create-cloudflare/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
#!/usr/bin/env node
// import { TextPrompt, SelectPrompt, ConfirmPrompt } from "@clack/core";
import Haikunator from "haikunator";
import { crash, logRaw, startSection } from "helpers/cli";
import { dim } from "helpers/colors";
import { processArgument } from "helpers/interactive";
import { blue, dim } from "helpers/colors";
import { runCommand } from "helpers/command";
import {
isInteractive,
processArgument,
spinner,
spinnerFrames,
} 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";
Expand All @@ -16,6 +23,7 @@ export const C3_DEFAULTS = {
projectName: new Haikunator().haikunate({ tokenHex: true }),
type: "hello-world",
framework: "angular",
autoUpdate: true,
deploy: true,
git: true,
open: true,
Expand All @@ -27,9 +35,51 @@ const WRANGLER_DEFAULTS = {
deploy: false,
};

const { npm } = detectPackageManager();

export const main = async (argv: string[]) => {
const args = await parseArgs(argv);

// Print a newline
logRaw("");

if (args.autoUpdate && (await isUpdateAvailable())) {
await runLatest();
} else {
await runCli(args);
}
};

// Detects if a newer version of c3 is available by comparing the version
// specified in package.json with the `latest` tag from npm
const isUpdateAvailable = async () => {
if (process.env.VITEST || process.env.CI || !isInteractive()) {
return false;
}

// Use a spinner when running this check since it may take some time
const s = spinner(spinnerFrames.vertical, blue);
s.start("Checking if a newer version is available");
const latestVersion = await runCommand(
`npm info create-cloudflare@latest dist-tags.latest`,
{ silent: true, useSpinner: false }
);
s.stop();

// Don't auto-update to major versions
if (semver.diff(latestVersion, version) === "major") return false;

return semver.gt(latestVersion, version);
};

// Spawn a separate process running the most recent version of c3
export const runLatest = async () => {
const args = process.argv.slice(2);
await runCommand(`${npm} create cloudflare@latest ${args.join(" ")}`);
};

// Entrypoint to c3
export const runCli = async (args: Partial<C3Args>) => {
printBanner();

const projectName = await processArgument<string>(args, "projectName", {
Expand Down Expand Up @@ -79,32 +129,61 @@ export const main = async (argv: string[]) => {
};

const printBanner = () => {
logRaw(dim(`\nusing create-cloudflare version ${version}\n`));
logRaw(dim(`using create-cloudflare version ${version}\n`));
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))
.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" })
.positional("name", {
type: "string",
description:
"The name of your application. Will be used as the directory name",
})
.option("type", {
type: "string",
description: `The base template to use when scaffolding your application`,
})
.option("framework", {
type: "string",
description:
"When using the `webApp` template, specifies the desired framework",
})
.option("deploy", {
type: "boolean",
description: "Deploy your application to Cloudflare after scaffolding",
})
.option("auto-update", {
type: "boolean",
default: C3_DEFAULTS.autoUpdate,
description:
"Automatically uses the latest version of `create-cloudflare`. Set --no-auto-update to disable",
})
.option("ts", {
type: "boolean",
description: "Adds typescript support to your application",
})
.option("git", {
type: "boolean",
description: "Initializes a git repository after scaffolding",
})
.option("open", {
type: "boolean",
default: true,
description:
"opens your browser after your deployment, set --no-open to disable",
"Opens your browser after your deployment, set --no-open to disable",
})
.option("existing-script", {
type: "string",
description:
"An existing workers script to initialize an application from",
hidden: templateMap["pre-existing"].hidden,
})
.option("accept-defaults", {
alias: "y",
description: "Accept all defaults and bypass interactive prompts",
type: "boolean",
})
.option("wrangler-defaults", { type: "boolean", hidden: true })
Expand Down
21 changes: 17 additions & 4 deletions packages/create-cloudflare/src/helpers/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isCancel } from "@clack/prompts";
import logUpdate from "log-update";
import { shapes, cancel, space, status, newline, logRaw } from "./cli";
import { blue, dim, gray, brandColor, bold } from "./colors";
import type { ChalkInstance } from "chalk";
import type { C3Arg, C3Args } from "types";

const grayBar = gray(shapes.bar);
Expand Down Expand Up @@ -259,10 +260,19 @@ const getConfirmRenderers = (config: ConfirmPromptConfig) => {
};
};

export const spinnerFrames = ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"];
export type SpinnerStyle = keyof typeof spinnerFrames;

export const spinnerFrames = {
clockwise: ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"],
vertical: ["▁", "▃", "▄", "▅", "▆", "▇", "▆", "▅", "▄", "▃"],
};

const ellipsisFrames = ["", ".", "..", "...", " ..", " .", ""];

export const spinner = () => {
export const spinner = (
frames: string[] = spinnerFrames.clockwise,
color: ChalkInstance = brandColor
) => {
// Alternative animations we considered. Keeping around in case we
// introduce different animations for different use cases.
// const frames = ["▁", "▃", "▄", "▅", "▆", "▇", "▆", "▅", "▄", "▃"];
Expand All @@ -272,7 +282,6 @@ export const spinner = () => {
// const frames = ["◐", "◓", "◑", "◒"];
// const frames = ["㊂", "㊀", "㊁"];

const color = brandColor;
const frameRate = 120;
let loop: NodeJS.Timer | null = null;
let startMsg: string;
Expand All @@ -296,7 +305,7 @@ export const spinner = () => {
clearLoop();
loop = setInterval(() => {
index++;
const spinnerFrame = spinnerFrames[index % spinnerFrames.length];
const spinnerFrame = frames[index % frames.length];
const ellipsisFrame = ellipsisFrames[index % ellipsisFrames.length];

if (msg) {
Expand All @@ -319,3 +328,7 @@ export const spinner = () => {
},
};
};

export const isInteractive = () => {
return process.stdin.isTTY;
};
1 change: 1 addition & 0 deletions packages/create-cloudflare/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type C3Args = {
deploy?: boolean;
open?: boolean;
git?: boolean;
autoUpdate?: boolean;
// pages specific
framework?: string;
// workers specific
Expand Down
2 changes: 1 addition & 1 deletion packages/create-cloudflare/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

"pipeline": {
"build": {
"env": ["VITEST", "TEST_PM"]
"env": ["VITEST", "TEST_PM", "CI"]
},
"test:e2e:*": {
"env": ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"]
Expand Down
Loading