UNPKG

@ibyar/cli

Version:

The Ibyar/Aurora CLI tool

192 lines 12.5 kB
import ts from 'typescript/lib/tsserverlibrary.js'; import { buildExpressionNodes } from '@ibyar/core/node.js'; import { htmlParser } from '@ibyar/elements/node.js'; import { getExtendsTypeBySelector } from '../elements/tags.js'; import { createConstructorOfViewInterfaceDeclaration, convertToProperties, createInterfaceType, createSignalsAssignment } from './factory.js'; import { moduleManger } from './modules.js'; import { getInputs, getOutputs, scanSignals, getTextValueFormLiteralProperty, isComponentDecorator } from './helpers.js'; import { SIGNAL_NAMES } from './signals.js'; /** * search for `@Component({})` and precompile the source code * @param program a ts program * @returns a transformer factory of source file */ export function beforeCompileComponentOptions(program) { const typeChecker = program.getTypeChecker(); return (context) => { return sourceFile => { let visitSourceFile = false; let componentPropertyName; const signals = {}; for (const statement of sourceFile.statements) { if (ts.isImportDeclaration(statement)) { const modulePath = statement.moduleSpecifier.getText(); if (statement.importClause && modulePath.includes('@ibyar/aurora') || modulePath.includes('@ibyar/core')) { statement.importClause?.namedBindings?.forEachChild(importSpecifier => { if (ts.isImportSpecifier(importSpecifier)) { const importName = importSpecifier.propertyName?.getText() ?? importSpecifier.name.getText(); if (importName === 'Component') { visitSourceFile = true; componentPropertyName = importSpecifier.name.getText(); } else if (SIGNAL_NAMES.includes(importName)) { signals[importName] = importSpecifier.name.getText(); } } }); } } if (visitSourceFile) { break; } } if (!visitSourceFile) { return sourceFile; } const classes = []; const updateSourceFile = ts.visitNode(sourceFile, (node) => { return ts.visitEachChild(node, (childNode) => { let decoratorArguments; if (ts.isClassDeclaration(childNode)) { const decorators = ts.getDecorators(childNode); if (!decorators || decorators.length === 0) { return childNode; } const hasDecorator = decorators.some(decorator => isComponentDecorator(decorator, componentPropertyName)); if (!hasDecorator) { return childNode; } const viewInfos = []; const signalMetadata = scanSignals(childNode, signals); const modifiers = childNode.modifiers?.map(modifier => { if (!ts.isDecorator(modifier)) { return modifier; } const updateDecoratorOptions = (option) => { const selector = getTextValueFormLiteralProperty(option, 'selector'); if (!selector) { console.error(`Component missing selector name: ${childNode.name?.getText()}`); return option; } const viewName = `HTML${selector.split('-') .map(name => name.replace(/^\w/, char => char.toUpperCase())) .join('')}Element`; const extendProperty = option.properties.find(prop => prop.name?.getText() === 'extend'); const extend = extendProperty?.initializer.getText().substring(1, extendProperty?.initializer.getText().length - 1); const extendsType = getExtendsTypeBySelector(extend); const formAssociatedProperty = option.properties.find(prop => prop.name?.getText() === 'formAssociated'); const formAssociated = formAssociatedProperty?.initializer.getText().substring(1, formAssociatedProperty?.initializer.getText().length - 1); const disabledFeaturesInitializer = option.properties.find(prop => prop.name?.getText() === 'disabledFeatures')?.initializer; let disabledFeatures = undefined; if (disabledFeaturesInitializer && ts.isArrayLiteralExpression(disabledFeaturesInitializer)) { disabledFeatures = disabledFeaturesInitializer.elements.filter(ts.isStringLiteral).map(el => el.getText(sourceFile)); } viewInfos.push({ selector, viewName, extendsType, interfaceType: createInterfaceType(viewName, extendsType), formAssociated: 'true' == formAssociated, disabledFeatures: disabledFeatures, }); const stylesProperty = option.properties.find(prop => prop.name?.getText() === 'styles'); const styles = stylesProperty?.initializer.getText().substring(1, stylesProperty?.initializer.getText().length - 1); const template = option.properties.find(prop => prop.name?.getText() === 'template'); if (template && ts.isPropertyAssignment(template) && ts.isStringLiteralLike(template.initializer)) { const text = template?.initializer.getText().substring(1, template?.initializer.getText().length - 1); const html = styles ? `<style>${styles}</style>${text}` : text; const domNode = htmlParser.toDomRootNode(html); buildExpressionNodes(domNode); const serialized = JSON.stringify(domNode); const json = JSON.parse(serialized); const name = ts.factory.createIdentifier('compiledTemplate'); const initializer = ts.factory.createObjectLiteralExpression(convertToProperties(json)); const update = ts.factory.updatePropertyAssignment(template, name, initializer); const signalsOption = createSignalsAssignment(signalMetadata); decoratorArguments = option.properties.map(prop => { if (template === prop) { return update; } return prop; }).filter(prop => { if (prop.name?.getText(sourceFile) === 'styles') { return false; } return true; }).concat([signalsOption]); return ts.factory.updateObjectLiteralExpression(option, decoratorArguments); } return option; // options.properties.filter((prop: ts.PropertyAssignment) => { // switch (prop.name.getText()) { // case 'template': // case 'templateUrl': // case 'styles': // return true; // default: return false; // } // }) }; const decoratorCall = modifier.expression; const options = decoratorCall.arguments[0]; let argumentsOption; if (ts.isArrayLiteralExpression(options)) { const elements = options.elements.map(element => { if (ts.isObjectLiteralExpression(element)) { return updateDecoratorOptions(element); } return element; }); argumentsOption = ts.factory.updateArrayLiteralExpression(options, elements); } else if (ts.isObjectLiteralExpression(options)) { argumentsOption = updateDecoratorOptions(options); } if (argumentsOption) { return ts.factory.updateDecorator(modifier, ts.factory.updateCallExpression(decoratorCall, decoratorCall.expression, decoratorCall.typeArguments, [argumentsOption])); } return modifier; }); classes.push({ type: 'component', name: childNode.name?.getText() ?? '', views: viewInfos, inputs: getInputs(childNode, typeChecker), outputs: getOutputs(childNode, typeChecker), signals: signalMetadata, }); // const staticMembers = viewInfos.map( // viewInfo => ts.factory.createPropertyDeclaration( // [ // ts.factory.createModifier(ts.SyntaxKind.StaticKeyword), // ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword) // ], // viewInfo.viewName, // undefined, // createStaticPropertyViewType(viewInfo.viewName), // undefined, // ) // ); return ts.factory.updateClassDeclaration(childNode, modifiers ? ts.factory.createNodeArray(modifiers) : void 0, childNode.name, childNode.typeParameters, childNode.heritageClauses, [/*...staticMembers,*/ ...childNode.members.slice()]); } return childNode; }, context); }); if (classes.length) { moduleManger.add({ path: sourceFile.fileName, classes }); const statements = updateSourceFile.statements?.slice() ?? []; statements.push(createConstructorOfViewInterfaceDeclaration()); const interfaces = classes.flatMap(s => s.views).map(info => { return ts.setTextRange(info.interfaceType, updateSourceFile); }); statements.push(); //emit inputs and outputs for component and directives; statements.push(...interfaces); return ts.factory.updateSourceFile(sourceFile, statements, updateSourceFile.isDeclarationFile, updateSourceFile.typeReferenceDirectives, updateSourceFile.referencedFiles, updateSourceFile.hasNoDefaultLib, updateSourceFile.libReferenceDirectives); } return updateSourceFile; }; }; } //# sourceMappingURL=before-component.js.map