import * as path from "path";
import { readFileSync, constants } from "node:fs";
import ts, { ExpressionStatement, Identifier, PropertyName, StringLiteral } from "typescript";
import Handlebars from "handlebars";
import { accessSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
import { HvigorNode, HvigorPlugin } from "hvigor";
import { json } from "stream/consumers";
export class DecorationPluginConfig {
moduleName?: string;
modulePath?: string;
annotation?: string;
scanFiles?: string[];
builderTpl?: string;
}
class NodeInfo {
value?: any;
}
class DecorationInfo {
methods: MethodInfo[] = [];
bindId: string = '';
}
class MethodInfo {
name: string;
value: string;
}
const PLUGIN_ID = "etsDecorationPlugin";
export function etsDecorationPlugin(pluginConfig: DecorationPluginConfig): HvigorPlugin {
pluginConfig.annotation = 'AutoAddInspector';
pluginConfig.builderTpl = 'methodBuilder.tpl'
return {
pluginId: PLUGIN_ID,
apply(node: HvigorNode) {
console.log(`Exec ${PLUGIN_ID}...${__dirname}`);
console.log(`Exec ${PLUGIN_ID}...${JSON.stringify(pluginConfig)}`);
console.log(`node:${node.getNodeName()},nodePath:${node.getNodePath()}`);
pluginConfig.moduleName = node.getNodeName();
pluginConfig.modulePath = node.getNodePath();
pluginExec(pluginConfig);
}
}
}
function pluginExec(config: DecorationPluginConfig) {
console.log("plugin exec...");
config.scanFiles.forEach((file) => {
let sourcePath = `${config.modulePath}/${file}`;
if (!sourcePath.endsWith('.ets')) {
sourcePath = sourcePath + '.ets';
}
const analyzer = new EtsAnalyzer(config, sourcePath);
analyzer.start();
if (analyzer.routerAnnotationExisted) {
if (analyzer.methodArray.length > 0) {
let fileContent = readFileSync(sourcePath, { encoding: "utf8" });
let firstHalfText = fileContent.slice(0, analyzer.positionOfBlockEnd - 1);
let secondHalfText = fileContent.slice(analyzer.positionOfBlockEnd - 1, fileContent.length)
if (analyzer.aboutToAppearExist) {
let beforeAboutToAppear = firstHalfText.slice(0, analyzer.positionOfAboutToAppear);
let inAboutToAppear =
firstHalfText.slice(analyzer.positionOfAboutToAppear, analyzer.positionOfAboutToAppearEnd - 2);
let afterAboutToAppear = firstHalfText.slice(analyzer.positionOfAboutToAppearEnd - 2, firstHalfText.length);
analyzer.decorationInfos.forEach((decorationInfo: DecorationInfo) => {
if (decorationInfo.bindId !== '' && !firstHalfText.includes(`${decorationInfo.bindId}Listener`)) {
beforeAboutToAppear = beforeAboutToAppear +
`\n\n ${decorationInfo.bindId}Listener: inspector.ComponentObserver = inspector.createComponentObserver('${decorationInfo.bindId}');\n`;
decorationInfo.methods.forEach((methodInfo: MethodInfo) => {
if (methodInfo.name === 'onDraw') {
inAboutToAppear =
inAboutToAppear +
` this.${decorationInfo.bindId}Listener.on('draw', this.${methodInfo.value});\n`
}
if (methodInfo.name === 'onLayout') {
inAboutToAppear =
inAboutToAppear +
` this.${decorationInfo.bindId}Listener.on('layout', this.${methodInfo.value});\n`
}
});
}
})
firstHalfText = beforeAboutToAppear + inAboutToAppear + ' ' + afterAboutToAppear;
} else {
analyzer.decorationInfos.forEach((decorationInfo: DecorationInfo) => {
if (decorationInfo.bindId !== '' && !firstHalfText.includes(`${decorationInfo.bindId}Listener`)) {
firstHalfText = firstHalfText + "\n" +
` ${decorationInfo.bindId}Listener: inspector.ComponentObserver = inspector.createComponentObserver('${decorationInfo.bindId}');\n\n aboutToAppear(): void {\n`;
decorationInfo.methods.forEach((methodInfo: MethodInfo) => {
if (methodInfo.name === 'onDraw') {
firstHalfText =
firstHalfText + ` this.${decorationInfo.bindId}Listener.on('draw', this.${methodInfo.value});\n`
}
if (methodInfo.name === 'onLayout') {
firstHalfText =
firstHalfText + ` this.${decorationInfo.bindId}Listener.on('layout', this.${methodInfo.value});`
}
});
firstHalfText = firstHalfText + '\n }\n';
}
})
firstHalfText = firstHalfText + '\n';
}
const builderPath = path.resolve(__dirname, `../${config.builderTpl}`);
const tpl = readFileSync(builderPath, { encoding: "utf8" });
const template = Handlebars.compile(tpl);
analyzer.methodArray.forEach((name: string) => {
const output = template({
functionName: name
});
firstHalfText = firstHalfText + output + "\n\n";
})
fileContent = firstHalfText + secondHalfText;
writeFileSync(sourcePath, fileContent, { encoding: "utf8" })
}
}
})
}
export class EtsAnalyzer {
sourcePath: string;
pluginConfig: DecorationPluginConfig;
keywordPos: number = 0;
routerAnnotationExisted: boolean = false;
positionOfStruct: number = -1;
positionOfComponent: number = -1;
positionOfBlock: number = -1;
positionOfBlockEnd: number = -1;
positionOfAboutToAppear: number = -1;
positionOfAboutToAppearEnd: number = -1;
currentMissDeclarationNode: ts.Node | undefined = undefined;
decorationInfos: DecorationInfo[] = [];
aboutToAppearExist: boolean = false;
bindId: string = '';
methodArray: string[] = [];
constructor(pluginConfig: DecorationPluginConfig, sourcePath: string) {
this.pluginConfig = pluginConfig;
this.sourcePath = sourcePath;
}
start() {
const sourceCode = readFileSync(this.sourcePath, "utf-8");
const sourceFile = ts.createSourceFile(this.sourcePath, sourceCode, ts.ScriptTarget.ES2021, false);
ts.forEachChild(sourceFile, (node: ts.Node) => {
console.log(JSON.stringify(node));
this.resolveNode(node);
});
}
resolveNode(node: ts.Node) {
switch (node.kind) {
case ts.SyntaxKind.MissingDeclaration:
this.resolveMissDeclaration(node);
break;
case ts.SyntaxKind.Decorator:
this.resolveDecoration(node);
break;
case ts.SyntaxKind.ExpressionStatement:
this.resolveExpressionStatement(node);
break;
case ts.SyntaxKind.Block:
this.resolveBlock(node);
break;
}
}
resolveMissDeclaration(node: ts.Node) {
this.currentMissDeclarationNode = node;
node.forEachChild((cnode) => {
this.resolveNode(cnode);
})
}
resolveDecoration(node: ts.Node) {
let decorator = node as ts.Decorator;
if (decorator.expression.kind === ts.SyntaxKind.CallExpression) {
const callExpression = decorator.expression as ts.CallExpression;
if (callExpression.expression.kind === ts.SyntaxKind.Identifier) {
const identifier = callExpression.expression as ts.Identifier;
if (identifier.text === this.pluginConfig.annotation) {
this.routerAnnotationExisted = true;
if (this.currentMissDeclarationNode !== undefined) {
this.positionOfStruct = this.currentMissDeclarationNode.end;
this.currentMissDeclarationNode = undefined;
}
const arg = callExpression.arguments[0];
if (arg.kind === ts.SyntaxKind.ObjectLiteralExpression) {
const properties = (arg as ts.ObjectLiteralExpression).properties;
let decoratorInfo: DecorationInfo = new DecorationInfo();
properties.forEach((propertie) => {
if (propertie.kind === ts.SyntaxKind.PropertyAssignment) {
if ((propertie.name as ts.StringLiteral).text !== 'bindId') {
let methodInfo: MethodInfo = new MethodInfo();
methodInfo.name = (propertie.name as ts.StringLiteral).text;
methodInfo.value = (propertie.initializer as ts.StringLiteral).text;
decoratorInfo.methods.push(methodInfo);
this.methodArray.push((propertie.initializer as ts.StringLiteral).text);
} else {
decoratorInfo.bindId = (propertie.initializer as ts.StringLiteral).text;
}
}
})
this.decorationInfos.push(decoratorInfo);
}
}
}
}
}
resolveExpressionStatement(node: ts.Node) {
if (node.pos === this.positionOfStruct) {
this.positionOfComponent = node.end;
}
if (node.pos === this.positionOfComponent) {
this.positionOfBlock = node.end;
}
if (node.pos === this.positionOfAboutToAppear) {
this.positionOfAboutToAppearEnd = node.end;
}
}
resolveBlock(node: ts.Node) {
if (node.pos === this.positionOfBlock) {
let method: ts.MethodDeclaration = undefined;
const block = node as ts.Block;
const statements = block.statements;
const methodNameArray: string[] = [];
statements.forEach((statement: ts.Statement) => {
if (statement.kind === ts.SyntaxKind.ExpressionStatement) {
const expression = (statement as ts.ExpressionStatement).expression;
if (expression.kind === ts.SyntaxKind.CallExpression) {
const callExpression = expression as ts.CallExpression;
if (callExpression.expression.kind === ts.SyntaxKind.Identifier) {
const identifier = callExpression.expression as Identifier;
methodNameArray.push(identifier.escapedText.toString());
if (identifier.escapedText === 'aboutToAppear') {
this.aboutToAppearExist = true;
this.positionOfAboutToAppear = statement.pos;
}
}
}
}
})
const temp = this.methodArray.filter((value: string, index: number) => {
return !methodNameArray.includes(value);
})
this.methodArray = temp;
this.positionOfBlockEnd = node.end;
}
}
}