import type { MixinModuleInfo } from '@/scripts/mf-modules' import { updatePluginConfigContent } from '@/scripts/shared' import { installProjectNpmDependencies, getUpdateMinSystemVersionTask } from '@/scripts/shared' import { BACKEND_DIR_NAME, PLUGIN_SQL_RELATIVE_PATH } from '@ones-open/cli-utils' import { checkFileExists, getYamlConfigContent } from '@ones-open/cli-utils' import type { AbilityTemplateSchema, ProjectPluginConfig } from '@ones-open/cli-utils' import { clone, includes, isNil, isString, mergeWith, replace, omit } from '@senojs/lodash' import fse from 'fs-extra' import inquirer from 'inquirer' import type { ListrTask } from 'listr' import { nanoid } from 'nanoid' import { join } from 'path' import type { AddTasksGetter } from '../common' import { generateAddModuleTasks } from '../module' import { getAbilitiesInfo, getTargetAbilityInfo } from './ability-info' import { getAbilityRelateModule, checkRelateModule, getBindRelateModuleInfo, } from './ability-relate-module' import { getTargetAbilityTemplate, getAbilityTemplates, updateAbilityDependencies, } from './ability-template' import type { AbilityTemplateInfo } from './ability-template' interface AddAbilityTasksContext { newPluginConfig: ProjectPluginConfig sqlTempContent: string codeTempDestPath: string sqlTempDestPath: string tsTemplateName: string shouldUpdateDependency: boolean } const { readFile, copy, writeFile } = fse function replaceValueForStringifyTemplatesContent( stringifyTemplates: string, records: Record, ) { const clonedStr = clone(stringifyTemplates) const recordEntries = Object.entries(records) const result = recordEntries.reduce((acc, [key, value]) => { return replace(acc, `"${key}"`, isString(value) ? `"${value}"` : value.toString()) }, clonedStr) return result } async function getSubProjectPluginConfig({ name, templateKey, yamlContent, }: AbilityTemplateInfo & { yamlContent: AbilityTemplateSchema }): Promise<{ abilityTemplateConfig: AbilityTemplateSchema partialPluginConfig: Partial }> { const abilityTemplateConfig = yamlContent const { templates: rawTemplatesField, customizes } = abilityTemplateConfig const result = { abilityTemplateConfig, partialPluginConfig: {} as Partial, } if (isNil(rawTemplatesField)) return result const templates = { ...rawTemplatesField, // Generate UUID for ability abilities: rawTemplatesField?.abilities?.map((ability) => ({ id: nanoid(8), name: ability.name, templateName: templateKey, ...omit(ability, ['id', 'name', 'templateName']), })), } as Partial const isNeedPromptForCustomizes = !isNil(customizes) && customizes.length > 0 if (!isNeedPromptForCustomizes) { result.partialPluginConfig = templates return result } const stringifyTemplates = JSON.stringify(templates) const { isUseDefaultValue } = await inquirer.prompt<{ isUseDefaultValue: boolean }>({ type: 'confirm', name: 'isUseDefaultValue', message: `Do you want to add '${name}' with default values?`, default: true, }) if (isUseDefaultValue) { const defaultValueRecords = customizes.reduce>( (acc, { name, default: defaultValue }) => { acc[name!] = defaultValue return acc }, {}, ) const partialPluginConfigWithDefaultValue = JSON.parse( replaceValueForStringifyTemplatesContent(stringifyTemplates, defaultValueRecords), ) result.partialPluginConfig = partialPluginConfigWithDefaultValue return result } const answer = await inquirer.prompt>(customizes) const partialPluginConfigWithAnswer = JSON.parse( replaceValueForStringifyTemplatesContent(stringifyTemplates, answer), ) result.partialPluginConfig = partialPluginConfigWithAnswer return result } function getMergedPluginConfig( pluginConfigContent: ProjectPluginConfig, partialPluginConfig: Partial, ) { const clonedPluginConfigContent = clone(pluginConfigContent) const result = mergeWith(clonedPluginConfigContent, partialPluginConfig, (objValue, srcValue) => { if (Array.isArray(objValue)) { return objValue.concat(srcValue) } }) return result } function generateAbilityTsTemplateName(abilityInfo: AbilityTemplateInfo) { const { rawName, templateKey } = abilityInfo return rawName + (templateKey ? `-${templateKey}` : '') } async function generateAddModulesTasks( tailModule: MixinModuleInfo[], currentWorkingDirectory: string, ) { const subTasks: ListrTask[] = tailModule.map((module) => generateAddModuleTasks(module, currentWorkingDirectory), ) return subTasks } function generateAddAbilityTasks({ pluginConfigContent, abilityInfo, partialPluginConfig, currentWorkingDirectory, }: { pluginConfigContent: ProjectPluginConfig abilityInfo: AbilityTemplateInfo partialPluginConfig: Partial currentWorkingDirectory: string }) { const addAbilityTasks: ListrTask[] = [ { title: 'Generating new plugin config', task: (ctx) => { ctx.newPluginConfig = getMergedPluginConfig(pluginConfigContent, partialPluginConfig) }, }, { title: 'Updating plugin config', task: ({ newPluginConfig }) => updatePluginConfigContent(newPluginConfig), }, getUpdateMinSystemVersionTask(), { title: 'Update backend package.json', task: (ctx: AddAbilityTasksContext, task) => { return updateAbilityDependencies(abilityInfo?.packageJsonPath, task, ctx) }, }, { title: 'Install dependencies', task: (ctx, task) => { if (!ctx.shouldUpdateDependency) { task.skip('There are no dependencies that need to be installed here~') return } return installProjectNpmDependencies(currentWorkingDirectory) }, }, { title: 'Generating ability code template', skip: async (ctx) => { const { templatePath } = abilityInfo const isCodeTemplateExists = await checkFileExists(templatePath, { ignoreError: true }) if (!isCodeTemplateExists) { return 'No code template for current ability, skip generating' } const tsTemplateName = generateAbilityTsTemplateName(abilityInfo) const CODE_TEMP_DEST_PATH = join( currentWorkingDirectory, BACKEND_DIR_NAME, 'src', `${tsTemplateName}.ts`, ) const isCodeTemplateExistsOnProject = await checkFileExists(CODE_TEMP_DEST_PATH, { ignoreError: true, }) ctx.codeTempDestPath = CODE_TEMP_DEST_PATH ctx.tsTemplateName = tsTemplateName if (isCodeTemplateExistsOnProject) { return 'Code template for current ability already exists on project, skip generating' } }, task: async ({ codeTempDestPath, tsTemplateName }) => { const { templatePath } = abilityInfo const abilitiesIndexPath = join( currentWorkingDirectory, BACKEND_DIR_NAME, 'src', `index.ts`, ) const copyCodeTemplate = copy(templatePath, codeTempDestPath) const updateAbilityReference = writeFile( abilitiesIndexPath, `\nexport * from './${tsTemplateName}'`, { flag: 'a', }, ) return Promise.all([copyCodeTemplate, updateAbilityReference]) }, }, { title: 'Generating ability sql template', skip: async (ctx) => { const { sqlTemplatePath } = abilityInfo const isSqlTemplateExists = await checkFileExists(sqlTemplatePath, { ignoreError: true }) if (!isSqlTemplateExists) { return 'No sql template for current ability, skip generating' } const sqlTemplateContent = await readFile(sqlTemplatePath, 'utf8') ctx.sqlTempContent = sqlTemplateContent // Read plugin sql content and check if sql template content is already exists const pluginSqlPath = join(currentWorkingDirectory, PLUGIN_SQL_RELATIVE_PATH) const pluginSqlContent = await readFile(pluginSqlPath, 'utf8') // String content matching maybe have performance issue, but it is fine for now // TODO: use more lightweight way to determine whether a sql template has been added const isSqlTemplateExistsOnProject = includes(pluginSqlContent, sqlTemplateContent) if (isSqlTemplateExistsOnProject) { return 'Sql template for current ability already exists on project, skip generating' } ctx.sqlTempDestPath = pluginSqlPath }, task: async ({ sqlTempDestPath, sqlTempContent }) => { // Append sql template content to plugin sql content return writeFile(sqlTempDestPath, `\n${sqlTempContent}`, { flag: 'a' }) }, }, ] return addAbilityTasks } async function getAddAbilityTasks({ initialAnswer, pluginConfigContent, currentWorkingDirectory, }: Parameters[0]) { if (initialAnswer.target !== 'ability') throw new Error('Invalid target type') const abilityInfoMap = await getAbilitiesInfo(currentWorkingDirectory) const targetAbilityInfo = await getTargetAbilityInfo(abilityInfoMap) const abilityTemplates = await getAbilityTemplates(targetAbilityInfo) let targetTemplateInfo if (abilityTemplates instanceof Map) { targetTemplateInfo = await getTargetAbilityTemplate(abilityTemplates) } else { targetTemplateInfo = abilityTemplates } const abilityTemplateConfig = await getYamlConfigContent( targetTemplateInfo.yamlPath, ) const abilityRelateModule = getAbilityRelateModule(abilityTemplateConfig) if (abilityRelateModule) { checkRelateModule(abilityRelateModule, targetTemplateInfo.yamlPath, currentWorkingDirectory) } const { partialPluginConfig } = await getSubProjectPluginConfig({ ...targetTemplateInfo, yamlContent: abilityTemplateConfig, }) let pluginConfig = pluginConfigContent let tailModule: MixinModuleInfo[] = [] let abilityPluginConfig = partialPluginConfig if (abilityRelateModule) { const taskInfo = await getBindRelateModuleInfo(pluginConfigContent, partialPluginConfig) pluginConfig = taskInfo.pluginConfig tailModule = taskInfo.tailModule abilityPluginConfig = taskInfo.abilityPluginConfig } const addAbilityTasks = generateAddAbilityTasks({ pluginConfigContent: pluginConfig, abilityInfo: targetTemplateInfo, partialPluginConfig: abilityPluginConfig, currentWorkingDirectory, }) const addModuleTasks = await generateAddModulesTasks(tailModule, currentWorkingDirectory) return [...addAbilityTasks, ...addModuleTasks] } export { getAddAbilityTasks } export type { AbilityTemplateInfo, AddAbilityTasksContext }