Skip to content

Commit

Permalink
feat: add prismic-next CLI to clear cached /api/v2 requests (#66)
Browse files Browse the repository at this point in the history
* feat: add `prismic-next` CLI to clear cached `/api/v2` requests

* feat: make implementation testable
  • Loading branch information
angeloashmore authored May 19, 2023
1 parent 2e01b67 commit e257ae1
Show file tree
Hide file tree
Showing 10 changed files with 492 additions and 9 deletions.
16 changes: 14 additions & 2 deletions .size-limit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,24 @@ function getObjectValues(input, acc = []) {
}

module.exports = [
...new Set([pkg.main, pkg.module, ...getObjectValues(pkg.exports)]),
...new Set([
pkg.main,
pkg.module,
...getObjectValues(pkg.bin),
...getObjectValues(pkg.exports),
]),
]
.sort()
.filter((path) => {
return path && path !== "./package.json";
})
.map((path) => {
return { path };
return {
path,
modifyEsbuildConfig(config) {
config.platform = "node";

return config;
},
};
});
3 changes: 3 additions & 0 deletions bin/prismic-next.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node

import("../dist/cli.cjs").then((mod) => mod.run(process.argv));
44 changes: 40 additions & 4 deletions package-lock.json

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

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@
"size": "size-limit",
"test": "npm run lint && npm run types && npm run unit && npm run build && npm run size"
},
"bin": {
"prismic-next": "./bin/prismic-next.js"
},
"dependencies": {
"imgix-url-builder": "^0.0.3"
"imgix-url-builder": "^0.0.3",
"mri": "^1.2.0"
},
"devDependencies": {
"@prismicio/client": "^7.0.0-alpha.3",
Expand All @@ -64,6 +68,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-tsdoc": "^0.2.17",
"happy-dom": "^9.9.2",
"memfs": "^3.5.1",
"next": "^13.4.0",
"node-fetch": "^3.3.1",
"prettier": "^2.8.8",
Expand Down
168 changes: 168 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import mri from "mri";
import * as path from "node:path";
import * as fs from "node:fs/promises";
import * as tty from "node:tty";
import { Buffer } from "node:buffer";

import * as pkg from "../../package.json";

async function pathExists(filePath: string) {
try {
await fs.access(filePath);

return true;
} catch {
return false;
}
}

function color(colorCode: number, string: string) {
return tty.WriteStream.prototype.hasColors()
? "\u001B[" + colorCode + "m" + string + "\u001B[39m"
: string;
}

function warn(string: string) {
// Yellow
return console.warn(`${color(33, "warn")} - ${string}`);
}

function info(string: string) {
// Magenta
return console.info(`${color(35, "info")} - ${string}`);
}

type Args = {
help: boolean;
version: boolean;
};

export async function run(argv: string[]) {
const args = mri<Args>(argv.slice(2), {
boolean: ["help", "version"],
alias: {
help: "h",
version: "v",
},
default: {
help: false,
version: false,
},
});

const command = args._[0];

switch (command) {
case "clear-cache": {
warn(
"`prismic-next clear-cache` is an experimental utility. It may be replaced with a different solution in the future.",
);

async function getAppRootDir() {
let currentDir = process.cwd();

while (
!(await pathExists(path.join(currentDir, ".next"))) &&
!(await pathExists(path.join(currentDir, "package.json")))
) {
if (currentDir === path.resolve("/")) {
break;
}

currentDir = path.join(currentDir, "..");
}

if (
(await pathExists(path.join(currentDir, ".next"))) ||
(await pathExists(path.join(currentDir, "package.json")))
) {
return currentDir;
}
}

const appRootDir = await getAppRootDir();

if (!appRootDir) {
warn(
"Could not find the Next.js app root. Run `prismic-next clear-cache` in a Next.js project with a `.next` directory or `package.json` file.",
);

return;
}

const fetchCacheDir = path.join(
appRootDir,
".next",
"cache",
"fetch-cache",
);

if (!(await pathExists(fetchCacheDir))) {
info("No Next.js fetch cache directory found. You are good to go!");

return;
}

const cacheEntries = await fs.readdir(fetchCacheDir);

await Promise.all(
cacheEntries.map(async (entry) => {
try {
const contents = await fs.readFile(
path.join(fetchCacheDir, entry),
"utf8",
);
const payload = JSON.parse(contents);

if (payload.kind !== "FETCH") {
return;
}

const bodyPayload = JSON.parse(
Buffer.from(payload.data.body, "base64").toString(),
);

// Delete `/api/v2` requests.
if (/\.prismic\.io\/auth$/.test(bodyPayload.oauth_initiate)) {
await fs.unlink(path.join(fetchCacheDir, entry));

info(`Prismic /api/v2 request cache cleared: ${entry}`);
}
} catch (e) {
// noop
}
}),
);

info(
"The Prismic request cache has been cleared. Uncached requests will begin on the next Next.js server start-up.",
);

return;
}

default: {
if (command && (!args.version || !args.help)) {
warn("Invalid command.\n");
}

if (args.version) {
console.info(pkg.version);

return;
}

console.info(
`
Usage:
prismic-next <command> [options...]
Available commands:
clear-cache
Options:
--help, -h Show help text
--version, -v Show version
`.trim(),
);
}
}
}
32 changes: 30 additions & 2 deletions test/__setup__.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
import { beforeEach, vi } from "vitest";
import { beforeAll, beforeEach, vi } from "vitest";
import { createMockFactory, MockFactory } from "@prismicio/mock";
import { Headers } from "node-fetch";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";

declare module "vitest" {
export interface TestContext {
mock: MockFactory;
appRoot: string;
}
}

vi.stubGlobal("Headers", Headers);

beforeEach((ctx) => {
beforeAll(async () => {
await fs.mkdir(os.tmpdir(), { recursive: true });

vi.spyOn(process, "cwd");
});

beforeEach(async (ctx) => {
ctx.mock = createMockFactory({ seed: ctx.meta.name });
ctx.appRoot = await fs.mkdtemp(
path.join(os.tmpdir(), "@prismicio__next___cli"),
);

vi.clearAllMocks();

await fs.mkdir(path.join(ctx.appRoot, "foo", "bar"), { recursive: true });
vi.mocked(process.cwd).mockReturnValue(path.join(ctx.appRoot, "foo", "bar"));

return async () => {
await fs.rm(ctx.appRoot, { recursive: true });
};
});

vi.mock("node:fs/promises", async () => {
const memfs: typeof import("memfs") = await vi.importActual("memfs");

return memfs.fs.promises;
});
3 changes: 3 additions & 0 deletions test/__testutils__/argv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function argv(...args: string[]): string[] {
return ["/node", "./prismic-next", ...args];
}
Loading

0 comments on commit e257ae1

Please sign in to comment.