import { generateModuleId, getAndCollectAllModuleId, isModuleHadEntry, isModuleHadSubModule, AddTypeForModuleAndEntryIsTrue, doesModuleHasModuleAndEntry, } from '@/scripts/mf-modules' import type { MixinModule, MixinModuleInfo, ModuleInfo, SubModuleInfo } from '@/scripts/mf-modules' import { updatePluginConfigContent } from '@/scripts/shared' import { WEB_DIR_NAME, WEB_MODULES_DIR_NAME, getOpConfig } from '@ones-open/cli-utils' import { batchValidateModuleConstraints } from '@ones-open/cli-utils' import type { ModuleTypeKey, ProjectPluginConfig, ProjectPluginConfigModulesField, } from '@ones-open/cli-utils' import { MODULE_TYPE } from '@ones-open/utils' import { cloneDeep } from '@senojs/lodash' import chalk from 'chalk' import enquirer from 'enquirer' import fse from 'fs-extra' import inquirer from 'inquirer' import { join, resolve, sep } from 'path' import type { AddTasksGetter } from './common' import { createWikiDecoratorEditorTemplate } from './frontend-template' const { writeFile, mkdir } = fse // Temporarily moving the old logic for adding modules // At present, the way of adding modules is too dynamic // And the content of the object is modified through the pointer // We know it is important to keep the function pure and immutable // So will be refactored whole file later // TODO: refactor the way of adding modules async function modifySubModuleInfo({ module, moduleType, modulePath }: MixinModuleInfo) { const { title } = await inquirer.prompt<{ title: string }>({ type: 'input', name: 'title', message: 'Please enter the sub module title:', validate: (rawInput: string) => { const input = rawInput.trim() if (input.length === 0) return 'Sub module title is required and cannot be empty' return true }, }) const moduleId = generateModuleId(moduleType) const subModule = { id: moduleId, title, } const subModuleInfo: SubModuleInfo = { module: subModule, moduleType, modulePath: `${modulePath}${moduleId}/`, } // TODO: refactor the way of adding modules if (Array.isArray(module?.modules)) { module.modules.push(subModuleInfo.module) } else { module.modules = [subModuleInfo.module] } return subModuleInfo } async function addSubModule( moduleInfo: MixinModuleInfo, { skipPrompt }: { skipPrompt: boolean } = { skipPrompt: false }, ): Promise { if (!skipPrompt) { const { needAddSubModule } = await inquirer.prompt<{ needAddSubModule: boolean }>({ type: 'confirm', name: 'needAddSubModule', message: 'continue to add a sub module:', validate: (rawInput: string) => { const input = rawInput.trim() if (input.length === 0) return 'Sub module title is required and cannot be empty' return true }, }) if (!needAddSubModule) return moduleInfo } const subModuleInfo = await modifySubModuleInfo(moduleInfo) return addSubModule(subModuleInfo) } // Only name are allowed for selected modules async function addModuleQuestion(moduleType: string) { const answer = await enquirer.prompt<{ title: string }>([ { type: 'input', name: 'title', message: `Please enter the module '${moduleType}' title you want to add:`, validate: (rawInput: string) => { const input = rawInput.trim() if (input.length === 0) return 'Module title is required and cannot be empty' return true }, }, ]) return answer } // You can change the question and answer for the selected module async function addOptionalModuleQuestion( pluginConfig: ProjectPluginConfig, targetModuleType?: string, ) { const opConfig = await getOpConfig() const allModuleTypes = Object.keys(MODULE_TYPE).filter((key) => { const targetKey = key as ModuleTypeKey return opConfig.onesVersion ? true // 如果指定了 onesVersion,则返回全量插槽,由约束校验逻辑进行插槽的过滤 : !Object.prototype.hasOwnProperty.call(MODULE_TYPE[targetKey], 'newModuleType') // 如果未指定 onesVersion,则直接返回被别名替换的插槽 }) as ModuleTypeKey[] const [modulesInfo] = await batchValidateModuleConstraints(allModuleTypes, { mode: 'add', pluginConfig, }) const cloneModulesInfo = cloneDeep(modulesInfo) const disabledModuleTypes = new Map() cloneModulesInfo.forEach((module) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const deprecated = MODULE_TYPE[module.name].deprecated if (deprecated) { const deprecatedTip = chalk.red(`(deprecated in ${deprecated})`) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore module.message = `${module.name} ${deprecatedTip}` } if (!module.disabled) { return } disabledModuleTypes.set(module.name, module.disabled) module.disabled = chalk.red(`(${module.disabled.replace(`[${module.name}] `, '')})`) }) if (cloneModulesInfo.every((module) => module.disabled)) { throw new Error('No available modules') } const answer = await enquirer.prompt<{ moduleType: string title: string }>([ { type: 'autocomplete', name: 'moduleType', message: 'Please select the module type you want to add:', choices: cloneModulesInfo, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore limit: 10, initial: targetModuleType && targetModuleType in MODULE_TYPE ? cloneModulesInfo.findIndex((i) => i.name === targetModuleType) : undefined, validate(value: string) { return disabledModuleTypes.get(value) || true }, }, { type: 'input', name: 'title', message: 'Please enter the module title:', validate: (rawInput: string) => { const input = rawInput.trim() if (input.length === 0) return 'Module title is required and cannot be empty' return true }, }, ]) return answer } async function questionForModuleAndEntryIsTrue(): Promise { const answer = await enquirer.prompt<{ addType: { [name: string]: AddTypeForModuleAndEntryIsTrue } }>([ { type: 'select', name: 'addType', message: 'Next step:', choices: [ { name: 'As a parent module(Continue to add sub module)', value: AddTypeForModuleAndEntryIsTrue.subModule, }, { name: 'As a parent module(Add sub module later)', value: AddTypeForModuleAndEntryIsTrue.none, }, { name: 'As a module(Disable to add sub module)', value: AddTypeForModuleAndEntryIsTrue.root, }, ], // eslint-disable-next-line @typescript-eslint/no-explicit-any result(this: any, name) { return this.map(name) }, }, ]) return Object.values(answer.addType)[0] } async function modifyModuleInfo( pluginConfig: ProjectPluginConfig, targetModuleType?: string, optional = true, ) { if (!optional && !targetModuleType) { throw new Error('when optional is true, target type cannot be undefined') } let moduleType let title // moduleType can be re-selected if (optional) { const answer = await addOptionalModuleQuestion(pluginConfig, targetModuleType) moduleType = answer.moduleType as ModuleTypeKey title = answer.title } else { const answer = await addModuleQuestion(targetModuleType as string) title = answer.title moduleType = targetModuleType as ModuleTypeKey } const moduleId = generateModuleId(moduleType) const modulePath = `${moduleId}/` const module = { id: moduleId, title, moduleType, // 这里的顺序会影响 plugin.yaml 写入顺序,所以放下面,生成的配置不会显得奇怪 // eslint-disable-next-line @typescript-eslint/no-explicit-any ...(MODULE_TYPE[moduleType] as any)?.default, } as ProjectPluginConfigModulesField // TODO: refactor the way of adding modules const existsModules = pluginConfig.modules as ProjectPluginConfigModulesField[] existsModules.push(module) let moduleInfo: ModuleInfo if (doesModuleHasModuleAndEntry(module)) { const addType = await questionForModuleAndEntryIsTrue() moduleInfo = { module, moduleType, modulePath, addType } } else { moduleInfo = { module, moduleType, modulePath } } if ( moduleInfo.addType === AddTypeForModuleAndEntryIsTrue.root || moduleInfo.addType === AddTypeForModuleAndEntryIsTrue.none ) { return moduleInfo } if (isModuleHadSubModule(moduleInfo.module)) return addSubModule(moduleInfo, { skipPrompt: moduleInfo.addType === AddTypeForModuleAndEntryIsTrue.subModule, }) if (isModuleHadSubModule(moduleInfo.module)) return addSubModule(moduleInfo) return moduleInfo } async function modifyModuleInfoForSubModule(existsModules: ProjectPluginConfigModulesField[]) { const { moduleIds, moduleIdToPath, moduleIdToType, moduleIdToModuleRef, moduleIdToRootModuleRef, } = getAndCollectAllModuleId(existsModules) const { moduleId } = await inquirer.prompt<{ moduleId: string }>({ type: 'list', name: 'moduleId', message: 'Please select the module you want to add a sub module:', choices: moduleIds, }) const parentModule = moduleIdToModuleRef.get(moduleId) as ProjectPluginConfigModulesField const parentModuleType = moduleIdToType.get(moduleId) as ModuleTypeKey const parentModulePath = moduleIdToPath.get(moduleId) as string const rootModule = moduleIdToRootModuleRef.get(moduleId) as ProjectPluginConfigModulesField const parentModuleInfo: ModuleInfo = { module: parentModule, moduleType: parentModuleType, modulePath: parentModulePath, } const subModule = await addSubModule(parentModuleInfo, { skipPrompt: true }) if (doesModuleHasModuleAndEntry(rootModule)) { return { ...subModule, addType: AddTypeForModuleAndEntryIsTrue.subModule, } } return subModule } async function getAddModuleInfo({ initialAnswer, pluginConfigContent, }: Pick[0], 'initialAnswer' | 'pluginConfigContent'>) { const legalTargetTypes = new Set(['module', 'sub-module']) if (!legalTargetTypes.has(initialAnswer.target)) throw new Error('Invalid target type') const clonedPluginConfigContent = cloneDeep(pluginConfigContent) if (!clonedPluginConfigContent?.modules?.length) { clonedPluginConfigContent.modules = [] } const existsModules = clonedPluginConfigContent.modules const { targetName, optional } = initialAnswer let tailModuleInfo: MixinModuleInfo if (initialAnswer.target === 'sub-module') { const isAllowToAddSubModule = existsModules?.length > 0 if (!isAllowToAddSubModule) { throw new Error( 'Can not add a sub module before adding a module, you may need to add a module first', ) } tailModuleInfo = await modifyModuleInfoForSubModule(existsModules) } else { tailModuleInfo = await modifyModuleInfo(clonedPluginConfigContent, targetName, optional) } if (isModuleHadEntry(tailModuleInfo.module, tailModuleInfo.addType)) { /** * The memory router has a breaking change for the module which uses the `@ones-open/router`, so we default to enable * memory router only for the new create module. */ tailModuleInfo.module.enableMemoryRouter = true tailModuleInfo.module.entry = generateModuleEntryPath(tailModuleInfo.modulePath) } return { tailModuleInfo, pluginConfigContent: clonedPluginConfigContent } } async function createModuleEntry(moduleFilesPath: string, { title, moduleType }: MixinModule) { await mkdir(moduleFilesPath, { recursive: true }) const ENTRY_INDEX_CSS = '#ones-mf-root {}' const ENTRY_INDEX_TSX = moduleType === 'ones:wiki:editor:decorator' ? createWikiDecoratorEditorTemplate(title) : `import React from 'react'` + `\nimport ReactDOM from 'react-dom'` + `\nimport { ConfigProvider } from '@ones-design/core'` + `\nimport { lifecycle, OPProvider } from '@ones-open/bridge'` + `\nimport './index.css'` + '\n' + `\nReactDOM.render(` + `\n ` + `\n ` + `\n ${title}` + `\n ` + `\n ,` + `\n document.getElementById('ones-mf-root')` + `\n)` + '\n' + `\nlifecycle.onDestroy(() => {` + `\n ReactDOM.unmountComponentAtNode(document.getElementById('ones-mf-root'))` + `\n})` + `\n` return Promise.all([ writeFile(resolve(moduleFilesPath, 'index.css'), ENTRY_INDEX_CSS), writeFile(resolve(moduleFilesPath, 'index.tsx'), ENTRY_INDEX_TSX), ]) } function generateModuleEntryPath(modulePath: string) { const TAIL_MODULE_ENTRY_PATH_STR = join(WEB_MODULES_DIR_NAME, modulePath, 'index.html') .split(sep) .join('/') return TAIL_MODULE_ENTRY_PATH_STR } // Creating module entry function generateAddModuleTasks(tailModuleInfo: MixinModuleInfo, currentWorkingDirectory: string) { const { module: tailModule, modulePath, addType } = tailModuleInfo const addModulesTasks = { title: `Creating module '${tailModule.id}' entry`, skip: () => !isModuleHadEntry(tailModule, addType), task: () => { const TAIL_MODULE_ENTRY_DIR_PATH = join( currentWorkingDirectory, WEB_DIR_NAME, 'src', WEB_MODULES_DIR_NAME, modulePath, ) return createModuleEntry(TAIL_MODULE_ENTRY_DIR_PATH, tailModule) }, } return addModulesTasks } // Updating plugin config function getUpdatePluginConfigTask(pluginConfigContent: ProjectPluginConfig) { const updatePluginConfigTask = { title: 'Updating plugin config', task: () => updatePluginConfigContent(pluginConfigContent), } return updatePluginConfigTask } async function getAddModuleTasks({ initialAnswer, pluginConfigContent, currentWorkingDirectory, }: Parameters[0]) { // get the new 'plugin.yaml' and module information const { pluginConfigContent: newPluginConfigContent, tailModuleInfo } = await getAddModuleInfo({ initialAnswer, pluginConfigContent, }) // adding a module const addModuleTasks = generateAddModuleTasks(tailModuleInfo, currentWorkingDirectory) // update plugin.yaml const updatePluginConfigTask = getUpdatePluginConfigTask(newPluginConfigContent) return [addModuleTasks, updatePluginConfigTask] } export { getAddModuleTasks, getAddModuleInfo, generateAddModuleTasks }