From af669a1983a02adc1b997798869b2b4260c10891 Mon Sep 17 00:00:00 2001 From: Carmen Popoviciu Date: Fri, 12 Aug 2022 00:23:40 +0200 Subject: [PATCH] feat[pages]: Add new internal pages functions `optimize-routes` command (#1648) This PR adds a new pages functions command called `optimize-routes`. This is an internal-use ONLY command at the moment, and is therefore not exposed to external consumers. The command is meant to parse a given `_routes.json` file, optimize and consolidate the route paths defined in it, and output the resulting optimized file. Co-authored-by: Carmen Popoviciu --- .changeset/gorgeous-beds-fetch.md | 5 + packages/wrangler/src/__tests__/pages.test.ts | 14 ++ packages/wrangler/src/pages/functions.tsx | 96 ++++++++++++++ .../pages/functions/routes-transformation.ts | 3 - packages/wrangler/src/pages/index.tsx | 120 ++++++++++-------- 5 files changed, 180 insertions(+), 58 deletions(-) create mode 100644 .changeset/gorgeous-beds-fetch.md create mode 100644 packages/wrangler/src/pages/functions.tsx diff --git a/.changeset/gorgeous-beds-fetch.md b/.changeset/gorgeous-beds-fetch.md new file mode 100644 index 000000000000..c686f795009a --- /dev/null +++ b/.changeset/gorgeous-beds-fetch.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Implement new wrangler pages functions optimize-routes command diff --git a/packages/wrangler/src/__tests__/pages.test.ts b/packages/wrangler/src/__tests__/pages.test.ts index 88925c808c4a..cb88d6a99265 100644 --- a/packages/wrangler/src/__tests__/pages.test.ts +++ b/packages/wrangler/src/__tests__/pages.test.ts @@ -97,6 +97,20 @@ describe("pages", () => { expect(std.out).toMatchInlineSnapshot(` "🚧 'wrangler pages ' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose + If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose" + `); + }); + + it("should display for pages:functions:optimize-routes", async () => { + await expect( + runWrangler( + 'pages functions optimize-routes --routes-path="/build/_routes.json" --output-routes-path="/build/_optimized-routes.json"' + ) + ).rejects.toThrowError(); + + expect(std.out).toMatchInlineSnapshot(` + "🚧 'wrangler pages ' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose + If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose" `); }); diff --git a/packages/wrangler/src/pages/functions.tsx b/packages/wrangler/src/pages/functions.tsx new file mode 100644 index 000000000000..23f331b6adfc --- /dev/null +++ b/packages/wrangler/src/pages/functions.tsx @@ -0,0 +1,96 @@ +import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { FatalError } from "../errors"; +import { logger } from "../logger"; +import { isInPagesCI, ROUTES_SPEC_VERSION } from "./constants"; +import { + isRoutesJSONSpec, + optimizeRoutesJSONSpec, +} from "./functions/routes-transformation"; +import { pagesBetaWarning } from "./utils"; +import type { YargsOptionsToInterface } from "./types"; +import type { Argv } from "yargs"; + +type OptimizeRoutesArgs = YargsOptionsToInterface; + +export function OptimizeRoutesOptions(yargs: Argv) { + return yargs + .options({ + "routes-path": { + type: "string", + demandOption: true, + description: "The location of the _routes.json file", + }, + }) + .options({ + "output-routes-path": { + type: "string", + demandOption: true, + description: "The location of the optimized output routes file", + }, + }); +} + +export async function OptimizeRoutesHandler({ + routesPath, + outputRoutesPath, +}: OptimizeRoutesArgs) { + if (!isInPagesCI) { + // Beta message for `wrangler pages ` usage + logger.log(pagesBetaWarning); + } + + let routesFileContents: string; + const routesOutputDirectory = path.dirname(outputRoutesPath); + + if (!existsSync(routesPath)) { + throw new FatalError( + `Oops! File ${routesPath} does not exist. Please make sure --routes-path is a valid file path (for example "/public/_routes.json").`, + 1 + ); + } + + if ( + !existsSync(routesOutputDirectory) || + !lstatSync(routesOutputDirectory).isDirectory() + ) { + throw new FatalError( + `Oops! Folder ${routesOutputDirectory} does not exist. Please make sure --output-routes-path is a valid file path (for example "/public/_routes.json").`, + 1 + ); + } + + try { + routesFileContents = readFileSync(routesPath, "utf-8"); + } catch (err) { + throw new FatalError(`Error while reading ${routesPath} file: ${err}`); + } + + const routes = JSON.parse(routesFileContents); + + if (!isRoutesJSONSpec(routes)) { + throw new FatalError( + ` + Invalid _routes.json file found at: ${routesPath}. Please make sure the JSON object has the following format: + { + version: ${ROUTES_SPEC_VERSION}; + include: string[]; + exclude: string[]; + } + `, + 1 + ); + } + + const optimizedRoutes = optimizeRoutesJSONSpec(routes); + const optimizedRoutesContents = JSON.stringify(optimizedRoutes); + + try { + writeFileSync(outputRoutesPath, optimizedRoutesContents); + } catch (err) { + throw new FatalError( + `Error writing to ${outputRoutesPath} file: ${err}`, + 1 + ); + } +} diff --git a/packages/wrangler/src/pages/functions/routes-transformation.ts b/packages/wrangler/src/pages/functions/routes-transformation.ts index de1dce436d21..6ee6b4c8f017 100644 --- a/packages/wrangler/src/pages/functions/routes-transformation.ts +++ b/packages/wrangler/src/pages/functions/routes-transformation.ts @@ -13,9 +13,6 @@ interface RoutesJSONSpec { type RoutesJSONRouteInput = Pick[]; -/** - * TODO can we do better naming? - */ export function convertRoutesToGlobPatterns( routes: RoutesJSONRouteInput ): string[] { diff --git a/packages/wrangler/src/pages/index.tsx b/packages/wrangler/src/pages/index.tsx index bd6f7e7c3884..ae7a63d8e7b7 100644 --- a/packages/wrangler/src/pages/index.tsx +++ b/packages/wrangler/src/pages/index.tsx @@ -3,6 +3,7 @@ import * as Build from "./build"; import * as Deployments from "./deployments"; import * as Dev from "./dev"; +import * as Functions from "./functions"; import * as Projects from "./projects"; import * as Publish from "./publish"; import * as Upload from "./upload"; @@ -19,66 +20,75 @@ process.on("SIGTERM", () => { }); export const pages: BuilderCallback = (yargs) => { - return yargs - .command( - "dev [directory] [-- command..]", - "🧑‍💻 Develop your full-stack Pages application locally", - Dev.Options, - Dev.Handler - ) - .command("functions", false, (yargs) => - // we hide this command from help output because - // it's not meant to be used directly right now - { - return yargs.command( - "build [directory]", - "Compile a folder of Cloudflare Pages Functions into a single Worker", - Build.Options, - Build.Handler - ); - } - ) - .command("project", "⚡️ Interact with your Pages projects", (yargs) => - yargs - .command( - "list", - "List your Cloudflare Pages projects", - Projects.ListOptions, - Projects.ListHandler - ) - .command( - "create [project-name]", - "Create a new Cloudflare Pages project", - Projects.CreateOptions, - Projects.CreateHandler - ) - .command("upload [directory]", false, Upload.Options, Upload.Handler) - .epilogue(pagesBetaWarning) - ) - .command( - "deployment", - "🚀 Interact with the deployments of a project", - (yargs) => + return ( + yargs + .command( + "dev [directory] [-- command..]", + "🧑‍💻 Develop your full-stack Pages application locally", + Dev.Options, + Dev.Handler + ) + /** + * `wrangler pages functions` is meant for internal use only for now, + * so let's hide this command from the help output + */ + .command("functions", false, (yargs) => + yargs + .command( + "build [directory]", + "Compile a folder of Cloudflare Pages Functions into a single Worker", + Build.Options, + Build.Handler + ) + .command( + "optimize-routes [routesPath] [outputRoutesPath]", + "Consolidate and optimize the route paths declared in _routes.json", + Functions.OptimizeRoutesOptions, + Functions.OptimizeRoutesHandler + ) + ) + .command("project", "⚡️ Interact with your Pages projects", (yargs) => yargs .command( "list", - "List deployments in your Cloudflare Pages project", - Deployments.ListOptions, - Deployments.ListHandler + "List your Cloudflare Pages projects", + Projects.ListOptions, + Projects.ListHandler ) .command( - "create [directory]", - "🆙 Publish a directory of static assets as a Pages deployment", - Publish.Options, - Publish.Handler + "create [project-name]", + "Create a new Cloudflare Pages project", + Projects.CreateOptions, + Projects.CreateHandler ) + .command("upload [directory]", false, Upload.Options, Upload.Handler) .epilogue(pagesBetaWarning) - ) - .command( - "publish [directory]", - "🆙 Publish a directory of static assets as a Pages deployment", - Publish.Options, - Publish.Handler - ) - .epilogue(pagesBetaWarning); + ) + .command( + "deployment", + "🚀 Interact with the deployments of a project", + (yargs) => + yargs + .command( + "list", + "List deployments in your Cloudflare Pages project", + Deployments.ListOptions, + Deployments.ListHandler + ) + .command( + "create [directory]", + "🆙 Publish a directory of static assets as a Pages deployment", + Publish.Options, + Publish.Handler + ) + .epilogue(pagesBetaWarning) + ) + .command( + "publish [directory]", + "🆙 Publish a directory of static assets as a Pages deployment", + Publish.Options, + Publish.Handler + ) + .epilogue(pagesBetaWarning) + ); };