UNPKG

@nestia/sdk

Version:

Nestia SDK and Swagger generator

329 lines 16.8 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NestiaConfigLoader = void 0; const core_1 = require("@nestia/core"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const url_1 = require("url"); const EmittedJavaScriptPatcher_1 = require("../../utils/EmittedJavaScriptPatcher"); const TsConfigReader_1 = require("../../utils/TsConfigReader"); const TtscExecutor_1 = require("../../utils/TtscExecutor"); var NestiaConfigLoader; (function (NestiaConfigLoader) { NestiaConfigLoader.compilerOptions = (project) => __awaiter(this, void 0, void 0, function* () { const configFileName = findConfigFile(process.cwd(), project); if (!configFileName) throw new Error(`unable to find "${project}" file.`); const tsconfig = yield TsConfigReader_1.TsConfigReader.read(configFileName); return { raw: { compilerOptions: typeof (tsconfig === null || tsconfig === void 0 ? void 0 : tsconfig.compilerOptions) === "object" ? tsconfig.compilerOptions : {}, }, }; }); NestiaConfigLoader.configurations = (file, compilerOptions) => __awaiter(this, void 0, void 0, function* () { if (fs_1.default.existsSync(path_1.default.resolve(file)) === false) throw new Error(`Unable to find "${file}" file.`); (0, core_1.doNotThrowTransformError)(false); if (compilerOptions.plugins !== undefined) assertPlugins(file, compilerOptions.plugins); const configFile = yield materializeConfiguration({ file, compilerOptions, }); const loaded = yield loadMaterializedModule(configFile); const instance = extractConfiguration(file, loaded); const configurations = Array.isArray(instance) ? instance : [instance]; return assertConfigurations(file, configurations); }); const MATERIALIZED_ROOTS = new Set(); let CLEANUP_REGISTERED = false; const materializeConfiguration = (props) => __awaiter(this, void 0, void 0, function* () { var _a, _b; const configFile = path_1.default.resolve(props.file); const project = (_a = process.env.NESTIA_PROJECT) !== null && _a !== void 0 ? _a : "tsconfig.json"; const projectFile = findConfigFile(process.cwd(), project); if (projectFile === undefined) throw new Error(`unable to find "${project}" file.`); const projectRoot = path_1.default.dirname(path_1.default.resolve(projectFile)); const wrapperRoot = fs_1.default.mkdtempSync(path_1.default.join(ensureMaterializedRoot(projectRoot), "tsconfig-")); const outputRoot = fs_1.default.mkdtempSync(path_1.default.join(ensureMaterializedRoot(projectRoot), "run-")); const wrapperFile = path_1.default.join(wrapperRoot, "tsconfig.json"); const wrapperConfig = { extends: projectFile, compilerOptions: Object.assign(Object.assign({ noEmit: false, noUnusedLocals: false, noUnusedParameters: false }, nodeAmbientCompilerOptions(projectRoot, props.compilerOptions)), { // The wrapper compiles the config file only to require() its JS; .d.ts // is never read, and tsgo's declaration emitter can nil-panic on a // config that calls into Nest. Force it off regardless of the project. declaration: false, declarationMap: false, outDir: outputRoot, plugins: materializePlugins(props.compilerOptions.plugins), rootDir: projectRoot }), include: [configFile], exclude: [path_1.default.join(projectRoot, "src", "test", "**", "*")], }; fs_1.default.writeFileSync(wrapperFile, JSON.stringify(wrapperConfig, null, 2), "utf8"); try { TtscExecutor_1.TtscExecutor.run({ cwd: projectRoot, env: sdkTransformEnv(), project: wrapperFile, }); } catch (error) { const stderr = readChildOutput(error, "stderr"); const stdout = readChildOutput(error, "stdout"); const detail = stderr || stdout; const cause = error instanceof Error ? error : new Error(String(error)); const status = (_b = cause.status) !== null && _b !== void 0 ? _b : cause.code; throw new Error(detail ? `failed to compile "${props.file}" through ttsc:\n${detail}` : `failed to compile "${props.file}" through ttsc (exit code ${status !== null && status !== void 0 ? status : "unknown"}). Run \`npx ttsc -p ${projectFile}\` to see the underlying diagnostics.`, { cause }); } finally { fs_1.default.rmSync(wrapperRoot, { force: true, recursive: true }); } yield EmittedJavaScriptPatcher_1.EmittedJavaScriptPatcher.importMetaUrl(outputRoot); const configKey = emittedJavaScriptKey(projectRoot, configFile); const next = path_1.default.join(outputRoot, configKey); if (fs_1.default.existsSync(next) === false) throw new Error(`failed to materialize "${props.file}" through ttsc native transform.`); return next; }); const ensureMaterializedRoot = (projectRoot) => { const root = path_1.default.join(projectRoot, "node_modules", ".nestia", "config-loader"); fs_1.default.mkdirSync(root, { recursive: true }); MATERIALIZED_ROOTS.add(root); if (CLEANUP_REGISTERED === false) { CLEANUP_REGISTERED = true; const sweep = () => { for (const location of MATERIALIZED_ROOTS) fs_1.default.rmSync(location, { force: true, recursive: true }); }; process.once("exit", sweep); // process.once("exit", …) does not fire on SIGINT/SIGTERM. Without // these handlers a Ctrl-C during codegen leaves `run-*` and // `tsconfig-*` mkdtempSync directories behind under // node_modules/.nestia/config-loader/ until a subsequent clean exit. // The module-level CLEANUP_REGISTERED flag above guards against // re-entrancy within this module; we deliberately do NOT gate on // `process.listenerCount(signal) > 0` because the parallel sweep in // `ConfigAnalyzer.ensureRuntimeCleanup` (or any user-app SIGINT // handler) could register first, and that gate would skip our // registration — leaving MATERIALIZED_ROOTS unswept on Ctrl-C. // Windows note: `process.kill(pid, "SIGINT")` calls TerminateProcess // rather than re-raising; whichever module registers FIRST runs its // sweep, the second is skipped, RUNTIME/MATERIALIZED cleanup is // best-effort on Windows. SIGHUP is a no-op for most common code // paths on Windows (Node fires it on console-close and exits within // seconds). const onSignal = (signal) => { sweep(); process.kill(process.pid, signal); }; for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { process.once(signal, onSignal); } } return root; }; const emittedJavaScriptKey = (projectRoot, file) => { const relative = path_1.default.relative(projectRoot, file); const extension = path_1.default.extname(relative).toLowerCase(); const emitted = extension === ".mts" ? ".mjs" : extension === ".cts" ? ".cjs" : ".js"; return path_1.default .join(path_1.default.dirname(relative), `${path_1.default.basename(relative, extension)}${emitted}`) .split(path_1.default.sep) .join(path_1.default.posix.sep); }; const nodeAmbientCompilerOptions = (projectRoot, compilerOptions) => { const typeRoots = uniqueStrings([ ...asStringArray(compilerOptions.typeRoots), ...resolveNodeTypeRoots(projectRoot), ]); const types = uniqueStrings([ "node", ...asStringArray(compilerOptions.types).filter((value) => value !== "*"), ]); return Object.assign(Object.assign({}, (typeRoots.length !== 0 ? { typeRoots } : {})), { types }); }; const resolveNodeTypeRoots = (projectRoot) => { const roots = []; for (const base of [projectRoot, process.cwd(), __dirname]) try { const location = require.resolve("@types/node/package.json", { paths: [base], }); roots.push(path_1.default.dirname(path_1.default.dirname(location))); } catch (_a) { continue; } return roots; }; const asStringArray = (input) => Array.isArray(input) ? input.filter((elem) => typeof elem === "string") : []; const uniqueStrings = (input) => [...new Set(input)]; const sdkTransformEnv = () => process.env.NESTIA_SDK_TRANSFORM === undefined ? { NESTIA_SDK_TRANSFORM: "1" } : {}; const materializePlugins = (input) => { const plugins = Array.isArray(input) ? input .filter((p) => typeof p === "object" && p !== null) .map((p) => (Object.assign({}, p))) : []; const typia = plugins.find((p) => isTransform(p, "typia")); const core = plugins.find((p) => isTransform(p, "@nestia/core")); return [ Object.assign(Object.assign({}, (typia !== null && typia !== void 0 ? typia : {})), { transform: "typia/lib/transform", enabled: false }), normalizePlugin(Object.assign(Object.assign({}, (core !== null && core !== void 0 ? core : {})), { transform: "@nestia/core/native/transform.cjs" })), ]; }; const normalizePlugin = (plugin) => { const output = Object.assign({}, plugin); if (output.enabled === false) delete output.enabled; return output; }; const isTransform = (plugin, name) => typeof plugin.transform === "string" && plugin.transform.includes(name); const findConfigFile = (cwd, project) => { const candidate = path_1.default.isAbsolute(project) ? project : path_1.default.resolve(cwd, project); if (fs_1.default.existsSync(candidate)) return candidate; if (path_1.default.isAbsolute(project) || project.includes(path_1.default.sep)) return undefined; let current = path_1.default.resolve(cwd); while (true) { const next = path_1.default.join(current, project); if (fs_1.default.existsSync(next)) return next; const parent = path_1.default.dirname(current); if (parent === current) return undefined; current = parent; } }; const extractConfiguration = (file, loaded) => { const candidates = []; const collect = (value) => { if (isObject(value)) { candidates.push(value.default); candidates.push(value.NESTIA_CONFIG); if (isObject(value.default)) { candidates.push(value.default.default); candidates.push(value.default.NESTIA_CONFIG); } } candidates.push(value); }; collect(loaded); const matched = candidates.find((value) => Array.isArray(value) || (isObject(value) && Object.hasOwn(value, "input"))); if (matched === undefined) throw new Error(`invalid "${file}" data: configuration must be exported.`); return matched; }; const loadMaterializedModule = (file) => __awaiter(this, void 0, void 0, function* () { if (file.endsWith(".mjs")) { const dynamicImport = new Function("specifier", "return import(specifier)"); return dynamicImport((0, url_1.pathToFileURL)(file).href); } return require(file); }); const assertPlugins = (file, input) => { if (Array.isArray(input) && input.every((elem) => typeof elem === "object" && elem !== null)) return input; throw new Error(`invalid "${file}" data: compilerOptions.plugins must be an array.`); }; const assertConfigurations = (file, input) => { input.forEach((config, index) => assertConfig(file, config, index)); return input; }; const assertConfig = (file, input, index) => { if (isObject(input) === false) throw new Error(`invalid "${file}" data: configuration #${index} must be an object.`); const config = input; if (isInput(config.input) === false) throw new Error(`invalid "${file}" data: configuration #${index}.input is invalid.`); for (const [key, value] of [ ["output", config.output], ["distribute", config.distribute], ["e2e", config.e2e], ]) if (value !== undefined && typeof value !== "string") throw new Error(`invalid "${file}" data: configuration #${index}.${key} must be a string.`); for (const [key, value] of [ ["keyword", config.keyword], ["simulate", config.simulate], ["propagate", config.propagate], ["clone", config.clone], ["primitive", config.primitive], ["assert", config.assert], ["json", config.json], ]) if (value !== undefined && typeof value !== "boolean") throw new Error(`invalid "${file}" data: configuration #${index}.${key} must be a boolean.`); if (config.swagger !== undefined && isSwagger(config.swagger) === false) throw new Error(`invalid "${file}" data: configuration #${index}.swagger is invalid.`); }; const isInput = (input) => { if (typeof input === "string" || typeof input === "function" || isStringArray(input)) return true; if (isObject(input) === false) return false; return (isStringArray(input.include) && (input.exclude === undefined || isStringArray(input.exclude))); }; const isSwagger = (input) => { if (isObject(input) === false) return false; return (typeof input.output === "string" && (input.openapi === undefined || ["2.0", "3.0", "3.1", "3.2"].includes(input.openapi)) && (input.beautify === undefined || typeof input.beautify === "boolean" || typeof input.beautify === "number") && (input.additional === undefined || typeof input.additional === "boolean") && (input.decompose === undefined || typeof input.decompose === "boolean") && (input.operationId === undefined || typeof input.operationId === "function")); }; const isObject = (input) => typeof input === "object" && input !== null && Array.isArray(input) === false; const isStringArray = (input) => Array.isArray(input) && input.every((elem) => typeof elem === "string"); const readChildOutput = (error, key) => { if (!error || typeof error !== "object" || !(key in error)) return ""; const value = error[key]; if (value === null || value === undefined) return ""; if (typeof value === "string") return value.trim(); if (Buffer.isBuffer(value)) return value.toString("utf8").trim(); return ""; }; })(NestiaConfigLoader || (exports.NestiaConfigLoader = NestiaConfigLoader = {})); //# sourceMappingURL=NestiaConfigLoader.js.map