@httpc/cli
Version:
httpc cli for building function-based API with minimal code and end-to-end type safety
168 lines (166 loc) • 7.67 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const promises_1 = __importDefault(require("fs/promises"));
const crypto_1 = __importDefault(require("crypto"));
const path_1 = __importDefault(require("path"));
const typescript_1 = __importDefault(require("typescript"));
const commander_1 = require("commander");
const prompts_1 = __importDefault(require("prompts"));
const utils_1 = require("../utils");
function getDefaultConfig(packageJson) {
const { name } = packageJson;
return {
name: `${name}-client`,
dest: "client",
entry: "src/calls/index.ts"
};
}
// read order
// 1. httpc.json file near the package.json
// 2. httpc field on the package.json
async function readConfig(options) {
const packageJson = await utils_1.packageUtils.read();
let httpcConfig;
if (await utils_1.fsUtils.exists(path_1.default.resolve("./httpc.json"))) {
httpcConfig = JSON.parse(await promises_1.default.readFile("httpc.json", "utf8"));
utils_1.log.verbose("Config from httpc.json");
}
else {
httpcConfig = packageJson.httpc;
if (httpcConfig) {
utils_1.log.verbose("Config from package.json");
}
}
if (!httpcConfig && options?.useDefault) {
httpcConfig = getDefaultConfig(packageJson);
}
if (!httpcConfig) {
throw new Error("No httpc client config set");
}
const configs = [httpcConfig].flat();
if (configs.length === 0) {
throw new Error("No httpc client config set");
}
return configs;
}
async function writeTypeIndex(rootFile, outDir) {
const typeFileName = path_1.default.basename(rootFile).replace(/^(.+)\.ts$/gi, (_, m) => m);
const typeFile = `${typeFileName}.d.ts`;
let dirs = sanitizePath(path_1.default.dirname(rootFile)).split("/");
while (dirs.shift()) {
const target = path_1.default.resolve(outDir, ...dirs, typeFile);
if (await utils_1.fsUtils.exists(target)) {
// no need to write if it's in the root and it's already the index
if (dirs.length === 0 && typeFile === "index.d.ts")
return;
// re-export the default from the inner path
const content = `export { default } from "./${sanitizePath(path_1.default.relative(outDir, path_1.default.dirname(target)))}/${typeFileName}"`;
await promises_1.default.writeFile(path_1.default.resolve(outDir, "index.d.ts"), content, "utf-8");
return;
}
}
throw new Error("Cant find root type");
}
function sanitizePath(path) {
return path.split("\\").join("/");
}
const init = (0, commander_1.createCommand)("init")
.description("initialize a client package")
.action(async () => {
const configs = await readConfig({ useDefault: true });
for (const config of configs) {
const dest = path_1.default.resolve(config.dest);
if (!await utils_1.fsUtils.isDirEmpty(dest)) {
const { confirm } = await (0, prompts_1.default)({
name: "confirm",
type: "confirm",
message: `The destination directory '${dest}' is not empty.\n Confirm initialization? (all content will be deleted)`
});
if (!confirm)
continue;
}
await utils_1.templateUtils.initialize("client", dest, {
packageName: config.name
});
console.log("Client '%s' initialized", config.name);
}
});
const generate = (0, commander_1.createCommand)("generate")
.description("generate a client typings")
.action(async () => {
const configs = await readConfig();
const tsConfigPath = path_1.default.resolve("tsconfig.json");
const tsConfig = typescript_1.default.readConfigFile(tsConfigPath, typescript_1.default.sys.readFile);
const { options, fileNames } = typescript_1.default.parseJsonConfigFileContent(tsConfig.config, typescript_1.default.sys, ".");
for (const config of configs) {
const entry = path_1.default.resolve(config.entry);
// check presence
if (!await utils_1.fsUtils.exists(path_1.default.resolve(config.dest, "package.json"))) {
await utils_1.templateUtils.initialize("client", config.dest, {
packageName: config.name
});
console.log("Client '%s' initialized", config.name);
}
const dest = path_1.default.resolve(config.dest, "types");
await utils_1.fsUtils.clearDir(dest);
const compilerOptions = {
...options,
noEmit: false,
skipLibCheck: true,
sourceMap: false,
emitDeclarationOnly: true,
declaration: true,
declarationMap: true,
outDir: dest,
removeComments: false,
};
if (!await utils_1.fsUtils.exists(entry)) {
throw new Error(`Client '${config.name}' entry '${config.entry}' not found`);
}
const host = typescript_1.default.createCompilerHost(compilerOptions);
const compiler = typescript_1.default.createProgram(fileNames, compilerOptions, host);
const originalWriteFile = host.writeFile;
host.writeFile = function (filename, text, ...args) {
if (filename.endsWith(".d.ts")) {
text = text.replaceAll(/import\(("|')@httpc\/server("|')\)\.HttpCallPipelineDefinition/g, "HttpCallPipelineDefinition");
text = text.replaceAll(/import\s?\{\s?HttpCallPipelineDefinition\s?\}\s?from ("|')@httpc\/server("|');?/g, "");
text = text.replaceAll(/import\(("|')@httpc\/kit("|')\)\.HttpCallPipelineDefinition/g, "HttpCallPipelineDefinition");
text = text.replaceAll(/import\s?\{\s?HttpCallPipelineDefinition\s?\}\s?from ("|')@httpc\/kit("|');?/g, "");
}
//@ts-ignore
return originalWriteFile.call(this, filename, text, ...args);
};
const result = compiler.emit();
if (result.emitSkipped) {
console.error(result.diagnostics);
throw new Error(`Client '${config.name}' generation failed`);
}
await writeTypeIndex(entry, dest);
// create random main file
// in order to execute metadata extraction
const main = path_1.default.join(__dirname, `main-${crypto_1.default.randomUUID()}.ts`);
await promises_1.default.writeFile(main, `
import "reflect-metadata";
import api from "${sanitizePath(entry.replace(".ts", ""))}";
import { writeMetadata } from "${sanitizePath(path_1.default.join(__dirname, "../utils/generateMetadata"))}";
writeMetadata(api, "${sanitizePath(dest)}")
.then(()=> process.exit(0))
.catch(err=> {
console.error(err);
process.exit(1);
});
`, "utf8");
const executeOptions = {};
await (0, utils_1.run)(`npx ts-node --transpileOnly --project "${tsConfigPath}" -O "${JSON.stringify(executeOptions).split('"').join('\\"')}" ${sanitizePath(main)}`)
.finally(() => promises_1.default.unlink(main));
utils_1.log.success("Client '%s' generated", config.name);
}
});
const ClientCommand = (0, commander_1.createCommand)("client")
.description("manage httpc client generation")
.addCommand(init)
.addCommand(generate);
exports.default = ClientCommand;
;