api
Version:
Magical SDK generation from an OpenAPI definition 🪄
213 lines • 8.53 kB
JavaScript
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