@o3r/components
Version:
This module contains component-related features (Component replacement, CMS compatibility, helpers, pipes, debugging developer tools...) It comes with an integrated ng builder to help you generate components compatible with Otter features (CMS integration
189 lines • 7.45 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComponentClassExtractor = void 0;
const tslib_1 = require("tslib");
const fs = tslib_1.__importStar(require("node:fs"));
const path = tslib_1.__importStar(require("node:path"));
const extractors_1 = require("@o3r/extractors");
const schematics_1 = require("@o3r/schematics");
const ts = tslib_1.__importStar(require("typescript"));
/**
* Component class extractor.
*/
class ComponentClassExtractor {
/**
* Component class extractor constructor
* @param source Typescript SourceFile node of the file
* @param logger Logger
* @param filePath Path to the file to extract the data from
*/
constructor(source, logger, filePath) {
this.source = source;
this.logger = logger;
this.filePath = filePath;
/** List of interfaces that a configurable component can implement */
this.CONFIGURABLE_INTERFACES = ['DynamicConfigurable', 'DynamicConfigurableWithSignal', 'Configurable'];
}
/**
* Indicates if the given decorator is a component decorator.
* @param decoratorNode The decorator node to test
*/
isComponentDecorator(decoratorNode) {
return new RegExp('^(@Component).*').test(decoratorNode.getText(this.source));
}
/**
* Get the component type from the given decorator node.
* @param decoratorNode The decorator node to get the component type from
*/
getComponentType(decoratorNode) {
if (ts.isCallExpression(decoratorNode.expression) && decoratorNode.expression.expression.getText(this.source) === 'O3rComponent') {
const arg1 = decoratorNode.expression.arguments[0];
if (ts.isObjectLiteralExpression(arg1)) {
return this.getComponentStructure(arg1.properties
.find((prop) => prop.name?.getText(this.source) === 'componentType')
?.initializer.getText(this.source));
}
}
}
/**
* Get the component selector from the given decorator node.
* @param decoratorNode The decorator node to get the component selector from
*/
getComponentSelector(decoratorNode) {
if (this.isComponentDecorator(decoratorNode)) {
const matches = /selector:\s*["'](.*)["']/.exec(decoratorNode.getText(this.source));
if (matches) {
return matches[1];
}
}
}
/**
* Sanitize component type by removing extra quotes
* Example: "'Page'" becomes 'Page'
* @param type
* @private
*/
sanitizeComponentType(type) {
if (!type) {
return;
}
return type.replaceAll(/["']/g, '');
}
getComponentStructure(type) {
const sanitizedType = this.sanitizeComponentType(type);
switch (sanitizedType) {
case 'Block': {
return 'BLOCK';
}
case 'Page': {
return 'PAGE';
}
case 'ExposedComponent': {
return 'EXPOSED_COMPONENT';
}
default: {
return 'COMPONENT';
}
}
}
/**
* Extract component information of a given class node
* @param classNode Typescript node of a class
*/
getComponentInformation(classNode) {
const regExp = new RegExp(`^(${this.CONFIGURABLE_INTERFACES.join('|')})`);
let configName;
let contextName;
let name;
let isDynamic = false;
let type = 'COMPONENT';
let selector;
let linkableToRuleset = false;
classNode.forEachChild((node) => {
if (!configName && ts.isHeritageClause(node)) {
node.forEachChild((implNode) => {
const interfaceValue = implNode.getText(this.source);
if (!configName && regExp.test(interfaceValue)) {
configName = interfaceValue.replace(/.*<(.*?)\s*(,\s*('\w*')\s*)?>.*/, '$1');
isDynamic = interfaceValue.startsWith('Dynamic');
}
else if (!contextName && interfaceValue.endsWith('Context')) {
contextName = interfaceValue;
}
else {
switch (interfaceValue) {
case 'LinkableToRuleset': {
linkableToRuleset = true;
break;
}
}
}
});
}
else if (ts.isDecorator(node)) {
selector = this.getComponentSelector(node);
type = this.getComponentType(node) || type;
}
else if (ts.isIdentifier(node)) {
name = node.getText(this.source);
}
});
if (configName) {
this.logger.debug(`Extracted component ${name} based on configuration ${configName}}`);
}
else {
this.logger.debug(`${name} is ignored because it is not a configurable component`);
}
const localizationFiles = (0, extractors_1.getLocalizationFileFromAngularElement)(classNode);
const localizationKeys = (localizationFiles || []).reduce((acc, file) => {
const resolvedFilePath = path.resolve(path.dirname(this.filePath), file);
const data = JSON.parse(fs.readFileSync(resolvedFilePath, 'utf8'));
return acc.concat(Object.keys(data));
}, []);
return name && type
? {
name, configName, contextName, isDynamicConfig: isDynamic, type, selector, linkableToRuleset,
...(localizationKeys.length > 0 ? { localizationKeys } : {})
}
: undefined;
}
/**
* Get the file path of the given class from the import
* @param className Name of the class to find in the imports
*/
getFilePath(className) {
if (!className) {
return undefined;
}
let res;
this.source.forEachChild((node) => {
if (!res && ts.isImportDeclaration(node) && new RegExp(className).test(node.getText(this.source))) {
const children = node.getChildren(this.source);
res = children.at(-2)?.getText(this.source).replace(/^["'](.*)["']/, '$1');
}
});
if (res && /^\./.test(res)) {
res = path.resolve(path.dirname(this.filePath), `${res}.ts`).replace(/[/\\]/g, '/');
}
return res;
}
/**
* Extract the component data of a typescript file
*/
extract() {
this.logger.debug(`Parsing configuration from ${this.filePath}`);
let componentInfo;
this.source.forEachChild((node) => {
if (!componentInfo && ts.isClassDeclaration(node) && (0, schematics_1.isO3rClassComponent)(node)) {
componentInfo = this.getComponentInformation(node);
}
});
if (componentInfo) {
componentInfo.configPath = this.getFilePath(componentInfo.configName);
componentInfo.contextPath = this.getFilePath(componentInfo.contextName);
}
return componentInfo;
}
}
exports.ComponentClassExtractor = ComponentClassExtractor;
//# sourceMappingURL=component-class.extractor.js.map
;