UNPKG

@namecheap/tsoa-cli

Version:

Build swagger-compliant REST APIs using TypeScript and Node

301 lines 12.9 kB
#!/usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateSpecAndRoutes = exports.runCLI = exports.validateSpecConfig = void 0; const path = require("path"); const YAML = require("yamljs"); const yargs = require("yargs"); const metadataGenerator_1 = require("./metadataGeneration/metadataGenerator"); const generate_routes_1 = require("./module/generate-routes"); const generate_spec_1 = require("./module/generate-spec"); const fs_1 = require("./utils/fs"); const workingDir = process.cwd(); let packageJson; const getPackageJsonValue = async (key, defaultValue = '') => { if (!packageJson) { try { const packageJsonRaw = await (0, fs_1.fsReadFile)(`${workingDir}/package.json`); packageJson = JSON.parse(packageJsonRaw.toString('utf8')); } catch (err) { return defaultValue; } } return packageJson[key] || ''; }; const nameDefault = () => getPackageJsonValue('name', 'TSOA'); const versionDefault = () => getPackageJsonValue('version', '1.0.0'); const descriptionDefault = () => getPackageJsonValue('description', 'Build swagger-compliant REST APIs using TypeScript and Node'); const licenseDefault = () => getPackageJsonValue('license', 'MIT'); const determineNoImplicitAdditionalSetting = (noImplicitAdditionalProperties) => { if (noImplicitAdditionalProperties === 'silently-remove-extras' || noImplicitAdditionalProperties === 'throw-on-extras' || noImplicitAdditionalProperties === 'ignore') { return noImplicitAdditionalProperties; } else { return 'ignore'; } }; const authorInformation = getPackageJsonValue('author', 'unknown'); const getConfig = async (configPath = 'tsoa.json') => { let config; const ext = path.extname(configPath); try { if (ext === '.yaml' || ext === '.yml') { config = YAML.load(configPath); } else if (ext === '.js') { config = await Promise.resolve().then(() => require(`${workingDir}/${configPath}`)); } else { const configRaw = await (0, fs_1.fsReadFile)(`${workingDir}/${configPath}`); config = JSON.parse(configRaw.toString('utf8')); } } catch (err) { if (err.code === 'MODULE_NOT_FOUND') { throw Error(`No config file found at '${configPath}'`); } else if (err.name === 'SyntaxError') { // eslint-disable-next-line no-console console.error(err); const errorType = ext === '.js' ? 'JS' : 'JSON'; // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw Error(`Invalid ${errorType} syntax in config at '${configPath}': ${err.message}`); } else { // eslint-disable-next-line no-console console.error(err); // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw Error(`Unhandled error encountered loading '${configPath}': ${err.message}`); } } return config; }; const resolveConfig = async (config) => { return typeof config === 'object' ? config : getConfig(config); }; const validateCompilerOptions = (config) => { return (config || {}); }; const validateSpecConfig = async (config) => { if (!config.spec) { throw new Error('Missing spec: configuration must contain spec. Spec used to be called swagger in previous versions of tsoa.'); } if (!config.spec.outputDirectory) { throw new Error('Missing outputDirectory: configuration must contain output directory.'); } if (!config.entryFile && (!config.controllerPathGlobs || !config.controllerPathGlobs.length)) { throw new Error('Missing entryFile and controllerPathGlobs: Configuration must contain an entry point file or controller path globals.'); } if (!!config.entryFile && !(await (0, fs_1.fsExists)(config.entryFile))) { throw new Error(`EntryFile not found: ${config.entryFile} - please check your tsoa config.`); } config.spec.version = config.spec.version || (await versionDefault()); config.spec.specVersion = config.spec.specVersion || 2; if (config.spec.specVersion !== 2 && config.spec.specVersion !== 3) { throw new Error('Unsupported Spec version.'); } if (config.spec.spec && !['immediate', 'recursive', 'deepmerge', undefined].includes(config.spec.specMerging)) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Invalid specMerging config: ${config.spec.specMerging}`); } const noImplicitAdditionalProperties = determineNoImplicitAdditionalSetting(config.noImplicitAdditionalProperties); config.spec.name = config.spec.name || (await nameDefault()); config.spec.description = config.spec.description || (await descriptionDefault()); config.spec.license = config.spec.license || (await licenseDefault()); config.spec.basePath = config.spec.basePath || '/'; // defaults to template that may generate non-unique operation ids. // @see https://github.com/lukeautry/tsoa/issues/1005 config.spec.operationIdTemplate = config.spec.operationIdTemplate || '{{titleCase method.name}}'; if (!config.spec.contact) { config.spec.contact = {}; } const author = await authorInformation; if (typeof author === 'string') { const contact = /^([^<(]*)?\s*(?:<([^>(]*)>)?\s*(?:\(([^)]*)\)|$)/m.exec(author); config.spec.contact.name = config.spec.contact.name || (contact === null || contact === void 0 ? void 0 : contact[1]); config.spec.contact.email = config.spec.contact.email || (contact === null || contact === void 0 ? void 0 : contact[2]); config.spec.contact.url = config.spec.contact.url || (contact === null || contact === void 0 ? void 0 : contact[3]); } else if (typeof author === 'object') { config.spec.contact.name = config.spec.contact.name || (author === null || author === void 0 ? void 0 : author.name); config.spec.contact.email = config.spec.contact.email || (author === null || author === void 0 ? void 0 : author.email); config.spec.contact.url = config.spec.contact.url || (author === null || author === void 0 ? void 0 : author.url); } return { ...config.spec, noImplicitAdditionalProperties, entryFile: config.entryFile, controllerPathGlobs: config.controllerPathGlobs, }; }; exports.validateSpecConfig = validateSpecConfig; const validateRoutesConfig = async (config) => { if (!config.entryFile && (!config.controllerPathGlobs || !config.controllerPathGlobs.length)) { throw new Error('Missing entryFile and controllerPathGlobs: Configuration must contain an entry point file or controller path globals.'); } if (!!config.entryFile && !(await (0, fs_1.fsExists)(config.entryFile))) { throw new Error(`EntryFile not found: ${config.entryFile} - Please check your tsoa config.`); } if (!config.routes.routesDir) { throw new Error('Missing routesDir: Configuration must contain a routes file output directory.'); } if (config.routes.authenticationModule && !((await (0, fs_1.fsExists)(config.routes.authenticationModule)) || (await (0, fs_1.fsExists)(config.routes.authenticationModule + '.ts')))) { throw new Error(`No authenticationModule file found at '${config.routes.authenticationModule}'`); } if (config.routes.iocModule && !((await (0, fs_1.fsExists)(config.routes.iocModule)) || (await (0, fs_1.fsExists)(config.routes.iocModule + '.ts')))) { throw new Error(`No iocModule file found at '${config.routes.iocModule}'`); } const noImplicitAdditionalProperties = determineNoImplicitAdditionalSetting(config.noImplicitAdditionalProperties); config.routes.basePath = config.routes.basePath || '/'; config.routes.middleware = config.routes.middleware || 'express'; return { ...config.routes, entryFile: config.entryFile, noImplicitAdditionalProperties, controllerPathGlobs: config.controllerPathGlobs, multerOpts: config.multerOpts, }; }; const configurationArgs = { alias: 'c', describe: 'tsoa configuration file; default is tsoa.json in the working directory', required: false, type: 'string', }; const hostArgs = { describe: 'API host', required: false, type: 'string', }; const basePathArgs = { describe: 'Base API path', required: false, type: 'string', }; const yarmlArgs = { describe: 'Swagger spec yaml format', required: false, type: 'boolean', }; const jsonArgs = { describe: 'Swagger spec json format', required: false, type: 'boolean', }; function runCLI() { yargs .usage('Usage: $0 <command> [options]') .demand(1) .command('spec', 'Generate OpenAPI spec', { basePath: basePathArgs, configuration: configurationArgs, host: hostArgs, json: jsonArgs, yaml: yarmlArgs, }, SpecGenerator) .command('swagger', 'Generate OpenAPI spec', { basePath: basePathArgs, configuration: configurationArgs, host: hostArgs, json: jsonArgs, yaml: yarmlArgs, }, SpecGenerator) .command('routes', 'Generate routes', { basePath: basePathArgs, configuration: configurationArgs, }, routeGenerator) .command('spec-and-routes', 'Generate OpenAPI spec and routes', { basePath: basePathArgs, configuration: configurationArgs, host: hostArgs, json: jsonArgs, yaml: yarmlArgs, }, generateSpecAndRoutes) .command('swagger-and-routes', 'Generate OpenAPI spec and routes', { basePath: basePathArgs, configuration: configurationArgs, host: hostArgs, json: jsonArgs, yaml: yarmlArgs, }, generateSpecAndRoutes) .help('help') .alias('help', 'h').argv; } exports.runCLI = runCLI; if (require.main === module) runCLI(); async function SpecGenerator(args) { try { const config = await resolveConfig(args.configuration); if (args.basePath) { config.spec.basePath = args.basePath; } if (args.host) { config.spec.host = args.host; } if (args.yaml) { config.spec.yaml = args.yaml; } if (args.json) { config.spec.yaml = false; } const compilerOptions = validateCompilerOptions(config.compilerOptions); const swaggerConfig = await (0, exports.validateSpecConfig)(config); await (0, generate_spec_1.generateSpec)(swaggerConfig, compilerOptions, config.ignore); } catch (err) { // eslint-disable-next-line no-console console.error('Generate swagger error.\n', err); process.exit(1); } } async function routeGenerator(args) { try { const config = await resolveConfig(args.configuration); if (args.basePath) { config.routes.basePath = args.basePath; } const compilerOptions = validateCompilerOptions(config.compilerOptions); const routesConfig = await validateRoutesConfig(config); await (0, generate_routes_1.generateRoutes)(routesConfig, compilerOptions, config.ignore); } catch (err) { // eslint-disable-next-line no-console console.error('Generate routes error.\n', err); process.exit(1); } } async function generateSpecAndRoutes(args, metadata) { try { const config = await resolveConfig(args.configuration); if (args.basePath) { config.spec.basePath = args.basePath; } if (args.host) { config.spec.host = args.host; } if (args.yaml) { config.spec.yaml = args.yaml; } if (args.json) { config.spec.yaml = false; } const compilerOptions = validateCompilerOptions(config.compilerOptions); const routesConfig = await validateRoutesConfig(config); const swaggerConfig = await (0, exports.validateSpecConfig)(config); if (!metadata) { metadata = new metadataGenerator_1.MetadataGenerator(config.entryFile, compilerOptions, config.ignore, config.controllerPathGlobs).Generate(); } await Promise.all([(0, generate_routes_1.generateRoutes)(routesConfig, compilerOptions, config.ignore, metadata), (0, generate_spec_1.generateSpec)(swaggerConfig, compilerOptions, config.ignore, metadata)]); return metadata; } catch (err) { // eslint-disable-next-line no-console console.error('Generate routes error.\n', err); process.exit(1); throw err; } } exports.generateSpecAndRoutes = generateSpecAndRoutes; //# sourceMappingURL=cli.js.map