355 lines
15 KiB
JavaScript
355 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.validateSpecConfig = void 0;
|
|
exports.runCLI = runCLI;
|
|
exports.generateSpecAndRoutes = generateSpecAndRoutes;
|
|
const yaml_1 = __importDefault(require("yaml"));
|
|
const yargs_1 = __importDefault(require("yargs"));
|
|
const helpers_1 = require("yargs/helpers");
|
|
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 node_path_1 = require("node:path");
|
|
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 isYamlExtension = (extension) => extension === '.yaml' || extension === '.yml';
|
|
const isJsExtension = (extension) => extension === '.js' || extension === '.cjs';
|
|
const getConfig = async (configPath = 'tsoa.json') => {
|
|
let config;
|
|
const ext = (0, node_path_1.extname)(configPath);
|
|
const configFullPath = (0, node_path_1.isAbsolute)(configPath) ? configPath : `${workingDir}/${configPath}`;
|
|
try {
|
|
if (isYamlExtension(ext)) {
|
|
const configRaw = await (0, fs_1.fsReadFile)(configFullPath);
|
|
config = yaml_1.default.parse(configRaw.toString('utf8'));
|
|
}
|
|
else if (isJsExtension(ext)) {
|
|
config = await Promise.resolve(`${configFullPath}`).then(s => __importStar(require(s)));
|
|
}
|
|
else {
|
|
const configRaw = await (0, fs_1.fsReadFile)(configFullPath);
|
|
config = JSON.parse(configRaw.toString('utf8'));
|
|
}
|
|
}
|
|
catch (err) {
|
|
if (!(err instanceof Error)) {
|
|
console.error(err);
|
|
throw Error(`Unhandled error encountered loading '${configPath}': ${String(err)}`);
|
|
}
|
|
else if ('code' in err && err.code === 'MODULE_NOT_FOUND') {
|
|
throw Error(`No config file found at '${configPath}'`);
|
|
}
|
|
else if (err.name === 'SyntaxError') {
|
|
console.error(err);
|
|
const errorType = isJsExtension(ext) ? 'JS' : 'JSON';
|
|
throw Error(`Invalid ${errorType} syntax in config at '${configPath}': ${err.message}`);
|
|
}
|
|
else {
|
|
console.error(err);
|
|
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?.[1];
|
|
config.spec.contact.email = config.spec.contact.email || contact?.[2];
|
|
config.spec.contact.url = config.spec.contact.url || contact?.[3];
|
|
}
|
|
else if (typeof author === 'object') {
|
|
config.spec.contact.name = config.spec.contact.name || author?.name;
|
|
config.spec.contact.email = config.spec.contact.email || author?.email;
|
|
config.spec.contact.url = config.spec.contact.url || author?.url;
|
|
}
|
|
if (!config.defaultNumberType) {
|
|
config.defaultNumberType = 'double';
|
|
}
|
|
if (config.spec.rootSecurity) {
|
|
if (!Array.isArray(config.spec.rootSecurity)) {
|
|
throw new Error('spec.rootSecurity must be an array');
|
|
}
|
|
if (config.spec.rootSecurity) {
|
|
const ok = config.spec.rootSecurity.every(security => typeof security === 'object' && security !== null && Object.values(security).every(scope => Array.isArray(scope)));
|
|
if (!ok) {
|
|
throw new Error('spec.rootSecurity must be an array of objects whose keys are security scheme names and values are arrays of scopes');
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
const bodyCoercion = config.routes.bodyCoercion ?? true;
|
|
config.routes.basePath = config.routes.basePath || '/';
|
|
return {
|
|
...config.routes,
|
|
entryFile: config.entryFile,
|
|
noImplicitAdditionalProperties,
|
|
bodyCoercion,
|
|
controllerPathGlobs: config.controllerPathGlobs,
|
|
multerOpts: config.multerOpts,
|
|
rootSecurity: config.spec.rootSecurity,
|
|
};
|
|
};
|
|
const configurationArgs = {
|
|
alias: 'c',
|
|
describe: 'tsoa configuration file; default is tsoa.json in the working directory',
|
|
required: false,
|
|
string: true,
|
|
};
|
|
const hostArgs = {
|
|
describe: 'API host',
|
|
required: false,
|
|
string: true,
|
|
};
|
|
const basePathArgs = {
|
|
describe: 'Base API path',
|
|
required: false,
|
|
string: true,
|
|
};
|
|
const yarmlArgs = {
|
|
describe: 'Swagger spec yaml format',
|
|
required: false,
|
|
boolean: true,
|
|
};
|
|
const jsonArgs = {
|
|
describe: 'Swagger spec json format',
|
|
required: false,
|
|
boolean: true,
|
|
};
|
|
function runCLI() {
|
|
return (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
.usage('Usage: $0 <command> [options]')
|
|
.command('spec', 'Generate OpenAPI spec', {
|
|
basePath: basePathArgs,
|
|
configuration: configurationArgs,
|
|
host: hostArgs,
|
|
json: jsonArgs,
|
|
yaml: yarmlArgs,
|
|
}, args => SpecGenerator(args))
|
|
.command('routes', 'Generate routes', {
|
|
basePath: basePathArgs,
|
|
configuration: configurationArgs,
|
|
}, args => routeGenerator(args))
|
|
.command('spec-and-routes', 'Generate OpenAPI spec and routes', {
|
|
basePath: basePathArgs,
|
|
configuration: configurationArgs,
|
|
host: hostArgs,
|
|
json: jsonArgs,
|
|
yaml: yarmlArgs,
|
|
}, args => void generateSpecAndRoutes(args))
|
|
.demandCommand(1, 1, 'Must provide a valid command.')
|
|
.help('help')
|
|
.alias('help', 'h')
|
|
.parse();
|
|
}
|
|
if (require.main === module) {
|
|
void (async () => {
|
|
try {
|
|
await runCLI();
|
|
}
|
|
catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('tsoa cli error:\n', err);
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
}
|
|
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, config.spec.rootSecurity, config.defaultNumberType, config.routes.esm).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;
|
|
}
|
|
}
|
|
//# sourceMappingURL=cli.js.map
|