Skip to content

Commit

Permalink
feat: Add wrangler pages project list (#757)
Browse files Browse the repository at this point in the history
Add wrangler pages project list to list your Cloudflare Pages projects
  • Loading branch information
sidharthachatterjee committed Apr 5, 2022
1 parent c63ea3d commit 13e57cd
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 22 deletions.
7 changes: 7 additions & 0 deletions .changeset/lucky-paws-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

feature: Add wrangler pages project list

Adds a new command to list your projects in Cloudflare Pages.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
75 changes: 75 additions & 0 deletions packages/wrangler/src/__tests__/pages.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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);
});
});
});
23 changes: 1 addition & 22 deletions packages/wrangler/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ import {
login,
logout,
listScopes,
loginOrRefreshIfRequired,
getAccountId,
validateScopeKeys,
requireAuth,
} from "./user";
import { whoami } from "./whoami";

Expand Down Expand Up @@ -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<string> {
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.
*
Expand Down
69 changes: 69 additions & 0 deletions packages/wrangler/src/pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
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.
Expand Down Expand Up @@ -1059,6 +1079,29 @@ export const pages: BuilderCallback<unknown, unknown> = (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<Project> = 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(<Table data={data}></Table>);
}
)
);
};

Expand All @@ -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<Array<Project>> => {
const pageSize = 10;
let page = 1;
const results = [];
while (results.length % pageSize === 0) {
const json: Array<Project> = 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;
};
21 changes: 21 additions & 0 deletions packages/wrangler/src/user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string> {
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;
}

0 comments on commit 13e57cd

Please sign in to comment.