counterfact
Version:
a library for building a fake REST API for testing
282 lines (226 loc) • 7.61 kB
JavaScript
import fs from "node:fs";
import { readFile } from "node:fs/promises";
import nodePath from "node:path";
import { fileURLToPath } from "node:url";
import { program } from "commander";
import createDebug from "debug";
import open from "open";
import { counterfact } from "../dist/app.js";
import { pathsToRoutes } from "../dist/migrate/paths-to-routes.js";
const MIN_NODE_VERSION = 17;
if (Number.parseInt(process.versions.node.split("."), 10) < MIN_NODE_VERSION) {
process.stdout.write(
`Counterfact works with Node version ${MIN_NODE_VERSION}+. You are running version ${process.version}`,
);
process.exit(1);
}
const taglinesFile = await readFile(
nodePath.join(
nodePath.dirname(fileURLToPath(import.meta.url)),
"taglines.txt",
),
"utf8",
);
const taglines = taglinesFile.split("\n").slice(0, -1);
const DEFAULT_PORT = 3100;
const debug = createDebug("counterfact:bin:counterfact");
debug("running ./bin/counterfact.js");
function padTagLine(tagLine) {
const headerLength = 51;
const padding = " ".repeat((headerLength - tagLine.length) / 2);
return `${padding}${tagLine}`;
}
function createWatchMessage(config) {
let watchMessage = "";
switch (true) {
case config.watch.routes && config.watch.types: {
watchMessage = "Watching for changes";
break;
}
case config.watch.routes: {
watchMessage = "Watching routes for changes";
break;
}
case config.watch.types: {
watchMessage = "Watching types for changes";
break;
}
default: {
watchMessage = undefined;
}
}
if (!watchMessage) {
switch (true) {
case config.generate.routes && config.generate.types: {
watchMessage = "Generating routes and types";
break;
}
case config.generate.routes: {
watchMessage = "Generating routes";
break;
}
case config.generate.types: {
watchMessage = "Generating types";
break;
}
default: {
watchMessage = undefined;
}
}
}
return watchMessage;
}
async function main(source, destination) {
debug("executing the main function");
const options = program.opts();
const args = process.argv;
const destinationPath = nodePath
.join(process.cwd(), destination)
.replaceAll("\\", "/");
const basePath = nodePath.resolve(destinationPath).replaceAll("\\", "/");
// If no action-related option is provided, default to all options
const actions = ["repl", "serve", "watch", "generate", "buildCache"];
if (
!Object.keys(options).some((argument) =>
actions.some((action) => argument.startsWith(action)),
)
) {
for (const action of actions) {
options[action] = true;
}
}
debug("options: %o", options);
debug("source: %s", source);
debug("destination: %s", destination);
const openBrowser = options.open;
const url = `http://localhost:${options.port}`;
const guiUrl = `${url}/counterfact/`;
const swaggerUrl = `${url}/counterfact/swagger/`;
const config = {
alwaysFakeOptionals: options.alwaysFakeOptionals,
basePath,
generate: {
routes:
options.generate ||
options.generateRoutes ||
options.watch ||
options.watchRoutes ||
options.buildCache,
types:
options.generate ||
options.generateTypes ||
options.watch ||
options.watchTypes ||
options.buildCache,
},
openApiPath: source,
port: options.port,
proxyPaths: new Map([["", Boolean(options.proxyUrl)]]),
proxyUrl: options.proxyUrl ?? "",
routePrefix: options.prefix,
startRepl: options.repl,
startServer: options.serve,
buildCache: options.buildCache || false,
watch: {
routes: options.watch || options.watchRoutes,
types: options.watch || options.watchTypes,
},
};
debug("loading counterfact (%o)", config);
let didMigrate = false;
// eslint-disable-next-line n/no-sync
if (fs.existsSync(nodePath.join(config.basePath, "paths"))) {
await pathsToRoutes(config.basePath);
await fs.promises.rmdir(nodePath.join(config.basePath, "paths"), {
recursive: true,
});
await fs.promises.rmdir(nodePath.join(config.basePath, "path-types"), {
recursive: true,
});
await fs.promises.rmdir(nodePath.join(config.basePath, "components"), {
recursive: true,
});
didMigrate = true;
}
const { start } = await counterfact(config);
debug("loaded counterfact", config);
const watchMessage = createWatchMessage(config);
const introduction = [
"____ ____ _ _ _ _ ___ ____ ____ ____ ____ ____ ___",
String.raw`|___ [__] |__| |\| | |=== |--< |--- |--| |___ | `,
padTagLine(taglines[Math.floor(Math.random() * taglines.length)]),
"",
`| API Base URL ==> ${url}`,
source === "_" ? undefined : `| Swagger UI ==> ${swaggerUrl}`,
"",
"| Instructions ==> https://counterfact.dev/docs/usage.html",
"| Help/feedback ==> https://github.com/pmcelhaney/counterfact/issues",
"",
watchMessage,
config.startServer ? "Starting server" : undefined,
config.startRepl ? "Starting REPL, type .help for more info" : undefined,
];
process.stdout.write(
introduction.filter((line) => line !== undefined).join("\n"),
);
process.stdout.write("\n\n");
debug("starting server");
await start(config);
debug("started server");
if (openBrowser) {
debug("opening browser");
await open(guiUrl);
debug("opened browser");
}
if (didMigrate) {
process.stdout.write("\n\n\n*******************************\n");
process.stdout.write("MIGRATING TO NEW FILE STRUCTURE\n\n");
process.stdout.write(
"In preparation for version 1.0, Counterfact has migrated to a new file structure.\n",
);
process.stdout.write("- The paths directory has been renamed to routes.\n");
process.stdout.write(
"- The path-types and components directories are now stored under types.\n",
);
process.stdout.write("Your files have automatically been migrated.\n");
process.stdout.write(
"Please report any issues to https://github.com/pmcelhaney/counterfact/issues\n",
);
process.stdout.write("*******************************\n\n\n");
}
}
program
.name("counterfact")
.description(
"Counterfact is a tool for mocking REST APIs in development. See https://counterfact.dev for more info.",
)
.argument(
"[openapi.yaml]",
'path or URL to OpenAPI document or "_" to run without OpenAPI',
"_",
)
.argument("[destination]", "path to generated code", ".")
.option("-p, --port <number>", "server port number", DEFAULT_PORT)
.option("-o, --open", "open a browser")
.option("-g, --generate", "generate all code for both routes and types")
.option("--generate-types", "generate types")
.option("--generate-routes", "generate routes")
.option("-w, --watch", "generate + watch all code for changes")
.option("--watch-types", "generate + watch types for changes")
.option("--watch-routes", "generate + watch routes for changes")
.option("-s, --serve", "start the server")
.option("-b, --build-cache", "builds the cache of compiled routes and types")
.option("-r, --repl", "start the REPL")
.option("--proxy-url <string>", "proxy URL")
.option(
"--prefix <string>",
"base path from which routes will be served (e.g. /api/v1)",
"",
)
.option(
"--always-fake-optionals",
"random responses will include optional fields",
)
.action(main)
.parse(process.argv);