@nestia/sdk
Version:
Nestia SDK and Swagger generator
329 lines • 16.8 kB
JavaScript
;
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