From 13e57cdca626cf0f38640c4aab1aa1ee1969312b Mon Sep 17 00:00:00 2001 From: Sidhartha Chatterjee Date: Tue, 5 Apr 2022 13:01:50 +0100 Subject: [PATCH] feat: Add wrangler pages project list (#757) Add wrangler pages project list to list your Cloudflare Pages projects --- .changeset/lucky-paws-report.md | 7 ++ package-lock.json | 11 +++ package.json | 1 + packages/wrangler/src/__tests__/pages.test.ts | 75 +++++++++++++++++++ packages/wrangler/src/index.tsx | 23 +----- packages/wrangler/src/pages.tsx | 69 +++++++++++++++++ packages/wrangler/src/user.tsx | 21 ++++++ 7 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 .changeset/lucky-paws-report.md diff --git a/.changeset/lucky-paws-report.md b/.changeset/lucky-paws-report.md new file mode 100644 index 000000000000..fbbc12e15cda --- /dev/null +++ b/.changeset/lucky-paws-report.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +feature: Add wrangler pages project list + +Adds a new command to list your projects in Cloudflare Pages. diff --git a/package-lock.json b/package-lock.json index 7adf7eb6862d..e55c3fcff94a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "jest": "^27.5.1", "npm-run-all": "^4.1.5", "prettier": "^2.5.1", + "timeago.js": "^4.0.2", "typescript": "^4.6.2" }, "engines": { @@ -16283,6 +16284,11 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" }, + "node_modules/timeago.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", + "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" + }, "node_modules/tmp": { "version": "0.0.33", "license": "MIT", @@ -28458,6 +28464,11 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" }, + "timeago.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", + "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" + }, "tmp": { "version": "0.0.33", "requires": { diff --git a/package.json b/package.json index 26032a7c65f6..466f4ab92d30 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "jest": "^27.5.1", "npm-run-all": "^4.1.5", "prettier": "^2.5.1", + "timeago.js": "^4.0.2", "typescript": "^4.6.2" }, "scripts": { diff --git a/packages/wrangler/src/__tests__/pages.test.ts b/packages/wrangler/src/__tests__/pages.test.ts index 15ae1981b7d1..3d8202790865 100644 --- a/packages/wrangler/src/__tests__/pages.test.ts +++ b/packages/wrangler/src/__tests__/pages.test.ts @@ -1,6 +1,9 @@ +import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; +import { setMockResponse, unsetAllMocks } from "./helpers/mock-cfetch"; import { mockConsoleMethods } from "./helpers/mock-console"; import { runInTempDir } from "./helpers/run-in-tmp"; import { runWrangler } from "./helpers/run-wrangler"; +import type { Project } from "../pages"; describe("subcommand implicit help ran on incomplete command execution", () => { runInTempDir(); @@ -54,4 +57,76 @@ describe("subcommand implicit help ran on incomplete command execution", () => { ); }); }); + + describe("project list", () => { + mockAccountId(); + mockApiToken(); + + afterEach(() => { + unsetAllMocks(); + }); + function mockListRequest(projects: unknown[]) { + const requests = { count: 0 }; + setMockResponse( + "/accounts/:accountId/pages/projects", + ([_url, accountId], init, query) => { + requests.count++; + expect(accountId).toEqual("some-account-id"); + expect(query.get("per_page")).toEqual("10"); + expect(query.get("page")).toEqual(`${requests.count}`); + expect(init).toEqual({}); + const pageSize = Number(query.get("per_page")); + const page = Number(query.get("page")); + return projects.slice((page - 1) * pageSize, page * pageSize); + } + ); + return requests; + } + + it("should make request to list projects", async () => { + const projects: Project[] = [ + { + name: "dogs", + domains: ["dogs.pages.dev"], + source: { + type: "github", + }, + latest_deployment: { + modified_on: "2021-11-17T14:52:26.133835Z", + }, + }, + { + name: "cats", + domains: ["cats.pages.dev", "kitten.pages.dev"], + latest_deployment: { + modified_on: "2021-11-17T14:52:26.133835Z", + }, + }, + ]; + + const requests = mockListRequest(projects); + await runWrangler("pages project list"); + + expect(requests.count).toBe(1); + }); + + it("should make multiple requests for paginated results", async () => { + const projects: Project[] = []; + for (let i = 0; i < 15; i++) { + projects.push({ + name: "dogs" + i, + domains: [i + "dogs.pages.dev"], + source: { + type: "github", + }, + latest_deployment: { + modified_on: "2021-11-17T14:52:26.133835Z", + }, + }); + } + const requests = mockListRequest(projects); + await runWrangler("pages project list"); + expect(requests.count).toEqual(2); + }); + }); }); diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index b1403bf507ca..a5d61bfdc60a 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -48,9 +48,8 @@ import { login, logout, listScopes, - loginOrRefreshIfRequired, - getAccountId, validateScopeKeys, + requireAuth, } from "./user"; import { whoami } from "./whoami"; @@ -133,26 +132,6 @@ function getLegacyScriptName( : args.name ?? config.name; } -/** - * Ensure that a user is logged in, and a valid account_id is available. - */ -async function requireAuth( - config: Config, - isInteractive = true -): Promise { - const loggedIn = await loginOrRefreshIfRequired(isInteractive); - if (!loggedIn) { - // didn't login, let's just quit - throw new Error("Did not login, quitting..."); - } - const accountId = config.account_id || (await getAccountId(isInteractive)); - if (!accountId) { - throw new Error("No account id found, quitting..."); - } - - return accountId; -} - /** * Get a promise to the streamed input from stdin. * diff --git a/packages/wrangler/src/pages.tsx b/packages/wrangler/src/pages.tsx index 612176fa645a..ace4df8f13d9 100644 --- a/packages/wrangler/src/pages.tsx +++ b/packages/wrangler/src/pages.tsx @@ -6,19 +6,39 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { URL } from "node:url"; import { watch } from "chokidar"; +import { render } from "ink"; +import Table from "ink-table"; import { getType } from "mime"; +import React from "react"; +import { format } from "timeago.js"; import { buildWorker } from "../pages/functions/buildWorker"; import { generateConfigFromFileTree } from "../pages/functions/filepath-routing"; import { writeRoutesModule } from "../pages/functions/routes"; +import { fetchResult } from "./cfetch"; +import { readConfig } from "./config"; import { FatalError } from "./errors"; import openInBrowser from "./open-in-browser"; import { toUrlPath } from "./paths"; +import { requireAuth } from "./user"; import type { Config } from "../pages/functions/routes"; import type { Headers, Request, fetch } from "@miniflare/core"; import type { BuildResult } from "esbuild"; import type { MiniflareOptions } from "miniflare"; import type { BuilderCallback } from "yargs"; +type ConfigPath = string | undefined; + +export type Project = { + name: string; + domains: Array; + source?: { + type: string; + }; + latest_deployment: { + modified_on: string; + }; +}; + // Defer importing miniflare until we really need it. This takes ~0.5s // and also modifies some `stream/web` and `undici` prototypes, so we // don't want to do this if pages commands aren't being called. @@ -1059,6 +1079,29 @@ export const pages: BuilderCallback = (yargs) => { }); } ) + ) + .command("project", false, (yargs) => + yargs.command( + "list", + "List your Cloudflare Pages projects", + () => {}, + async (args) => { + const config = readConfig(args.config as ConfigPath, args); + const accountId = await requireAuth(config); + + const projects: Array = await listProjects({ accountId }); + + const data = projects.map((project) => { + return { + "Project Name": project.name, + "Project Domains": `${project.domains.join(", ")}`, + "Git Provider": project.source ? "Yes" : "No", + "Last Modified": format(project.latest_deployment.modified_on), + }; + }); + render(
); + } + ) ); }; @@ -1067,3 +1110,29 @@ const invalidAssetsFetch: typeof fetch = () => { "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode." ); }; + +const listProjects = async ({ + accountId, +}: { + accountId: string; +}): Promise> => { + const pageSize = 10; + let page = 1; + const results = []; + while (results.length % pageSize === 0) { + const json: Array = await fetchResult( + `/accounts/${accountId}/pages/projects`, + {}, + new URLSearchParams({ + per_page: pageSize.toString(), + page: page.toString(), + }) + ); + page++; + results.push(...json); + if (json.length < pageSize) { + break; + } + } + return results; +}; diff --git a/packages/wrangler/src/user.tsx b/packages/wrangler/src/user.tsx index ee4d94813e58..27722552ea42 100644 --- a/packages/wrangler/src/user.tsx +++ b/packages/wrangler/src/user.tsx @@ -223,6 +223,7 @@ import { getCloudflareApiBaseUrl } from "./cfetch"; import { getEnvironmentVariableFactory } from "./environment-variables"; import openInBrowser from "./open-in-browser"; import { readFileSync } from "./parse"; +import type { Config } from "./config"; import type { Item as SelectInputItem } from "ink-select-input/build/SelectInput"; import type { ParsedUrlQuery } from "node:querystring"; import type { Response } from "undici"; @@ -1143,3 +1144,23 @@ export function ChooseAccount(props: { ); } + +/** + * Ensure that a user is logged in, and a valid account_id is available. + */ +export async function requireAuth( + config: Config, + isInteractive = true +): Promise { + const loggedIn = await loginOrRefreshIfRequired(isInteractive); + if (!loggedIn) { + // didn't login, let's just quit + throw new Error("Did not login, quitting..."); + } + const accountId = config.account_id || (await getAccountId(isInteractive)); + if (!accountId) { + throw new Error("No account id found, quitting..."); + } + + return accountId; +}