diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index e0a136103a..6e54bc5763 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -47,6 +47,7 @@ const CreationFlow: React.FC = () => { createNewBot, saveProjectAs, fetchProjectById, + createNewBotV2, } = useRecoilValue(dispatcherState); const templateProjects = useRecoilValue(filteredTemplatesSelector); @@ -127,7 +128,11 @@ const CreationFlow: React.FC = () => { alias: formData.alias, preserveRoot: formData.preserveRoot, }; - createNewBot(newBotData); + if (templateId === 'conversationalcore') { + createNewBotV2(newBotData); + } else { + createNewBot(newBotData); + } }; const handleSaveAs = (formData) => { diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index 6760acd45a..f783176cb5 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -4,6 +4,7 @@ import { atom, atomFamily } from 'recoil'; import { FormDialogSchemaTemplate, FeatureFlagMap, BotTemplate, UserSettings } from '@bfc/shared'; import { ExtensionMetadata } from '@bfc/extension-client'; +import formatMessage from 'format-message'; import { StorageFolder, @@ -212,6 +213,11 @@ export const botOpeningState = atom({ default: false, }); +export const botOpeningMessage = atom({ + key: getFullyQualifiedKey('botOpeningMessage'), + default: formatMessage('Loading'), +}); + export const formDialogLibraryTemplatesState = atom({ key: getFullyQualifiedKey('formDialogLibraryTemplates'), default: [], diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 5a0fa9fb56..976127147a 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -2,6 +2,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { BotProjectFile } from '@bfc/shared'; import formatMessage from 'format-message'; import findIndex from 'lodash/findIndex'; import { CallbackInterface, useRecoilCallback } from 'recoil'; @@ -17,7 +18,9 @@ import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; import { botErrorState, botNameIdentifierState, + botOpeningMessage, botOpeningState, + botProjectFileState, botProjectIdsState, botProjectSpaceLoadedState, botStatusState, @@ -32,6 +35,7 @@ import { logMessage, setError } from './../dispatchers/shared'; import { checkIfBotExistsInBotProjectFile, createNewBotFromTemplate, + createNewBotFromTemplateV2, fetchProjectDataById, flushExistingTasks, getSkillNameIdentifier, @@ -144,7 +148,7 @@ export const projectDispatcher = () => { const { set, snapshot } = callbackHelpers; const dispatcher = await snapshot.getPromise(dispatcherState); try { - const { templateId, name, description, location, schemaUrl, locale, qnaKbUrls } = newProjectData; + const { templateId, name, description, location, schemaUrl, locale } = newProjectData; set(botOpeningState, true); const { projectId, mainDialog } = await createNewBotFromTemplate( @@ -164,7 +168,7 @@ export const projectDispatcher = () => { }); set(botProjectIdsState, (current) => [...current, projectId]); await dispatcher.addLocalSkillToBotProjectFile(projectId); - navigateToBot(callbackHelpers, projectId, mainDialog, qnaKbUrls, templateId); + navigateToBot(callbackHelpers, projectId, mainDialog); return projectId; } catch (ex) { handleProjectFailure(callbackHelpers, ex); @@ -236,7 +240,6 @@ export const projectDispatcher = () => { location, schemaUrl, locale, - qnaKbUrls, templateDir, eTag, urlSuffix, @@ -264,7 +267,7 @@ export const projectDispatcher = () => { isRemote: false, }); projectIdCache.set(projectId); - navigateToBot(callbackHelpers, projectId, mainDialog, qnaKbUrls, templateId, urlSuffix); + navigateToBot(callbackHelpers, projectId, mainDialog, urlSuffix); } catch (ex) { set(botProjectIdsState, []); handleProjectFailure(callbackHelpers, ex); @@ -274,6 +277,49 @@ export const projectDispatcher = () => { } }); + const createNewBotV2 = useRecoilCallback((callbackHelpers: CallbackInterface) => async (newProjectData: any) => { + const { set, snapshot } = callbackHelpers; + try { + await flushExistingTasks(callbackHelpers); + const dispatcher = await snapshot.getPromise(dispatcherState); + set(botOpeningState, true); + const { + templateId, + name, + description, + location, + schemaUrl, + locale, + templateDir, + eTag, + urlSuffix, + alias, + preserveRoot, + } = newProjectData; + // starts the creation process and stores the jobID in state for tracking + const response = await createNewBotFromTemplateV2( + callbackHelpers, + templateId, + name, + description, + location, + schemaUrl, + locale, + templateDir, + eTag, + alias, + preserveRoot + ); + if (response.data.jobId) { + dispatcher.updateCreationMessage(response.data.jobId, templateId, urlSuffix); + } + } catch (ex) { + set(botProjectIdsState, []); + handleProjectFailure(callbackHelpers, ex); + navigateTo('/home'); + } + }); + const saveProjectAs = useRecoilCallback( (callbackHelpers: CallbackInterface) => async (oldProjectId, name, description, location) => { const { set } = callbackHelpers; @@ -365,7 +411,7 @@ export const projectDispatcher = () => { const reloadProject = async (callbackHelpers: CallbackInterface, response: any) => { callbackHelpers.reset(filePersistenceState(response.data.id)); - const { projectData, botFiles } = loadProjectData(response); + const { projectData, botFiles } = loadProjectData(response.data); await initBotState(callbackHelpers, projectData, botFiles); }; @@ -377,9 +423,59 @@ export const projectDispatcher = () => { await initBotState(callbackHelpers, projectData, botFiles); }); + const updateCreationMessage = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async (jobId: string, templateId: string, urlSuffix: string) => { + const timer = setInterval(async () => { + try { + const response = await httpClient.get(`/status/${jobId}`); + if (response.data?.httpStatusCode === 200 && response.data.result) { + // Bot creation successful + clearInterval(timer); + callbackHelpers.set(botOpeningMessage, response.data.latestMessage); + const { botFiles, projectData } = loadProjectData(response.data.result); + const projectId = response.data.result.id; + if (settingStorage.get(projectId)) { + settingStorage.remove(projectId); + } + const currentBotProjectFileIndexed: BotProjectFile = botFiles.botProjectSpaceFiles[0]; + callbackHelpers.set(botProjectFileState(projectId), currentBotProjectFileIndexed); + + const mainDialog = await initBotState(callbackHelpers, projectData, botFiles); + callbackHelpers.set(botProjectIdsState, [projectId]); + + // Post project creation + callbackHelpers.set(projectMetaDataState(projectId), { + isRootBot: true, + isRemote: false, + }); + projectIdCache.set(projectId); + navigateToBot(callbackHelpers, projectId, mainDialog, urlSuffix); + callbackHelpers.set(botOpeningMessage, ''); + callbackHelpers.set(botOpeningState, false); + } else { + if (response.data.httpStatusCode !== 500) { + // pending + callbackHelpers.set(botOpeningMessage, response.data.latestMessage); + } else { + // failure + callbackHelpers.set(botOpeningMessage, response.data.latestMessage); + clearInterval(timer); + } + } + } catch (err) { + clearInterval(timer); + callbackHelpers.set(botProjectIdsState, []); + handleProjectFailure(callbackHelpers, err); + navigateTo('/home'); + } + }, 5000); + } + ); + return { openProject, createNewBot, + createNewBotV2, deleteBot, saveProjectAs, fetchProjectById, @@ -395,5 +491,6 @@ export const projectDispatcher = () => { replaceSkillInBotProject, reloadProject, reloadExistingProject, + updateCreationMessage, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 5f765fd407..07a0dbe819 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -144,8 +144,6 @@ export const navigateToBot = ( callbackHelpers: CallbackInterface, projectId: string, mainDialog: string, - qnaKbUrls?: string[], - templateId?: string, urlSuffix?: string ) => { if (projectId) { @@ -161,8 +159,8 @@ export const navigateToBot = ( } }; -export const loadProjectData = (response) => { - const { files, botName, settings, skills: skillContent, id: projectId } = response.data; +export const loadProjectData = (data) => { + const { files, botName, settings, skills: skillContent, id: projectId } = data; const mergedSettings = getMergedSettings(projectId, settings); const storedLocale = languageStorage.get(botName)?.locale; const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage; @@ -174,7 +172,7 @@ export const loadProjectData = (response) => { return { botFiles: { ...indexedFiles, qnaFiles: updateQnAFiles, mergedSettings }, - projectData: response.data, + projectData: data, error: undefined, }; }; @@ -185,7 +183,7 @@ export const fetchProjectDataByPath = async ( ): Promise<{ botFiles: any; projectData: any; error: any }> => { try { const response = await httpClient.put(`/projects/open`, { path, storageId }); - const projectData = loadProjectData(response); + const projectData = loadProjectData(response.data); return projectData; } catch (ex) { return { @@ -199,7 +197,7 @@ export const fetchProjectDataByPath = async ( export const fetchProjectDataById = async (projectId): Promise<{ botFiles: any; projectData: any; error: any }> => { try { const response = await httpClient.get(`/projects/${projectId}`); - const projectData = loadProjectData(response); + const projectData = loadProjectData(response.data); return projectData; } catch (ex) { return { @@ -434,7 +432,7 @@ export const createNewBotFromTemplate = async ( alias, preserveRoot, }); - const { botFiles, projectData } = loadProjectData(response); + const { botFiles, projectData } = loadProjectData(response.data); const projectId = response.data.id; if (settingStorage.get(projectId)) { settingStorage.remove(projectId); @@ -451,6 +449,35 @@ export const createNewBotFromTemplate = async ( return { projectId, mainDialog }; }; +export const createNewBotFromTemplateV2 = async ( + callbackHelpers, + templateId: string, + name: string, + description: string, + location: string, + schemaUrl?: string, + locale?: string, + templateDir?: string, + eTag?: string, + alias?: string, + preserveRoot?: boolean +) => { + const jobId = await httpClient.post(`/v2/projects`, { + storageId: 'default', + templateId, + name, + description, + location, + schemaUrl, + locale, + templateDir, + eTag, + alias, + preserveRoot, + }); + return jobId; +}; + const addProjectToBotProjectSpace = (set, projectId: string, skillCt: number) => { let isBotProjectLoaded = false; set(botProjectIdsState, (current: string[]) => { @@ -559,7 +586,7 @@ export const saveProject = async (callbackHelpers, oldProjectData) => { description, location, }); - const data = loadProjectData(response); + const data = loadProjectData(response.data); if (data.error) { throw data.error; } diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx index fda81b41a4..366e78357f 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -12,7 +12,14 @@ import { resolveToBasePath } from './utils/fileUtil'; import { data } from './styles'; import { NotFound } from './components/NotFound'; import { BASEPATH } from './constants'; -import { dispatcherState, schemasState, botProjectIdsState, botOpeningState, pluginPagesSelector } from './recoilModel'; +import { + dispatcherState, + schemasState, + botProjectIdsState, + botOpeningState, + pluginPagesSelector, + botOpeningMessage, +} from './recoilModel'; import { openAlertModal } from './components/Modal/AlertDialog'; import { dialogStyle } from './components/Modal/dialogStyle'; import { LoadingSpinner } from './components/LoadingSpinner'; @@ -32,6 +39,7 @@ const FormDialogPage = React.lazy(() => import('./pages/form-dialog/FormDialogPa const Routes = (props) => { const botOpening = useRecoilValue(botOpeningState); const pluginPages = useRecoilValue(pluginPagesSelector); + const spinnerText = useRecoilValue(botOpeningMessage); return (
@@ -87,7 +95,7 @@ const Routes = (props) => {
- +
)}
diff --git a/Composer/packages/server/src/controllers/project.ts b/Composer/packages/server/src/controllers/project.ts index 9d7d083031..5234672c4d 100644 --- a/Composer/packages/server/src/controllers/project.ts +++ b/Composer/packages/server/src/controllers/project.ts @@ -1,13 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import * as fs from 'fs'; - import { Request, Response } from 'express'; import { Archiver } from 'archiver'; import { ExtensionContext } from '@bfc/extension'; -import { SchemaMerger } from '@microsoft/bf-dialog/lib/library/schemaMerger'; import { remove } from 'fs-extra'; +import formatMessage from 'format-message'; import log from '../logger'; import { BotProjectService } from '../services/project'; @@ -16,6 +14,8 @@ import { LocationRef } from '../models/bot/interface'; import { getSkillManifest } from '../models/bot/skillManager'; import StorageService from '../services/storage'; import settings from '../settings'; +import { ejectAndMerge, getLocationRef, getNewProjRef } from '../utility/project'; +import { BackgroundProcessManager } from '../services/backgroundProcessManager'; import { Path } from './../utility/path'; @@ -38,40 +38,15 @@ async function createProject(req: Request, res: Response) { templateId = 'EmptyBot'; } - // default the path to the default folder. - let path = settings.botsFolder; - // however, if path is specified as part of post body, use that one. - // this allows developer to specify a custom home for their bot. - if (location) { - // validate that this path exists - // prettier-ignore - if (fs.existsSync(location)) { // lgtm [js/path-injection] - path = location; - } - } - - const locationRef: LocationRef = { - storageId, - path: Path.resolve(path, name), - }; - - log('Attempting to create project at %s', path); + const locationRef = getLocationRef(location, storageId, name); try { // the template was downloaded remotely (via import) and will be used instead of an internal Composer template const createFromRemoteTemplate = !!templateDir; await BotProjectService.cleanProject(locationRef); - let newProjRef; - if (createFromRemoteTemplate) { - log('Creating project from remote template at %s', templateDir); - newProjRef = await AssetService.manager.copyRemoteProjectTemplateTo(templateDir, locationRef, user, locale); - // clean up the temporary template directory -- fire and forget - remove(templateDir); - } else { - log('Creating project from internal template %s', templateId); - newProjRef = await AssetService.manager.copyProjectTemplateTo(templateId, locationRef, user, locale); - } + const newProjRef = await getNewProjRef(templateDir, templateId, locationRef, user, locale); + const id = await BotProjectService.openProject(newProjRef, user); // in the case of a remote template, we need to update the eTag and alias used by the import mechanism createFromRemoteTemplate && BotProjectService.setProjectLocationData(id, { alias, eTag }); @@ -83,33 +58,6 @@ async function createProject(req: Request, res: Response) { } if (currentProject !== undefined) { - if (currentProject.settings?.runtime?.customRuntime === true) { - const runtime = ExtensionContext.getRuntimeByProject(currentProject); - const runtimePath = currentProject.settings.runtime.path; - - if (!fs.existsSync(runtimePath)) { - await runtime.eject(currentProject, currentProject.fileStorage); - } - - // install all dependencies and build the app - await runtime.build(runtimePath, currentProject); - - const manifestFile = runtime.identifyManifest(runtimePath); - - // run the merge command to merge all package dependencies from the template to the bot project - const realMerge = new SchemaMerger( - [manifestFile], - Path.join(currentProject.dataDir, 'schemas/sdk'), - Path.join(currentProject.dataDir, 'dialogs/imported'), - false, - false, - console.log, - console.warn, - console.error - ); - - await realMerge.merge(); - } await currentProject.updateBotInfo(name, description, preserveRoot); if (schemaUrl && !createFromRemoteTemplate) { await currentProject.saveSchemaToProject(schemaUrl, locationRef.path); @@ -555,6 +503,72 @@ async function copyTemplateToExistingProject(req: Request, res: Response) { } } +function createProjectV2(req: Request, res: Response) { + const jobId = BackgroundProcessManager.startProcess(202, 'create', 'Creating Bot Project'); + createProjectAsync(req, jobId); + res.status(202).json({ + jobId: jobId, + }); +} +async function createProjectAsync(req: Request, jobId: string) { + let { templateId } = req.body; + const { + name, + description, + storageId, + location, + schemaUrl, + locale, + preserveRoot, + templateDir, + eTag, + alias, + } = req.body; + const user = await ExtensionContext.getUserFromRequest(req); + if (templateId === '') { + templateId = 'EmptyBot'; + } + + const locationRef = getLocationRef(location, storageId, name); + try { + // the template was downloaded remotely (via import) and will be used instead of an internal Composer template + const createFromRemoteTemplate = !!templateDir; + + await BotProjectService.cleanProject(locationRef); + BackgroundProcessManager.updateProcess(jobId, 202, formatMessage('Getting template')); + const newProjRef = await getNewProjRef(templateDir, templateId, locationRef, user, locale); + + const id = await BotProjectService.openProject(newProjRef, user); + // in the case of a remote template, we need to update the eTag and alias used by the import mechanism + createFromRemoteTemplate && BotProjectService.setProjectLocationData(id, { alias, eTag }); + const currentProject = await BotProjectService.getProjectById(id, user); + + // inject shared content into every new project. this comes from assets/shared + if (!createFromRemoteTemplate) { + await AssetService.manager.copyBoilerplate(currentProject.dataDir, currentProject.fileStorage); + } + + if (currentProject !== undefined) { + await ejectAndMerge(currentProject, jobId); + BackgroundProcessManager.updateProcess(jobId, 202, formatMessage('Initializing bot project')); + await currentProject.updateBotInfo(name, description, preserveRoot); + if (schemaUrl && !createFromRemoteTemplate) { + await currentProject.saveSchemaToProject(schemaUrl, locationRef.path); + } + await currentProject.init(); + + const project = currentProject.getProject(); + log('Project created successfully.'); + BackgroundProcessManager.updateProcess(jobId, 200, 'Created Successfully', { + id, + ...project, + }); + } + } catch (err) { + BackgroundProcessManager.updateProcess(jobId, 500, err instanceof Error ? err.message : err); + } +} + export const ProjectController = { getProjectById, openProject, @@ -568,6 +582,7 @@ export const ProjectController = { exportProject, saveProjectAs, createProject, + createProjectV2, getAllProjects, getRecentProjects, updateBoilerplate, diff --git a/Composer/packages/server/src/controllers/status.ts b/Composer/packages/server/src/controllers/status.ts new file mode 100644 index 0000000000..a64a8f2d15 --- /dev/null +++ b/Composer/packages/server/src/controllers/status.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { BackgroundProcessManager } from '../services/backgroundProcessManager'; + +const getStatus = async (req, res) => { + try { + const jobId = req.params.jobId; + if (jobId) { + const result = BackgroundProcessManager.getProcessStatus(jobId); + if (result) { + res.status(result.httpStatusCode).json(result); + } else { + res.status(404).json({ + statusCode: '404', + message: 'JobId not found', + }); + } + } else { + res.status(400).json({ + statusCode: '400', + message: 'JobId not provided', + }); + } + } catch (err) { + res.status(500).json({ + statusCode: '500', + message: err.message, + }); + } +}; + +export const StatusController = { + getStatus, +}; diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index 8a3438d858..b5c2552e8c 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -1829,6 +1829,9 @@ "open_notification_panel_5796edb3": { "message": "Open notification panel" }, + "open_skills_page_for_configuration_details_a2a484ea": { + "message": "Open Skills page for configuration details" + }, "optional_221bcc9d": { "message": "Optional" }, @@ -1922,6 +1925,9 @@ "powervirtualagents_logo_11858924": { "message": "PowerVirtualAgents Logo" }, + "powervirtualagents_logo_11858924": { + "message": "PowerVirtualAgents Logo" + }, "press_enter_to_add_this_item_or_tab_to_move_to_the_6beb8a14": { "message": "press Enter to add this item or Tab to move to the next interactive element" }, @@ -2366,6 +2372,12 @@ "something_went_wrong_d238c551": { "message": "Something went wrong" }, + "something_happened_while_attempting_to_pull_e_952c7afe": { + "message": "Something happened while attempting to pull: { e }" + }, + "something_went_wrong_d238c551": { + "message": "Something went wrong" + }, "sorry_something_went_wrong_with_connecting_bot_run_7d6785e3": { "message": "Sorry, something went wrong with connecting bot runtime" }, diff --git a/Composer/packages/server/src/router/api.ts b/Composer/packages/server/src/router/api.ts index ef2166804f..03d2580a95 100644 --- a/Composer/packages/server/src/router/api.ts +++ b/Composer/packages/server/src/router/api.ts @@ -16,12 +16,14 @@ import { FeatureFlagController } from '../controllers/featureFlags'; import { AuthController } from '../controllers/auth'; import { csrfProtection } from '../middleware/csrfProtection'; import { ImportController } from '../controllers/import'; +import { StatusController } from '../controllers/status'; import { UtilitiesController } from './../controllers/utilities'; const router: Router = express.Router({}); router.post('/projects', ProjectController.createProject); +router.post('/v2/projects', ProjectController.createProjectV2); router.get('/projects', ProjectController.getAllProjects); router.get('/projects/recent', ProjectController.getRecentProjects); router.get('/projects/generateProjectId', ProjectController.generateProjectId); @@ -94,7 +96,7 @@ router.post('/extensions/proxy/:url', ExtensionsController.performExtensionFetch // authentication from client router.get('/auth/getAccessToken', csrfProtection, AuthController.getAccessToken); -//FeatureFlags +// FeatureFlags router.get('/featureFlags', FeatureFlagController.getFeatureFlags); router.post('/featureFlags', FeatureFlagController.updateFeatureFlags); @@ -102,6 +104,9 @@ router.post('/featureFlags', FeatureFlagController.updateFeatureFlags); router.post('/import/:source', ImportController.startImport); router.post('/import/:source/authenticate', ImportController.authenticate); +// Process status +router.get('/status/:jobId', StatusController.getStatus); + const errorHandler = (handler: RequestHandler) => (req: Request, res: Response, next: NextFunction) => { Promise.resolve(handler(req, res, next)).catch(next); }; diff --git a/Composer/packages/server/src/services/backgroundProcessManager.ts b/Composer/packages/server/src/services/backgroundProcessManager.ts new file mode 100644 index 0000000000..05adea2c96 --- /dev/null +++ b/Composer/packages/server/src/services/backgroundProcessManager.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { v4 as uuid } from 'uuid'; + +export interface ProcessStatus { + id: string; + startTime: Date; // contains start time + httpStatusCode: number; // contains http status code + latestMessage: string; // contains latest message + logs: string[]; // contains all messages + comment?: string; // contains user supplied comment about process + result?: any; // contains provision result +} + +type ProcessList = Record; + +export class BackgroundProcessManager { + static processes: ProcessList = {}; + + static startProcess(initialStatus: number, processName: string, initialMessage?: string, comment?: string): string { + const id = uuid(); + this.processes[id] = { + id: id, + startTime: new Date(), + httpStatusCode: initialStatus, + latestMessage: initialMessage || '', + logs: [initialMessage || ''], + comment: comment, + }; + return id; + } + + static getProcessStatus(id: string): ProcessStatus { + return this.processes[id]; + } + + static updateProcess(id: string, status: number, message: string, result?: any) { + if (this.processes[id]) { + this.processes[id].httpStatusCode = status; + this.processes[id].latestMessage = message; + this.processes[id].logs.push(message); + if (result) { + this.processes[id].result = result; + } + } + } + + static removeProcess(id: string) { + delete this.processes[id]; + } +} diff --git a/Composer/packages/server/src/utility/project.ts b/Composer/packages/server/src/utility/project.ts new file mode 100644 index 0000000000..46b8d0b1c4 --- /dev/null +++ b/Composer/packages/server/src/utility/project.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as fs from 'fs'; + +import { ExtensionContext, UserIdentity } from '@bfc/extension'; +import { remove } from 'fs-extra'; +import { SchemaMerger } from '@microsoft/bf-dialog/lib/library/schemaMerger'; +import formatMessage from 'format-message'; + +import { LocationRef } from '../models/bot/interface'; +import settings from '../settings'; +import log from '../logger'; +import AssetService from '../services/asset'; +import { BotProject } from '../models/bot/botProject'; +import { BackgroundProcessManager } from '../services/backgroundProcessManager'; + +import { Path } from './path'; + +export function getLocationRef(location: string, storageId: string, name: string) { + // default the path to the default folder. + let path = settings.botsFolder; + // however, if path is specified as part of post body, use that one. + // this allows developer to specify a custom home for their bot. + if (location) { + // validate that this path exists + // prettier-ignore + if (fs.existsSync(location)) { // lgtm [js/path-injection] + path = location; + } + } + const locationRef: LocationRef = { + storageId, + path: Path.resolve(path, name), + }; + log('Attempting to create project at %s', path); + return locationRef; +} + +export async function getNewProjRef( + templateDir: string, + templateId: string, + locationRef: LocationRef, + user?: UserIdentity, + locale?: string +) { + const createFromRemoteTemplate = !!templateDir; + let newProjRef; + if (createFromRemoteTemplate) { + log('Creating project from remote template at %s', templateDir); + newProjRef = await AssetService.manager.copyRemoteProjectTemplateTo(templateDir, locationRef, user, locale); + // clean up the temporary template directory -- fire and forget + remove(templateDir); + } else { + log('Creating project from internal template %s', templateId); + newProjRef = await AssetService.manager.copyProjectTemplateTo(templateId, locationRef, user, locale); + } + return newProjRef; +} + +export async function ejectAndMerge(currentProject: BotProject, jobId: string) { + if (currentProject.settings?.runtime?.customRuntime === true) { + const runtime = ExtensionContext.getRuntimeByProject(currentProject); + const runtimePath = currentProject.settings.runtime.path; + + if (!fs.existsSync(runtimePath)) { + await runtime.eject(currentProject, currentProject.fileStorage); + } + + // install all dependencies and build the app + BackgroundProcessManager.updateProcess(jobId, 202, formatMessage('Building runtime')); + await runtime.build(runtimePath, currentProject); + + const manifestFile = runtime.identifyManifest(runtimePath); + + // run the merge command to merge all package dependencies from the template to the bot project + BackgroundProcessManager.updateProcess(jobId, 202, formatMessage('Merging Packages')); + const realMerge = new SchemaMerger( + [manifestFile], + Path.join(currentProject.dataDir, 'schemas/sdk'), + Path.join(currentProject.dataDir, 'dialogs/imported'), + false, + false, + console.log, + console.warn, + console.error + ); + + await realMerge.merge(); + } +} diff --git a/extensions/azurePublish/yarn.lock b/extensions/azurePublish/yarn.lock index 5194f0edf6..520c14dbc1 100644 --- a/extensions/azurePublish/yarn.lock +++ b/extensions/azurePublish/yarn.lock @@ -1231,6 +1231,9 @@ buffer@^5.1.0, buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" caniuse-lite@^1.0.30001154: version "1.0.30001156" diff --git a/extensions/localPublish/yarn.lock b/extensions/localPublish/yarn.lock index d751d391e7..55c66f5e0f 100644 --- a/extensions/localPublish/yarn.lock +++ b/extensions/localPublish/yarn.lock @@ -230,6 +230,9 @@ buffer@^5.1.0, buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" chalk@^2.3.0: version "2.4.2" diff --git a/extensions/vacore/yarn.lock b/extensions/vacore/yarn.lock index 3afe4a4744..68a9697fba 100644 --- a/extensions/vacore/yarn.lock +++ b/extensions/vacore/yarn.lock @@ -372,6 +372,9 @@ buffer@^5.1.0, buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" caniuse-lite@^1.0.30001154: version "1.0.30001156"