@sleekify/sleekify
Version:
A TypeScript decorator driven approach for developing web applications.
349 lines (348 loc) • 14.9 kB
JavaScript
;
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 (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__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.Annotation = void 0;
const node_fs_1 = __importDefault(require("node:fs"));
const lodash_1 = __importDefault(require("lodash"));
const node_path_1 = __importDefault(require("node:path"));
const PathUtil_1 = require("../utils/PathUtil");
const StringUtil_1 = require("../utils/StringUtil");
;
;
const EMPTY = {};
/**
* Gets or sets annotations on a class or class property.
*/
class Annotation {
constructor() {
throw new Error('Annotation cannot be instantiated');
}
/**
* Indicates whether the annotation exists on the class or property.
* @param target The class
* @param propertyKey The property name
* @param decorator The decorator associated with the annotation
* @returns The annotation's value.
*/
static exists(target, propertyKey, decorator) {
const settings = decorator.$annotations;
if (settings === undefined) {
return false;
}
const { key: decoratorKey, isInherited } = settings;
let valueList;
if (undefined === propertyKey) {
valueList = Annotation.getValueListForClass(target, decoratorKey, isInherited);
}
else {
valueList = Annotation.getValueListForProperty(target, propertyKey, decoratorKey, isInherited);
}
return valueList.length > 0;
}
/**
* Gets the annotation's value.
* @param target The class
* @param propertyKey The property name
* @param decorator The decorator associated with the annotation
* @returns The annotation's value.
*/
static get(target, propertyKey, decorator) {
const settings = decorator.$annotations;
if (settings === undefined) {
return;
}
const { key: decoratorKey, isAdditive, isInherited } = settings;
let valueList;
if (undefined === propertyKey) {
valueList = Annotation.getValueListForClass(target, decoratorKey, isInherited);
}
else {
valueList = Annotation.getValueListForProperty(target, propertyKey, decoratorKey, isInherited);
}
if (valueList.length === 0 || valueList.every(((v) => v === EMPTY))) {
/* no values */
return;
}
if (!lodash_1.default.isObject(valueList[0]) || lodash_1.default.isArray(valueList[0])) {
/* primitive or array value */
return valueList[0];
}
/* build an object which inherits from parent decorator values */
const objectValue = {};
for (const value of valueList) {
if (lodash_1.default.isObject(value)) {
lodash_1.default.defaultsDeep(objectValue, value);
}
if (!isAdditive) {
break;
}
}
return objectValue;
}
/**
* Finds classes annotated with the decoration by searching files recursively
* under the provided relative path. The classes must be exported as a named
* exports since default exports aren't supported.
*
* @param relativePath The relative file path to search
* @param decorator The decorator applied to the class
*/
static async getClassesAnnotatedWith(relativePath, decorator) {
const callerFilename = PathUtil_1.PathUtil.getCallerFilename();
const callerDirectory = PathUtil_1.PathUtil.filenameToDirectory(callerFilename);
const currentDir = process.cwd();
const tsConfigPath = node_path_1.default.join(currentDir, 'tsconfig.json');
let isTypeScript = false;
let hasTypesScriptOutDir = false;
if (/[.]ts$/.test(callerFilename)) {
isTypeScript = true;
}
if (isTypeScript && node_fs_1.default.existsSync(tsConfigPath)) {
const contents = node_fs_1.default.readFileSync(tsConfigPath);
const tsConfig = JSON.parse(contents.toString());
const outDir = tsConfig.compilerOptions?.outDir;
if (outDir != null && outDir.length > 0) {
hasTypesScriptOutDir = true;
}
}
let updatedRelativePath = relativePath.replace(/\//g, node_path_1.default.sep);
let updatedDirectory = callerDirectory;
const parentDirectoryRegex = new RegExp(`[.][.]\\${node_path_1.default.sep}`);
while (parentDirectoryRegex.test(updatedRelativePath)) {
updatedRelativePath = updatedRelativePath.substring(3);
updatedDirectory = updatedDirectory.substring(0, updatedDirectory.lastIndexOf(node_path_1.default.sep));
}
updatedRelativePath = updatedRelativePath.replace(`.${node_path_1.default.sep}`, '');
const globPath = `${updatedDirectory}${updatedRelativePath !== '' ? `${node_path_1.default.sep}${updatedRelativePath}` : ''}${node_path_1.default.sep}**${node_path_1.default.sep}`.replace(/\\/g, '/');
const myDirectory = __dirname;
const commonBasePath = PathUtil_1.PathUtil.getCommonBasePath(`${currentDir}${node_path_1.default.sep}`, `${myDirectory}${node_path_1.default.sep}`);
const mySegmentCount = myDirectory.replace(commonBasePath, '').split(node_path_1.default.sep).length;
const modulePaths = await PathUtil_1.PathUtil.getModulePaths(globPath, {
isTypeScript,
hasTypesScriptOutDir
});
const modulePathPrefixSuffix = `${currentDir}${node_path_1.default.sep}`.replace(commonBasePath, '');
const modulePathPrefix = `${`..${node_path_1.default.sep}`.repeat(mySegmentCount)}${modulePathPrefixSuffix}`;
const moduleBasePath = `${currentDir}${node_path_1.default.sep}`;
return await Annotation.importClasses(modulePaths, modulePathPrefix, moduleBasePath, decorator);
}
/**
* Every annotation must be registered with a unique key and its annotation options.
*
* @param decorator The decorator being used as an annotation
* @param key The decorator's unique key
* @param options The decorator's options (optional)
*/
static register(decorator, key, options) {
const annotatedDecorator = decorator;
if (annotatedDecorator.$annotations === undefined) {
annotatedDecorator.$annotations = {
key,
isInherited: options?.isInherited ?? true,
isAdditive: options?.isAdditive ?? true
};
}
}
/**
* Sets the annotation's value.
* @param target The class
* @param propertyKey The property name
* @param decorator The decorator being used as an annotation
* @param value The annotation's value
*/
static set(target, propertyKey, decorator, value) {
const annotatedDecorator = decorator;
if (annotatedDecorator.$annotations === undefined) {
throw new Error('The annotation has not been registered');
}
const decoratorKey = annotatedDecorator.$annotations.key;
const annotationMap = Annotation.getAnnotationMap(target, propertyKey);
if (undefined === annotationMap) {
return;
}
annotationMap[decoratorKey] = value ?? EMPTY;
}
static getAnnotationMap(target, propertyKey) {
if (undefined === propertyKey) {
const classReference = target;
const classKey = this.getClassKey(classReference);
/* class annotation */
if (undefined === classReference.$annotations) {
classReference.$annotations = {};
}
if (undefined === classReference.$annotations[classKey]) {
classReference.$annotations[classKey] = {};
}
return classReference.$annotations[classKey];
}
const descriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
const targetValue = target[propertyKey];
if (undefined !== descriptor && lodash_1.default.isFunction(targetValue)) {
const classReference = target.constructor.name === 'Function' ? target : target.constructor;
const classKey = this.getClassKey(classReference);
/* property annotation */
if (undefined === classReference.$annotations) {
classReference.$annotations = {};
}
if (undefined === classReference.$annotations[classKey]) {
classReference.$annotations[classKey] = {};
}
if (undefined === classReference.$annotations[classKey][propertyKey]) {
classReference.$annotations[classKey][propertyKey] = {};
}
return classReference.$annotations[classKey][propertyKey];
}
return undefined;
}
static getClassKey(target) {
return ClassKeyCache.get(target);
}
static getValueForClass(target, decoratorKey) {
if (undefined === target.$annotations) {
return undefined;
}
const classKey = this.getClassKey(target);
if (undefined === target.$annotations[classKey]) {
return undefined;
}
return target.$annotations[classKey][decoratorKey];
}
static getValueListForClass(target, decoratorKey, inherited) {
const valueList = [];
for (let currentTarget = target; currentTarget !== undefined && currentTarget?.constructor?.name !== 'Object'; currentTarget = Object.getPrototypeOf(currentTarget)) {
const value = Annotation.getValueForClass(currentTarget, decoratorKey);
if (value != null) {
valueList.push(value);
}
if (!inherited) {
break;
}
}
return valueList;
}
static getValueForProperty(target, propertyKey, decoratorKey) {
if (undefined === target.$annotations) {
return undefined;
}
const classKey = this.getClassKey(target);
if (undefined === target.$annotations[classKey]) {
return undefined;
}
if (undefined === target.$annotations[classKey][propertyKey]) {
return undefined;
}
return target.$annotations[classKey][propertyKey][decoratorKey];
}
static getValueListForProperty(target, propertyKey, decoratorKey, inherited) {
const valueList = [];
let isProperty = false;
const staticDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
const staticTargetValue = (target)[propertyKey];
if (undefined !== staticDescriptor && lodash_1.default.isFunction(staticTargetValue)) {
/* static property annotation */
isProperty = true;
}
else {
const instanceTargetValue = target.prototype?.[propertyKey];
if (lodash_1.default.isFunction(instanceTargetValue)) {
/* property annotation */
isProperty = true;
}
}
if (isProperty) {
for (let currentTarget = target; currentTarget !== undefined && currentTarget?.constructor?.name !== 'Object'; currentTarget = Object.getPrototypeOf(currentTarget)) {
const value = Annotation.getValueForProperty(currentTarget, propertyKey, decoratorKey);
if (value != null) {
valueList.push(value);
}
if (!inherited) {
break;
}
}
}
return valueList;
}
static async importClasses(modulePaths, modulePathPrefix, moduleBasePath, decorator) {
const classArray = [];
for (const modulePath of modulePaths) {
const relativeModulePath = `${modulePathPrefix}${modulePath.replace(moduleBasePath, '')}`.replace(/\\/g, '/');
/* ignore types and tests */
if (/[.]d[.]ts$/.test(relativeModulePath) || /[.]spec[.][jt]s$/.test(relativeModulePath)) {
continue;
}
const module = await Promise.resolve(`${relativeModulePath}`).then(s => __importStar(require(s)));
if (lodash_1.default.isObject(module)) {
for (const exportName in module) {
const exportValue = module[exportName];
if (lodash_1.default.isObject(exportValue)) {
if (Annotation.exists(exportValue, undefined, decorator)) {
classArray.push(exportValue);
}
}
}
}
}
return classArray;
}
}
exports.Annotation = Annotation;
/**
* Class keys are hash codes of the cache definition. Cache them to improve performance.
*/
class ClassKeyCache {
static classNameMap = {};
/**
* Get the key for the given class.
*
* @param target The class
* @returns The class key or zero.
*/
static get(target) {
const constructorName = target?.prototype?.constructor?.name;
let entry = {
reference: target,
key: 0
};
if (constructorName != null) {
let entryList = ClassKeyCache.classNameMap[constructorName];
if (entryList === undefined) {
entryList = [];
ClassKeyCache.classNameMap[constructorName] = entryList;
}
entry = entryList.find((v) => v.reference === target);
if (entry === undefined) {
entry = {
reference: target,
key: StringUtil_1.StringUtil.getHashCode(target.toString())
};
entryList.push(entry);
}
}
return entry.key;
}
}