import { logLevelToWebpackStatsPresetMap, currentLogLevel } from '@/output' import { getCheckAbilityRelateModuleTask } from '@/scripts/relate-module' import { getPluginConfigContent, updatePluginConfigContent } from '@/scripts/shared' import { getUpdateMinSystemVersionTask } from '@/scripts/shared' import { LOCAL_CONFIG_RELATIVE_PATH, PLUGIN_CONFIG_RELATIVE_PATH, RELATIVE_FILE_PATH_FOR_VALIDATE, batchValidateAbilityConstraints, batchValidateModuleConstraints, getPluginAbilityTypes, getPluginModuleTypes, } from '@ones-open/cli-utils' import { checkProjectFilesExists, PluginInvokeProcess, PluginLifecycle } from '@ones-open/cli-utils' import type { ProjectLocalConfig, ProjectPluginConfig } from '@ones-open/cli-utils' import type { InvokeCLIOptionsSchema, LoginRequestParamsSchema } from '@ones-open/cli-utils' import { MODULE_TYPE } from '@ones-open/utils' import { isBoolean, isNil, isUndefined } from '@senojs/lodash' import inquirer from 'inquirer' import type { QuestionCollection, Answers } from 'inquirer' import type { ListrTask } from 'listr' import { join } from 'path' import webpack from 'webpack' import type { Configuration, StatsOptions } from 'webpack' import webpackDevServer from 'webpack-dev-server' import { buildPluginProjectBackEnd, getDefaultWebpackConfig } from '../build' import { hasWorkspacesInPackageJson } from '../check-workspaces' import { convertUsernameField, fetchUserProfile, getLocalConfigContent } from '../login' import { cwd } from 'process' type InvokeProcess = `${PluginInvokeProcess}` type InvokeLifecycle = `${PluginLifecycle}` type InvokeTarget = InvokeProcess | InvokeLifecycle interface InvokeCLISchemedOptions { webpackStatsPreset: StatsOptions['preset'] reinstallPlugin: string } const ALLOW_PROCESS = Object.values(PluginInvokeProcess) const ALLOW_LIFECYCLE = Object.values(PluginLifecycle) const ALLOW_PROCESS_SET = new Set(ALLOW_PROCESS) const ALLOW_LIFECYCLE_SET = new Set(ALLOW_LIFECYCLE) const DEFAULT_WEB_DEV_SERVER_PORT = 3000 const OLD_NODE_HOST_PATH = 'backend/node_modules/@ones-op/node-host/dist/index.js' const NODE_HOST_PATH = 'node_modules/@ones-op/node-host/dist/index.js' async function getNodeHostPath() { const hasWorkspace = await hasWorkspacesInPackageJson() if (hasWorkspace) { return NODE_HOST_PATH } return OLD_NODE_HOST_PATH } function generateQuestionCollection(pluginConfig: ProjectPluginConfig) { const pluginModules = pluginConfig?.modules ?? [] const prefixMessage = 'There is an expired slot in the slot you are using.\n' const innerMessage = pluginModules.reduce((initMessage, currentModule) => { const moduleType = currentModule?.moduleType as keyof typeof MODULE_TYPE let innerMessage = initMessage // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const newModuleType = MODULE_TYPE[moduleType].newModuleType if (newModuleType) { innerMessage = `${innerMessage} ${moduleType} -> ${newModuleType}\n` } return innerMessage }, prefixMessage) return [ { type: 'confirm', name: 'validateModuleType', message: `${innerMessage}Did you need to update ?`, }, ] as QuestionCollection } function shouldValidateModuleType(pluginConfig: ProjectPluginConfig) { const pluginModules = pluginConfig?.modules ?? [] return pluginModules.some((module) => 'newModuleType' in MODULE_TYPE[module.moduleType]) } function replaceDeprecatedModuleType(pluginConfig: ProjectPluginConfig) { const pluginModules = pluginConfig?.modules ?? [] const content = pluginModules.map((module) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const newModuleType = MODULE_TYPE[module.moduleType].newModuleType return newModuleType ? Object.assign({}, module, { moduleType: newModuleType, }) : module }) return Object.assign({}, pluginConfig, { modules: content, }) } function isInvokeCommandAllowTarget(target: string): target is InvokeTarget { return ALLOW_PROCESS_SET.has(target) || ALLOW_LIFECYCLE_SET.has(target) } function isLegalWebpackLogLevel(logging?: boolean | string): logging is StatsOptions['logging'] { if (isUndefined(logging) || isBoolean(logging)) return true const legalWebpackLogLevels = new Set([ 'errors-only', 'errors-warnings', 'minimal', 'none', 'normal', 'verbose', 'detailed', 'summary', ]) return legalWebpackLogLevels.has(logging) } function checkLegalMode(mode?: string) { if (isUndefined(mode) || isBoolean(mode)) return true const legalMode = new Set(['backend', 'frontend', 'all']) return legalMode.has(mode) } async function validateProjectCanExecuteInvokeCommand(currentWorkingDirectory = cwd()) { const nodeHostPath = await getNodeHostPath() const isProjectFilesValidated = await checkProjectFilesExists(currentWorkingDirectory, [ ...RELATIVE_FILE_PATH_FOR_VALIDATE, LOCAL_CONFIG_RELATIVE_PATH, PLUGIN_CONFIG_RELATIVE_PATH, nodeHostPath, ]) return isProjectFilesValidated } interface GetFrontEndDevServerParams { schemedOptions: InvokeCLISchemedOptions port?: string | number localConfig?: ProjectLocalConfig pluginConfig?: ProjectPluginConfig currentWorkingDirectory?: string } async function getFrontEndDevServer({ port, schemedOptions: { webpackStatsPreset }, localConfig, pluginConfig, currentWorkingDirectory, }: GetFrontEndDevServerParams) { currentWorkingDirectory = currentWorkingDirectory ?? cwd() const pluginConfigContent = pluginConfig || (await getPluginConfigContent()) const { local: { web_service_port: webServicePort, web_service_ip: webServiceIp }, } = localConfig || (await getLocalConfigContent(currentWorkingDirectory)) const pluginModules = pluginConfigContent?.modules ?? [] const defaultConfig = getDefaultWebpackConfig(pluginModules, currentWorkingDirectory, { getPublicPath() { return `http://${webServiceIp}:${webServicePort}/` }, appID: pluginConfigContent.service.app_id, }) const webpackConfig: Configuration = { ...defaultConfig, mode: 'development', stats: { preset: webpackStatsPreset, }, } const compiler = webpack(webpackConfig) const portNumber = port || webServicePort || DEFAULT_WEB_DEV_SERVER_PORT const webpackDevServerConfig = { allowedHosts: 'all', host: '0.0.0.0', port: portNumber, client: { progress: true, }, headers: { 'Access-Control-Allow-Origin': '*', }, } const frontEndDevServer = new webpackDevServer(webpackDevServerConfig, compiler) return frontEndDevServer } interface GetNodeHostParameterParams { schemedOptions: InvokeCLISchemedOptions target: InvokeTarget localConfig?: ProjectLocalConfig pluginConfig?: ProjectPluginConfig currentWorkingDirectory?: string } async function getNodeHostParameter({ schemedOptions: { reinstallPlugin }, target, localConfig, currentWorkingDirectory, }: GetNodeHostParameterParams) { const NODE_HOST_PATH = await getNodeHostPath() const nodeHostPath = join(currentWorkingDirectory ?? cwd(), NODE_HOST_PATH) const { platform: { username, password }, } = localConfig || (await getLocalConfigContent(currentWorkingDirectory)) const nodeHostParams = [ '--conf_path=config/local.yaml', '--tags=local', `--function=${target}`, `--email=${username}`, `--password=${password}`, `--reinstall=${reinstallPlugin}`, '--verbose', ] return { nodeHostPath, nodeHostParams, } } interface InvokeLifecycleTask { schemedOptions: InvokeCLISchemedOptions nodeHostParameter: { nodeHostPath: string nodeHostParams: string[] } frontEndDevServer: webpackDevServer localConfig: ProjectLocalConfig pluginConfig: ProjectPluginConfig } async function getInvokePrepTasks(target: InvokeTarget, options: InvokeCLIOptionsSchema) { const { webpackStatsPreset = logLevelToWebpackStatsPresetMap[currentLogLevel()], reinstallPlugin, mode, skipVerifyUserProfile, } = options const localConfig = await getLocalConfigContent() const pluginConfig = await getPluginConfigContent() if (shouldValidateModuleType(pluginConfig)) { const prompts = generateQuestionCollection<{ validateModuleType: boolean }>(pluginConfig) const answers = await inquirer.prompt(prompts) const validated = answers.validateModuleType if (validated) { const content = replaceDeprecatedModuleType(pluginConfig) await updatePluginConfigContent(content) } } const invokeLifecycleTasks: ListrTask[] = [ { title: 'Validating command options', task: async (ctx) => { const isLegalMode = checkLegalMode(mode) if (!isLegalMode) { const errorMessage = `Mode invalid, please execute 'npx op help invoke' to see the help information` throw new Error(errorMessage) } const isLegalLogLevel = isLegalWebpackLogLevel(webpackStatsPreset) if (!isLegalLogLevel) { const errorMessage = `Webpack logging level invalid, please execute 'npx op help invoke' to see the help information` throw new Error(errorMessage) } ctx.schemedOptions = { webpackStatsPreset, // node-host options processor implementation is bad for invoke command // temporary solution for reinstall flag reinstallPlugin: reinstallPlugin ? 'true' : 'false', } }, }, { title: 'Validating config/local.yaml fields', task: async (ctx) => { const { platform: { address, baseURL, username, password }, local: { organization_uuid: orgUUID, team_uuid: teamUUID }, } = localConfig const isMissingField = [address, baseURL, username, password, orgUUID, teamUUID].some(isNil) if (isMissingField) { const errorMessage = `Missing fields, 'config/local.yaml' needs to be configured correctly. ` + `please make sure you have executed 'npx op login' and 'npx op pickteam local' before, ` throw new Error(errorMessage) } if (!skipVerifyUserProfile) { const loginParams = { baseURL, password, ...convertUsernameField(username as string), } as LoginRequestParamsSchema const userProfile = await fetchUserProfile(loginParams) if (!userProfile) throw new Error('Failed to fetch user profile') } ctx.localConfig = localConfig }, }, getCheckAbilityRelateModuleTask(), getUpdateMinSystemVersionTask(), ] return invokeLifecycleTasks } function getInvokeFrontEndTasks(currentWorkingDirectory = cwd()) { const invokeLifecycleTasks: ListrTask[] = [ { title: 'validate module constraints', task: async (ctx) => { if (!ctx.pluginConfig) { ctx.pluginConfig = await getPluginConfigContent() } const [, errorMsgs] = await batchValidateModuleConstraints( getPluginModuleTypes(ctx.pluginConfig), { mode: 'build', pluginConfig: ctx.pluginConfig, }, ) if (errorMsgs) { throw new Error( 'Module constraints validation failed:\n\n' + errorMsgs + '\n\nPlease check config/plugin.yaml', ) } }, }, { title: 'Initializing the plugin project front-end development server', task: async (ctx) => { const pluginConfig = ctx.pluginConfig ?? (await getPluginConfigContent()) const frontEndDevServer = await getFrontEndDevServer({ schemedOptions: ctx.schemedOptions, localConfig: ctx.localConfig, pluginConfig, currentWorkingDirectory, }) ctx.frontEndDevServer = frontEndDevServer }, }, ] return invokeLifecycleTasks } function getInvokeBackendTasks(target: InvokeTarget) { const invokeLifecycleTasks: ListrTask[] = [ { title: 'validate ability constraints', task: async (ctx) => { if (!ctx.pluginConfig) { ctx.pluginConfig = await getPluginConfigContent() } const [, errorMsgs] = await batchValidateAbilityConstraints( getPluginAbilityTypes(ctx.pluginConfig), { mode: 'build', pluginConfig: ctx.pluginConfig, }, ) if (errorMsgs) { throw new Error( 'Ability constraints validation failed:\n\n' + errorMsgs + '\n\nPlease check config/plugin.yaml', ) } }, }, { title: 'check event', task: async (ctx) => { const pluginConfig = ctx.pluginConfig ?? (await getPluginConfigContent()) const { events = [] } = pluginConfig const eventTypes = events.map((event) => event.eventType) const existedEventTypeSet = new Set() for (const eventType of eventTypes) { if (existedEventTypeSet.has(eventType)) { throw new Error(`Duplicate event type: ${eventType}`) } existedEventTypeSet.add(eventType) } }, }, { title: 'Building plugin project Back-End dist files', task: () => buildPluginProjectBackEnd({ sourcemap: true }), }, { title: 'Initializing the node-host params', task: async (ctx) => { const pluginConfig = await getPluginConfigContent() const nodeHostParameter = await getNodeHostParameter({ target, localConfig: ctx.localConfig, pluginConfig, schemedOptions: ctx.schemedOptions, }) ctx.nodeHostParameter = nodeHostParameter }, }, ] return invokeLifecycleTasks } export { isInvokeCommandAllowTarget, validateProjectCanExecuteInvokeCommand, getFrontEndDevServer, getInvokePrepTasks, getInvokeFrontEndTasks, getInvokeBackendTasks, }