create-cttq
Version: 
CTTQ大前端脚手架项目
326 lines (303 loc) • 16 kB
JavaScript
/**
 * 集成能力库的入口
 */
const fs = require("fs");
const path = require("path");
const { logWithSpinner, succeedSpinner, failSpinner } = require("./util/spinner");
const TemplateManager = require("./TemplateManager");
const prompts = require("prompts");
const globby = require('./util/globby');
const recast = require("recast");
const ejs = require("ejs")
const mergePackage = require("./util/mergePackage");
const { FindImport, FindNode } = require("./recast/FilterDeclaration");
const { hasProjectNpm, hasProjectPnpm, hasProjectYarn, tryRun } = require("./util/shared")
const sortPkg = require("./util/sortPkg")
const injectInfo = require("./util/injectInfo")
function hasMonitor(main) {
    return main.includes("CTTQMonitor().init(");
}
function hasSensors(main) {
    return main.includes(".SensorsConfig.init(");
}
function getSetProperty(ast) {
    let setproperty;
    recast.visit(ast, {
        visitExpressionStatement({ node }) {
            if (node.expression && node.expression.callee && node.expression.callee.object && node.expression.callee.object.name == "Object" && node.expression.callee.property && node.expression.callee.property.name == "defineProperty" && node.expression.arguments && node.expression.arguments.length > 2 && node.expression.arguments[0].name == "window" && node.expression.arguments[1].value == "$app" && node.expression.arguments[2].properties && node.expression.arguments[2].properties.length > 1) {
                setproperty = node.expression.arguments[2].properties.filter(property => 
                    property.key && property.key.name == "set"
                ).pop();
            }
            return false;
        },
    })
    return setproperty;
}
async function integrateAbility() {
    let context = process.cwd();
    const { capability } = await prompts(
        [{
            type: "multiselect",
            name: "capability",
            message: "选择接入的能力",
            choices: [
                { title: "监控", value: "monitor" },
                { title: "埋点", value: "sensors" },
                { title: "自动刷新", value: "autorefresh" },
            ],
            min: 1,
            // hint: "- 用 Space / ← / → 键 来切换选择/取消状态,用 ↑ ↓ 切换问题。回车 确认",
            instructions: "\n  ↑/↓: 高亮可选项\n  ←/→/[space]: 切换选择/取消状态\n  a: 全选\n  enter/return: 确认选择",
            onState: (state) => {},
        }],
        {
            onCancel: () => {
                throw new Error(red("✖") + " 取消接入能力库");
            },
        },
    );
    try {
        logWithSpinner("开始集成能力库...");
        let templateManager = new TemplateManager(context, process.env.CTTQ_TEMPLATE_VERSION);
        logWithSpinner("下载模版库中...");
        await templateManager.download().catch(() => {
            failSpinner("模版库下载失败, 无法连接Gitlab");
            templateManager.delete();
            throw new Error("能力库集成失败");
        });
        logWithSpinner("集成能力库配置...");
        let monitorpath = templateManager.pathAt("monitor");
        let sensorspath = templateManager.pathAt("sensors");
        let autorefreshpath = templateManager.pathAt("auto-refresh");
        // 合并pkg
        let conflictDepSources = {};
        let pkgpath = path.resolve(context, "package.json");
        let pkg = JSON.parse(fs.readFileSync(pkgpath, "utf-8") || "{}");
        if (capability.includes("monitor")) {
            let monitorpkg = JSON.parse(fs.readFileSync(path.resolve(monitorpath, "package.json"), "utf-8") || "{}");
            mergePackage("monitor", pkg, monitorpkg, conflictDepSources);
        }
        if (capability.includes("sensors")) {
            let sensorspkg = JSON.parse(fs.readFileSync(path.resolve(sensorspath, "package.json"), "utf-8") || "{}");
            mergePackage("sensors", pkg, sensorspkg, conflictDepSources);
        }
        if (capability.includes("autorefresh")) {
            let autorefreshpkg = JSON.parse(fs.readFileSync(path.resolve(autorefreshpath, "package.json"), "utf-8") || "{}");
            mergePackage("autorefresh", pkg, autorefreshpkg, conflictDepSources);
        }
        pkg = sortPkg(pkg);
        fs.writeFileSync(pkgpath, JSON.stringify(pkg, null, 2) + "\n");
        injectInfo(context, "ability");
        // 渲染入口文件
        const files = await globby(context, ["**/*/main.js", "!node_modules", "!**/.template/**/*", "!.git", "!.git"]);
        // 变更文件列表
        let changeFiles = [];
        for (const rawPath of Object.keys(files)) {
            const fileContext = files[rawPath];
            const needMonitor = capability.includes("monitor") && !hasMonitor(fileContext);
            const needSensors = capability.includes("sensors") && !hasSensors(fileContext);
            const isApp = fileContext.includes('"@cttq/cttq"') || fileContext.includes("'@cttq/cttq'");
            let ast = recast.parse(fileContext);
            // 找到最后一个ImportDeclaration节点
            let lastImportIndex = -1;
            const lastImport = ast.program.body.filter(node => 
                recast.types.namedTypes.ImportDeclaration.check(node)
            ).pop();
            if (lastImport) {
                lastImportIndex = ast.program.body.indexOf(lastImport);
            }
            // 查找Vue变量
            if (!isApp) {
                let vueVariableName = "";
                let vueVariable;
                // 设置vue实例变量
                recast.visit(ast, {
                    visitExpressionStatement(path) {
                        const node = path.node;
                        if (recast.types.namedTypes.CallExpression.check(node.expression) && recast.types.namedTypes.MemberExpression.check(node.expression.callee) && recast.types.namedTypes.NewExpression.check(node.expression.callee.object) && recast.types.namedTypes.Identifier.check(node.expression.callee.object.callee) && node.expression.callee.object.callee.name == "Vue") {
                            return recast.types.builders.variableDeclaration(
                                "const",
                                [
                                    recast.types.builders.variableDeclarator(recast.types.builders.identifier("appVue"),recast.parse(recast.print(node).code).program.body[0].expression)
                                ]
                            );
                        }
                        return false;
                    }
                });
                vueVariable = ast.program.body.filter(node => {
                    if (recast.types.namedTypes.VariableDeclaration.check(node) && node.declarations && node.declarations.length > 0) {
                        for (const variable of node.declarations) {
                            if (recast.types.namedTypes.VariableDeclarator.check(variable) && recast.types.namedTypes.CallExpression.check(variable.init) && recast.types.namedTypes.MemberExpression.check(variable.init.callee) && variable.init.callee.object && variable.init.callee.object.callee && variable.init.callee.object.callee.name == "Vue") {
                                vueVariableName = variable.id.name;
                                return true;
                            }
                        }
                    }
                }
                ).pop();
                // 引入监控
                if (needMonitor) {
                    let mainContext = ejs.render(fs.readFileSync(templateManager.pathAt("monitor/main.js"), 'utf-8'), {
                        name: pkg.name,
                        userInfo: `${vueVariableName}.$store.state`
                    });
                    let mainAST = recast.parse(mainContext);
                    let imports = FindImport(mainAST);
                    if (imports && imports.length > 0) {
                        ast.program.body.splice(lastImportIndex + 1, 0, ...imports);
                    }
                    let otherNode = FindNode(mainAST, {exclude: ["ImportDeclaration"]});
                    if (otherNode && otherNode.length > 0) {
                        const vueVariableIndex = ast.program.body.indexOf(vueVariable);
                        ast.program.body.splice(vueVariableIndex + 1, 0, ...otherNode);
                    }
                }
                // 引入埋点
                if (needSensors) {
                    let mainContext = ejs.render(fs.readFileSync(templateManager.pathAt("sensors/main.js"), 'utf-8'), {
                        name: pkg.name,
                        description: pkg.description,
                        userInfo: "newValue"
                    });
                    let mainAST = recast.parse(mainContext);
                    let imports = FindImport(mainAST);
                    if (imports && imports.length > 0) {
                        ast.program.body.splice(lastImportIndex + 1, 0, ...imports);
                        for (const importDec of imports) {
                            mainAST.program.body.splice(mainAST.program.body.indexOf(importDec), 1);
                        }
                    }
                    let otherNode = FindNode(mainAST, {exclude: ["ImportDeclaration"]});
                    mainContext = `${vueVariableName}.$store.watch((state) => {
                        return (state.user || {}).userInfo;
                    }, (newValue) => {
                        if (newValue) {
                            ${recast.print(mainAST).code}
                        }
                    })`
                    otherNode = recast.parse(mainContext).program.body;
                    if (otherNode && otherNode.length > 0) {
                        const vueVariableIndex = ast.program.body.indexOf(vueVariable);
                        ast.program.body.splice(vueVariableIndex + 1, 0, ...otherNode);
                    }
                }
                if (needMonitor || needSensors) {
                    // 将AST转换回代码字符串
                    const sourcePath = path.resolve(context, rawPath);
                    let code = recast.prettyPrint(ast, {tabWidth: 4, useTabs: true}).code;
                    code = code.replace(/ {4}/g, "\t");
                    code = code.replace(/^\s*[\r\n]/gm, "");
                    fs.writeFileSync(sourcePath, code + "\n");
                    changeFiles.push(rawPath);
                }
            } else {
                // 引入监控
                let monitorCode = "";
                if (needMonitor) {
                    let mainContext = ejs.render(fs.readFileSync(templateManager.pathAt("monitor/main.js"), 'utf-8'), {
                        name: pkg.name,
                        userInfo: `app.$store.state`
                    });
                    let mainAST = recast.parse(mainContext);
                    let imports = FindImport(mainAST);
                    if (imports && imports.length > 0) {
                        ast.program.body.splice(lastImportIndex + 1, 0, ...imports);
                        for (const importDec of imports) {
                            mainAST.program.body.splice(mainAST.program.body.indexOf(importDec), 1);
                        }
                    }
                    monitorCode = recast.print(mainAST).code;
                }
                // 引入埋点
                let sensorsCode = "";
                if (needSensors) {
                    let mainContext = ejs.render(fs.readFileSync(templateManager.pathAt("sensors/main.js"), 'utf-8'), {
                        name: pkg.name,
                        description: pkg.description,
                        userInfo: "newValue"
                    });
                    let mainAST = recast.parse(mainContext);
                    let imports = FindImport(mainAST);
                    if (imports && imports.length > 0) {
                        ast.program.body.splice(lastImportIndex + 1, 0, ...imports);
                        for (const importDec of imports) {
                            mainAST.program.body.splice(mainAST.program.body.indexOf(importDec), 1);
                        }
                    }
                    sensorsCode = `app.$store.watch((state) => {
                        return (state.userInfo || {}).person;
                    }, (newValue) => {
                        if (newValue) {
                            ${recast.print(mainAST).code}
                        }
                    });`
                }
                if (monitorCode || sensorsCode) {
                    let setproperty = getSetProperty(ast);
                    if (!setproperty) {
                        let injectAst = recast.parse(`Object.defineProperty(window, "$app", {
                            get() {return this.__app;},
                            set(app) {
                                this.__app = app;
                            }})
                        `).program.body;
                        ast.program.body.splice(ast.program.body.length, 0, ...injectAst);
                        setproperty = getSetProperty(ast);
                    }
                    if (setproperty.value) {
                        let paramname = (setproperty.value.params[0] || {}).name || "app";
                        if (setproperty.value.body && setproperty.value.body.body) {
                            if (monitorCode) {
                                monitorCode.replace("app.$store", `${paramname}.$store`);
                                setproperty.value.body.body.push(...recast.parse(monitorCode).program.body);
                            }
                            if (sensorsCode) {
                                sensorsCode.replace("app.$store", `${paramname}.$store`);
                                setproperty.value.body.body.push(...recast.parse(sensorsCode).program.body);
                            }
                        }
                    }
                    // 将AST转换回代码字符串
                    const sourcePath = path.resolve(context, rawPath);
                    let code = recast.prettyPrint(ast, {tabWidth: 4, useTabs: true}).code;
                    code = code.replace(/ {4}/g, "\t");
                    code = code.replace(/^\s*[\r\n]/gm, "");
                    fs.writeFileSync(sourcePath, code + "\n");
                    changeFiles.push(rawPath);
                }
            }
        }
        templateManager.delete();
        // 自动更新项目
        if (hasProjectPnpm(context)) {
            logWithSpinner("运行 pnpm install 更新项目...");
            await tryRun("pnpm install").catch(() => {
                failSpinner("pnpm install 更新项目失败,需要自己手动更新");
            });
        } else if (hasProjectYarn(context)) {
            logWithSpinner("运行 yarn install 更新项目...");
            await tryRun("yarn install").catch(() => {
                failSpinner("yarn install 更新项目失败,需要自己手动更新");
            });
        } else if (hasProjectNpm(context) || fs.existsSync(path.resolve(context, "node_modules"))) {
            logWithSpinner("运行 npm install 更新项目...");
            await tryRun("npm install").catch(() => {
                failSpinner("npm install 更新项目失败,需要自己手动更新");
            });
        }
        succeedSpinner("能力库集成成功");
        if (changeFiles.length > 0) {
            console.log("已变更如下入口文件:");
            console.log("  " + changeFiles.join("\n  "));
        }
    } catch (error) {
        failSpinner("能力库集成失败");
    }
}
module.exports = (...args) => {
    return integrateAbility().catch((error) => {
        process.exit(1);
    });
}