UNPKG

@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
"use strict"; 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;