@o3r/configuration
Version:
This module contains configuration-related features such as CMS compatibility, Configuration override, store and debugging. It enables your application runtime configuration and comes with an integrated ng builder to help you generate configurations suppo
287 lines (284 loc) • 19.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ngAddConfig = void 0;
exports.ngAddConfigFn = ngAddConfigFn;
const node_path_1 = require("node:path");
const schematics_1 = require("@angular-devkit/schematics");
const schematics_2 = require("@o3r/schematics");
const ts = require("typescript");
const configProperties = [
'dynamicConfig$', 'config', 'config$', 'configSignal'
];
const checkConfiguration = (componentPath, tree) => {
const files = [
node_path_1.posix.join((0, node_path_1.dirname)(componentPath), `${(0, node_path_1.basename)(componentPath, '.component.ts')}.config.ts`)
];
if (files.some((file) => tree.exists(file))) {
throw new schematics_2.O3rCliError(`Unable to add configuration to this component because it already has at least one of these files: ${files.join(', ')}.`);
}
const componentSourceFile = ts.createSourceFile(componentPath, tree.readText(componentPath), ts.ScriptTarget.ES2020, true);
const o3rClassDeclaration = componentSourceFile.statements
.find((statement) => ts.isClassDeclaration(statement)
&& (0, schematics_2.isO3rClassComponent)(statement));
if (o3rClassDeclaration.members.some((classElement) => ts.isPropertyDeclaration(classElement)
&& ts.isIdentifier(classElement.name)
&& configProperties.includes(classElement.name.escapedText.toString()))) {
throw new schematics_2.O3rCliError(`Unable to add config to this component because it already have at least one of these properties: ${configProperties.join(', ')}.`);
}
};
/**
* Add configuration to an existing component
* @param options
*/
function ngAddConfigFn(options) {
return async (tree, context) => {
try {
const componentPath = options.path;
const { name } = (0, schematics_2.getO3rComponentInfoOrThrowIfNotFound)(tree, componentPath);
checkConfiguration(componentPath, tree);
const properties = {
componentConfig: name.concat('Config'),
projectName: options.projectName || (0, schematics_2.getLibraryNameFromPath)(componentPath),
configKey: schematics_1.strings.underscore(name).toUpperCase(),
name: (0, node_path_1.basename)(componentPath, '.component.ts')
};
const createConfigFilesRule = (0, schematics_1.mergeWith)((0, schematics_1.apply)((0, schematics_1.url)('./templates'), [
(0, schematics_1.template)(properties),
(0, schematics_1.renameTemplateFiles)(),
(0, schematics_1.move)((0, node_path_1.dirname)(componentPath))
]), schematics_1.MergeStrategy.Overwrite);
const updateComponentRulesWithObservable = (0, schematics_1.chain)([
(0, schematics_2.addImportsRule)(componentPath, [
{
from: '@o3r/configuration',
importNames: [
'ConfigurationBaseService',
'ConfigurationObserver',
'DynamicConfigurable',
'O3rConfig'
]
},
{
from: `./${properties.name}.config`,
importNames: [
`${properties.configKey}_DEFAULT_CONFIG`,
`${properties.configKey}_CONFIG_ID`,
properties.componentConfig
]
},
{
from: '@angular/core',
importNames: [
'Input',
'Optional',
'OnChanges',
'SimpleChanges'
]
},
{
from: 'rxjs',
importNames: [
'Observable'
]
}
]),
() => {
const componentSourceFile = ts.createSourceFile(componentPath, tree.readText(componentPath), ts.ScriptTarget.ES2020, true);
const result = ts.transform(componentSourceFile, [
(0, schematics_2.addInterfaceToClassTransformerFactory)(`OnChanges, DynamicConfigurable<${properties.componentConfig}>`, schematics_2.isO3rClassComponent),
(ctx) => (rootNode) => {
const { factory } = ctx;
const visit = (node) => {
if (ts.isClassDeclaration(node) && (0, schematics_2.isO3rClassComponent)(node)) {
const propertiesToAdd = (0, schematics_2.generateClassElementsFromString)(`
@Input()
public config: Partial<${properties.componentConfig}> | undefined;
@O3rConfig()
private dynamicConfig$: ConfigurationObserver<${properties.componentConfig}>;
public config$: Observable<${properties.componentConfig}>;
`);
const constructorDeclaration = node.members.find((classElement) => ts.isConstructorDeclaration(classElement));
const configurationService = constructorDeclaration?.parameters.find((parameter) => !!parameter.type
&& ts.isTypeReferenceNode(parameter.type)
&& ts.isIdentifier(parameter.type.typeName)
&& parameter.type.typeName.escapedText.toString() === 'ConfigurationBaseService'
&& ts.isIdentifier(parameter.name));
if (!configurationService
&& constructorDeclaration?.parameters.find((parameter) => ts.isIdentifier(parameter.name)
&& parameter.name.escapedText.toString() === 'configurationService')) {
throw new schematics_2.O3rCliError(`Unable to add configurationService because there is already a constructor's parameter with this name in ${componentPath}.`);
}
const configurationServiceVariableName = configurationService?.name.escapedText.toString() || 'configurationService';
const configServiceParameter = (0, schematics_2.generateParametersDeclarationFromString)(`@Optional() ${configurationServiceVariableName}: ConfigurationBaseService`);
const configConstructorBlockStatements = (0, schematics_2.generateBlockStatementsFromString)(`
this.dynamicConfig$ = new ConfigurationObserver<${properties.componentConfig}>(${properties.configKey}_CONFIG_ID, ${properties.configKey}_DEFAULT_CONFIG, ${configurationServiceVariableName});
this.config$ = this.dynamicConfig$.asObservable();
`);
const newContructorDeclaration = constructorDeclaration
? factory.updateConstructorDeclaration(constructorDeclaration, ts.getModifiers(constructorDeclaration) || [], constructorDeclaration.parameters.concat(configurationService ? [] : configServiceParameter), constructorDeclaration.body
? factory.updateBlock(constructorDeclaration.body, constructorDeclaration.body.statements.concat(configConstructorBlockStatements))
: factory.createBlock(configConstructorBlockStatements, true))
: factory.createConstructorDeclaration([], configServiceParameter, factory.createBlock(configConstructorBlockStatements, true));
const isNgOnChangesMethod = (classElement) => ts.isMethodDeclaration(classElement)
&& ts.isIdentifier(classElement.name)
&& classElement.name.escapedText.toString() === 'ngOnChanges';
const ngOnChangesMethod = node.members.find((member) => isNgOnChangesMethod(member));
const changesVariableName = ngOnChangesMethod?.parameters[0].name.getText() || 'changes';
const ifStatementToAdd = (0, schematics_2.generateBlockStatementsFromString)(`
if (${changesVariableName}.config) {
this.dynamicConfig$.next(this.config);
}
`);
const newNgOnChanges = ngOnChangesMethod
? factory.updateMethodDeclaration(ngOnChangesMethod, ts.getModifiers(ngOnChangesMethod), ngOnChangesMethod.asteriskToken, ngOnChangesMethod.name, ngOnChangesMethod.questionToken, ngOnChangesMethod.typeParameters, ngOnChangesMethod.parameters?.length
? ngOnChangesMethod.parameters
: (0, schematics_2.generateParametersDeclarationFromString)(`${changesVariableName}: SimpleChanges`), ngOnChangesMethod.type, ngOnChangesMethod.body
? factory.updateBlock(ngOnChangesMethod.body, ngOnChangesMethod.body.statements.concat(ifStatementToAdd))
: factory.createBlock(ifStatementToAdd, true))
: factory.createMethodDeclaration([factory.createToken(ts.SyntaxKind.PublicKeyword)], undefined, factory.createIdentifier('ngOnChanges'), undefined, undefined, (0, schematics_2.generateParametersDeclarationFromString)(`${changesVariableName}: SimpleChanges`), undefined, factory.createBlock(ifStatementToAdd, true));
const decorators = ts.getDecorators(node);
const o3rDecorator = decorators.find((decorator) => (0, schematics_2.isO3rClassDecorator)(decorator));
const firstArg = o3rDecorator.expression.arguments[0];
const shouldUpdateDecorator = options.exposeComponent && ts.isObjectLiteralExpression(firstArg) && firstArg.properties.find((prop) => ts.isPropertyAssignment(prop)
&& prop.name?.getText() === 'componentType'
&& ts.isStringLiteral(prop.initializer)
&& prop.initializer.text === 'Component');
const newO3rDecorator = shouldUpdateDecorator
? factory.updateDecorator(o3rDecorator, factory.updateCallExpression(o3rDecorator.expression, o3rDecorator.expression.expression, o3rDecorator.expression.typeArguments, [
factory.createObjectLiteralExpression([
...o3rDecorator.expression.arguments[0].properties.filter((prop) => prop.name?.getText() !== 'componentType'),
factory.createPropertyAssignment('componentType', factory.createStringLiteral('ExposedComponent', true))
])
]))
: o3rDecorator;
const newModifiers = [newO3rDecorator]
.concat(decorators.filter((decorator) => !(0, schematics_2.isO3rClassDecorator)(decorator)))
.concat((ts.getModifiers(node) || []));
const newMembers = node.members
.filter((classElement) => !(ts.isConstructorDeclaration(classElement) || isNgOnChangesMethod(classElement)))
.concat(propertiesToAdd, newContructorDeclaration, newNgOnChanges)
.sort(schematics_2.sortClassElement);
(0, schematics_2.addCommentsOnClassProperties)(newMembers, {
config: '@inheritDoc',
dynamicConfig: 'Dynamic configuration based on the input override configuration and the configuration service if used by the application',
config$: '@inheritDoc'
});
return factory.updateClassDeclaration(node, newModifiers, node.name, node.typeParameters, node.heritageClauses, newMembers);
}
return ts.visitEachChild(node, visit, ctx);
};
return ts.visitNode(rootNode, visit);
}
]);
const printer = ts.createPrinter({
removeComments: false,
newLine: ts.NewLineKind.LineFeed
});
tree.overwrite(componentPath, printer.printFile(result.transformed[0]));
return tree;
}
]);
const updateComponentRulesWithSignal = (0, schematics_1.chain)([
(0, schematics_2.addImportsRule)(componentPath, [
{
from: '@angular/core',
importNames: [
'inject',
'input'
]
},
{
from: '@o3r/configuration',
importNames: [
'configSignal',
'O3rConfig',
'DynamicConfigurableWithSignal'
]
},
{
from: `./${properties.name}.config`,
importNames: [
`${properties.configKey}_DEFAULT_CONFIG`,
`${properties.configKey}_CONFIG_ID`,
properties.componentConfig
]
}
]),
() => {
const componentSourceFile = ts.createSourceFile(componentPath, tree.readText(componentPath), ts.ScriptTarget.ES2020, true);
const result = ts.transform(componentSourceFile, [
(0, schematics_2.addInterfaceToClassTransformerFactory)(`DynamicConfigurableWithSignal<${properties.componentConfig}>`, schematics_2.isO3rClassComponent),
(ctx) => (rootNode) => {
const { factory } = ctx;
const visit = (node) => {
if (ts.isClassDeclaration(node) && (0, schematics_2.isO3rClassComponent)(node)) {
const propertiesToAdd = (0, schematics_2.generateClassElementsFromString)(`
public config = input<Partial<${properties.componentConfig}>>();
@O3rConfig()
public readonly configSignal = configSignal(this.config, ${properties.configKey}_CONFIG_ID, ${properties.configKey}_DEFAULT_CONFIG);`);
const decorators = ts.getDecorators(node);
const o3rDecorator = decorators.find((decorator) => (0, schematics_2.isO3rClassDecorator)(decorator));
const firstArg = o3rDecorator.expression.arguments[0];
const shouldUpdateDecorator = options.exposeComponent && ts.isObjectLiteralExpression(firstArg) && firstArg.properties.find((prop) => ts.isPropertyAssignment(prop)
&& prop.name?.getText() === 'componentType'
&& ts.isStringLiteral(prop.initializer)
&& prop.initializer.text === 'Component');
const newO3rDecorator = shouldUpdateDecorator
? factory.updateDecorator(o3rDecorator, factory.updateCallExpression(o3rDecorator.expression, o3rDecorator.expression.expression, o3rDecorator.expression.typeArguments, [
factory.createObjectLiteralExpression([
...o3rDecorator.expression.arguments[0].properties.filter((prop) => prop.name?.getText() !== 'componentType'),
factory.createPropertyAssignment('componentType', factory.createStringLiteral('ExposedComponent', true))
])
]))
: o3rDecorator;
const newModifiers = [newO3rDecorator]
.concat(decorators.filter((decorator) => !(0, schematics_2.isO3rClassDecorator)(decorator)))
.concat((ts.getModifiers(node) || []));
const newMembers = propertiesToAdd.concat(node.members);
(0, schematics_2.addCommentsOnClassProperties)(newMembers, {
config: '@inheritDoc',
configSignal: '@inheritDoc'
});
return factory.updateClassDeclaration(node, newModifiers, node.name, node.typeParameters, node.heritageClauses, newMembers);
}
return ts.visitEachChild(node, visit, ctx);
};
return ts.visitNode(rootNode, visit);
}
]);
const printer = ts.createPrinter({
removeComments: false,
newLine: ts.NewLineKind.LineFeed
});
tree.overwrite(componentPath, printer.printFile(result.transformed[0]));
return tree;
}
]);
return (0, schematics_1.chain)([
createConfigFilesRule,
options.useSignal ? updateComponentRulesWithSignal : updateComponentRulesWithObservable,
options.skipLinter ? (0, schematics_1.noop)() : (0, schematics_2.applyEsLintFix)()
]);
}
catch (e) {
if (e instanceof schematics_2.NoOtterComponent && context.interactive) {
const shouldConvertComponent = await (0, schematics_2.askConfirmationToConvertComponent)();
if (shouldConvertComponent) {
return (0, schematics_1.chain)([
(0, schematics_1.externalSchematic)('@o3r/core', 'convert-component', {
path: options.path
}),
ngAddConfigFn(options)
]);
}
}
throw e;
}
};
}
/**
* Add configuration to an existing component
* @param options
*/
exports.ngAddConfig = (0, schematics_2.createOtterSchematic)(ngAddConfigFn);
//# sourceMappingURL=index.js.map