@nodeboot/aot
Version:
Node-Boot module for Ahead-of-Time (AOT) compilation. Generates node-boot beans and OpenAPI schemas at compile time
193 lines • 7.43 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const typescript_1 = __importDefault(require("typescript"));
const typescript_json_schema_1 = require("typescript-json-schema");
/**
* Node-Boot AOT Model Schema Generator
*
* This script performs **Ahead-of-Time (AOT)** compilation of all `@Model`-decorated
* TypeScript classes found in the `src/` directory into a single OpenAPI-compatible
* JSON schema file.
*
* ### Features:
* - **Decorator-Driven**: Detects classes annotated with `@Model()` via static analysis.
* - **JSON Schema Output**: Uses `typescript-json-schema` to convert TypeScript models into JSON Schema.
* - **OpenAPI Ready**: Wraps output under `components.schemas` for OpenAPI integration.
* - **Schema Merging**: Merges multiple model schemas into one unified structure.
* - **Definition Ref Rewriting**: Converts `#/definitions/` to `#/components/schemas/`.
*
* ### Output:
* Writes merged schema to:
* ```
* dist/node-boot-models.json
* ```
*
* ### Usage:
* Run manually:
* ```sh
* node node-boot-aot-models.js
* ```
*
* Or add to your `package.json` scripts:
* ```json
* {
* "scripts": {
* "postbuild": "node node-boot-aot-models.js"
* }
* }
* ```
*
* ### Requirements:
* - Ensure you have installed `typescript-json-schema`:
* ```sh
* npm install --save-dev typescript-json-schema
* ```
* - Decorate your DTOs with `@Model()` from `@nodeboot/context`.
*
* ---
* Inspired by AOT techniques to speed up runtime behavior by generating schemas ahead of startup.
*
* @author Manuel Santos <https://github.com/manusant>
*/
const SRC_DIR = path_1.default.resolve(process.cwd(), "src");
const OUTPUT_PATH = path_1.default.resolve(process.cwd(), "dist/node-boot-models.json");
/**
* Recursively searches for `.ts` files that contain the `@Model` decorator.
*
* @param {string} dir - Directory to search recursively.
* @returns {string[]} Array of absolute file paths that contain `@Model`.
*/
function findModelFiles(dir) {
const result = [];
for (const entry of fs_1.default.readdirSync(dir)) {
const fullPath = path_1.default.join(dir, entry);
const stat = fs_1.default.statSync(fullPath);
if (stat.isDirectory()) {
result.push(...findModelFiles(fullPath));
}
else if (entry.endsWith(".ts") &&
!entry.endsWith(".d.ts")
//&& fs.readFileSync(fullPath, "utf-8").includes("@Model")
) {
result.push(fullPath);
}
}
return result;
}
/**
* Creates a TypeScript program from a list of file paths.
*
* @param {readonly string[]} files - Paths to TypeScript files.
* @returns {ts.Program} A TypeScript program instance.
*/
function createProgramFromFiles(files) {
return typescript_1.default.createProgram(files, {
strictNullChecks: true,
experimentalDecorators: true,
emitDecoratorMetadata: true,
target: typescript_1.default.ScriptTarget.ESNext,
module: typescript_1.default.ModuleKind.CommonJS,
});
}
/**
* Wraps collected schemas into an OpenAPI-compatible components object,
* and removes unresolved $ref entries by replacing them with an empty object.
*
* @param {Record<string, object>} schemas - Individual schemas keyed by model name.
* @returns {object} Merged and cleaned OpenAPI-compatible schema structure.
*/
function mergeSchemas(schemas) {
const cleanedSchemas = {};
// Helper to clean unresolved $ref
const resolveRefs = obj => {
if (Array.isArray(obj)) {
return obj.map(resolveRefs);
}
else if (obj && typeof obj === "object") {
if (obj.$ref) {
const refMatch = obj.$ref.match(/^#\/definitions\/(.+)$/);
if (refMatch) {
const refName = refMatch[1];
if (!schemas[refName]) {
// Replace unresolved $ref with empty object
return {};
}
}
}
const result = {};
for (const key of Object.keys(obj)) {
result[key] = resolveRefs(obj[key]);
}
return result;
}
return obj;
};
for (const [key, schema] of Object.entries(schemas)) {
cleanedSchemas[key] = resolveRefs(schema);
}
return {
$schema: "http://json-schema.org/draft-07/schema#",
components: {
schemas: cleanedSchemas,
},
};
}
function extractModelAndEnumNames(filePath) {
const content = fs_1.default.readFileSync(filePath, "utf-8");
const modelMatches = [...content.matchAll(/@Model\(\)\s+export\s+class\s+(\w+)/g)];
const enumMatches = [...content.matchAll(/export\s+enum\s+(\w+)/g)];
const modelNames = modelMatches.map(([, name]) => name);
const enumNames = enumMatches.map(([, name]) => name);
return {
modelNames,
enumNames,
};
}
/**
* Main logic for generating and saving the OpenAPI schema
* by scanning model files and converting classes to JSON Schema.
*/
function runAOTModelSchema() {
console.log("🧠 Generating OpenAPI model schemas from @Model classes...");
const modelFiles = findModelFiles(SRC_DIR);
if (!modelFiles.length) {
console.warn("⚠️ No @Model classes found.");
return;
}
const program = createProgramFromFiles(modelFiles);
const schemas = {};
for (const sourceFile of modelFiles) {
const { modelNames, enumNames } = extractModelAndEnumNames(sourceFile);
for (const className of modelNames.concat(enumNames)) {
// @ts-ignore
const schema = (0, typescript_json_schema_1.generateSchema)(program, className, {
ref: true, // ⚠️ Enables `$ref` usage — better for reusability
topRef: false, // Do NOT wrap entire schema in one top-level $ref
titles: true, // Adds `title` field to each schema (improves Swagger UI)
defaultProps: true, // Include default values if present
ignoreErrors: true, // Prevent crashes on edge types
aliasRef: true, // Keeps references to type aliases (important for enums and Records)
validate: false, // Speeds up schema generation
});
if (schema) {
delete schema["$schema"]; // Remove top-level $schema key
delete schema["definitions"]; // Remove inlined definitions
schemas[className] = schema;
}
}
}
const merged = mergeSchemas(schemas);
fs_1.default.mkdirSync(path_1.default.dirname(OUTPUT_PATH), { recursive: true });
// Replace local $ref references to definitions with OpenAPI-style component references
const modelsSchema = JSON.stringify(merged, null, 2).replace(/#\/definitions\//g, "#/components/schemas/");
fs_1.default.writeFileSync(OUTPUT_PATH, modelsSchema);
console.log(`✅ ${Object.keys(schemas).length} Model schemas saved to ${OUTPUT_PATH}`);
}
runAOTModelSchema();
//# sourceMappingURL=node-boot-aot-model-schema.js.map