UNPKG

api

Version:

Magical SDK generation from an OpenAPI definition 🪄

213 lines • 8.53 kB
import chalk from 'chalk'; import { Command, Option } from 'commander'; import { common, createEmphasize } from 'emphasize'; import figures from 'figures'; import Oas from 'oas'; import OASNormalize from 'oas-normalize'; import ora from 'ora'; import uslug from 'uslug'; import { codegenFactory, SupportedLanguages } from '../codegen/factory.js'; import Fetcher from '../fetcher.js'; import promptTerminal from '../lib/prompt.js'; import logger, { oraOptions } from '../logger.js'; import Storage from '../storage.js'; const { highlight } = createEmphasize(common); async function getLanguage(options) { let language; if (options.lang) { language = options.lang; } else { ({ value: language } = await promptTerminal({ type: 'select', name: 'value', message: 'What language would you like to generate an SDK for?', choices: [{ title: 'JavaScript', value: SupportedLanguages.JS }], initial: 1, })); } return language; } async function getIdentifier(oas, uri, options) { let identifier; if (options.identifier) { // `Storage.isIdentifierValid` will throw an exception if an identifier is invalid. if (Storage.isIdentifierValid(options.identifier)) { identifier = options.identifier; } } else if (Fetcher.isAPIRegistryUUID(uri)) { identifier = Fetcher.getProjectPrefixFromRegistryUUID(uri); } else { ({ value: identifier } = await promptTerminal({ type: 'text', name: 'value', initial: oas.api?.info?.title ? uslug(oas.api.info.title, { lower: true }) : undefined, message: 'What would you like to identify this API as? This will be how you use the SDK. (e.g. entering `petstore` would result in `@api/petstore`)', validate: value => { if (!value) { return false; } try { return Storage.isIdentifierValid(value, true); } catch (err) { return err.message; } }, })); } return identifier; } // @todo log logs to `.api/.logs` and have `.logs` ignored const cmd = new Command(); cmd .name('install') .description('install an API SDK into your codebase') .argument('<uri>', 'an API to install') .option('-i, --identifier <identifier>', 'API identifier (eg. `@api/petstore`)') .addOption(new Option('-l, --lang <language>', 'SDK language').default(SupportedLanguages.JS).choices([SupportedLanguages.JS])) .addOption(new Option('-y, --yes', 'Automatically answer "yes" to any prompts printed')) .action(async (uri, options) => { const language = await getLanguage(options); // @todo let them know that we're going to be creating a `.api/ directory // @todo detect if they have a gitigore and .npmignore and if .api woudl be ignored by that // @todo don't support swagger files without upconverting them if (Storage.isInLockFile({ source: uri })) { // @todo // logger(`It looks like you already have this API installed. Would you like to update it?`); } let spinner = ora({ text: 'Fetching your API definition', ...oraOptions() }).start(); const storage = new Storage(uri, language); const oas = await storage .load(false) .then(res => { spinner.succeed(spinner.text); return res; }) .then(async (spec) => { // This may result in some data loss if there's already a `title` present, but in the case // where we want to generate code for the API definition (see http://npm.im/api), we'd // prefer to retain original reference name as a title for any generated types. // // This work used to exist within the `oas` library as a `preserveRefAsJSONSchemaTitle` // option but was deprecated and removed in https://github.com/readmeio/oas/pull/1084. if (spec?.components?.schemas && typeof spec.components?.schemas === 'object') { Object.keys(spec.components.schemas).forEach(schemaName => { // oxlint-disable-next-line no-param-reassign, no-unsafe-optional-chaining (spec.components?.schemas?.[schemaName]).title = schemaName; // oxlint-disable-next-line no-param-reassign, no-unsafe-optional-chaining (spec.components?.schemas?.[schemaName])['x-readme-ref-name'] = schemaName; }); } const normalize = new OASNormalize(spec, { parser: { dereference: { circular: 'ignore', }, }, }); return normalize.dereference(); }) .then(spec => spec) .then(Oas.init) .catch(err => { // @todo cleanup installed files spinner.fail(spinner.text); throw err; }); const identifier = await getIdentifier(oas, uri, options); if (!identifier) { throw new Error('You must tell us what you would like to identify this API as in order to install it.'); } // Now that we've got an identifier we can save their spec and generate the directory structure // for their SDK. storage.setIdentifier(identifier); await storage.save(oas.api); // @todo look for a prettier config and if we find one ask them if we should use it spinner = ora({ text: 'Generating your SDK', ...oraOptions() }).start(); const generator = codegenFactory(language, oas, '../openapi.json', identifier); const sdkSource = await generator .generate() .then(res => { spinner.succeed(spinner.text); return res; }) .catch(err => { // @todo cleanup installed files spinner.fail(spinner.text); throw err; }); spinner = ora({ text: 'Saving your SDK into your codebase', ...oraOptions() }).start(); await storage .saveSourceFiles(sdkSource) .then(() => { spinner.succeed(spinner.text); }) .catch(err => { // @todo cleanup installed files spinner.fail(spinner.text); throw err; }); if (generator.hasRequiredPackages()) { logger(`${figures.warning} This generator requires some packages to be installed alongside it:`); Object.entries(generator.requiredPackages).forEach(([pkg, pkgInfo]) => { let msg = ` ${figures.pointerSmall} ${pkg}: ${pkgInfo.reason}`; if (pkgInfo.url) { msg += ` ${pkgInfo.url}`; } logger(msg); }); if (!options.yes) { await promptTerminal({ type: 'confirm', name: 'value', message: 'OK to proceed with package installation?', initial: true, }).then(({ value }) => { if (!value) { // @todo cleanup installed files throw new Error('Installation cancelled.'); } }); } spinner = ora({ text: 'Installing required packages', ...oraOptions() }).start(); try { await generator.install(storage); spinner.succeed(spinner.text); } catch (err) { // @todo cleanup installed files spinner.fail(spinner.text); throw err; } } spinner = ora({ text: 'Compiling your SDK', ...oraOptions() }).start(); try { await generator.compile(storage); spinner.succeed(spinner.text); } catch (err) { // @todo cleanup installed files spinner.fail(spinner.text); throw err; } logger(''); logger(`🚀 All done! Your SDK is now available to use locally via the ${chalk.yellow(storage.getPackageName())} package.`); const exampleSnippet = await generator.getExampleCodeSnippet(); if (exampleSnippet) { logger(''); logger(chalk.bold("👇 Here's an example code snippet you can try out 👇")); logger(''); logger(highlight(language, exampleSnippet).value); } logger(''); }) .addHelpText('after', ` Examples: $ npx api install @developers/v2.0#nysezql0wwo236 $ npx api install https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json $ npx api install ./petstore.json`); export default cmd; //# sourceMappingURL=install.js.map