diff --git a/.vscode/launch.json b/.vscode/launch.json index ec120d4404..4a02a43460 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -84,7 +84,7 @@ "NODE_ENV": "development", "DEBUG": "composer*", "COMPOSER_DEV_TOOLS": "true", - "COMPOSER_ENABLE_ONEAUTH": "false" + "COMPOSER_ENABLE_ONEAUTH": "true" }, "outputCapture": "std", "preLaunchTask": "electron: build", diff --git a/Composer/cypress/integration/CreateNewBot.spec.ts b/Composer/cypress/integration/CreateNewBot.spec.ts index 60822b3708..1ad5e991aa 100644 --- a/Composer/cypress/integration/CreateNewBot.spec.ts +++ b/Composer/cypress/integration/CreateNewBot.spec.ts @@ -11,25 +11,15 @@ context('Creating a new bot', () => { }); it('can create a new bot', () => { - cy.findByTestId('Create from scratch').click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(3000); + cy.findByTestId('@microsoft/generator-bot-empty').click(); cy.findByTestId('NextStepButton').click(); - cy.enterTextAndSubmit('NewDialogName', '__TestNewProject', 'SubmitNewBotBtn'); + cy.enterTextAndSubmit('NewDialogName', 'TestNewProject3', 'SubmitNewBotBtn'); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(150000); cy.findByTestId('ProjectTree').within(() => { - cy.findAllByText('__TestNewProject').should('exist'); - }); - }); - - it('can create a bot from the ToDo template', () => { - cy.findByTestId('Create from template').click(); - cy.findByTestId('TodoSample').click({ force: true }); - cy.findByTestId('NextStepButton').click(); - cy.enterTextAndSubmit('NewDialogName', '__TestNewProject2', 'SubmitNewBotBtn'); - cy.findByTestId('ProjectTree').within(() => { - cy.findAllByText('__TestNewProject2').should('exist'); - cy.findByText('addtodo').should('exist'); - cy.findByText('cleartodos').should('exist'); - cy.findByText('deletetodo').should('exist'); - cy.findByText('showtodos').should('exist'); + cy.findAllByText('TestNewProject3').should('exist'); }); }); }); diff --git a/Composer/cypress/integration/HomePage.spec.ts b/Composer/cypress/integration/HomePage.spec.ts index 9053d21263..debb821d1e 100644 --- a/Composer/cypress/integration/HomePage.spec.ts +++ b/Composer/cypress/integration/HomePage.spec.ts @@ -9,7 +9,6 @@ context('Home Page ', () => { it('can open buttons in home page', () => { cy.findByTestId('LeftNav-CommandBarButtonHome').click(); cy.findByTestId('homePage-Toolbar-New').click(); - cy.findByText('Create bot from template or scratch?').should('exist'); cy.findByText('Cancel').should('exist'); cy.findByText('Cancel').click(); cy.findByTestId('homePage-Toolbar-Open').click(); @@ -17,6 +16,5 @@ context('Home Page ', () => { cy.findByText('Cancel').should('exist'); cy.findByText('Cancel').click(); cy.findByTestId('homePage-body-New').click(); - cy.findByText('Create bot from template or scratch?').should('exist'); }); }); diff --git a/Composer/cypress/integration/NavigateUrls.ts b/Composer/cypress/integration/NavigateUrls.spec.ts similarity index 72% rename from Composer/cypress/integration/NavigateUrls.ts rename to Composer/cypress/integration/NavigateUrls.spec.ts index 113869d331..99bb4916fd 100644 --- a/Composer/cypress/integration/NavigateUrls.ts +++ b/Composer/cypress/integration/NavigateUrls.spec.ts @@ -6,10 +6,9 @@ context('Navigate Url', () => { cy.visit('/home'); }); - it('should open Create From Scratch/Template window from a url', () => { + it('should open Select Template window from a url', () => { cy.visit('/projects/create'); - cy.get('[data-testid="Create from scratch"]').should('be.visible'); - cy.get('[data-testid="Create from template"]').should('be.visible'); + cy.findAllByTestId('dotnetFeed').should('exist'); }); it('should open Define Conversations window from a url', () => { diff --git a/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx b/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx index 0c7b4075c1..cc739071a2 100644 --- a/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx +++ b/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx @@ -27,6 +27,7 @@ describe('', () => { onboardingAddCoachMarkRef: jest.fn(), fetchRecentProjects: jest.fn(), fetchTemplates: jest.fn(), + fetchTemplatesV2: jest.fn(), setCreationFlowStatus: jest.fn(), navTo: jest.fn(), saveTemplateId: jest.fn(), diff --git a/Composer/packages/client/__tests__/components/CreationFlowV2/CreateOptions/index.test.tsx b/Composer/packages/client/__tests__/components/CreationFlowV2/CreateOptions/index.test.tsx index a295488469..3cc2d52b16 100644 --- a/Composer/packages/client/__tests__/components/CreationFlowV2/CreateOptions/index.test.tsx +++ b/Composer/packages/client/__tests__/components/CreationFlowV2/CreateOptions/index.test.tsx @@ -20,6 +20,8 @@ describe('', () => { id: 'generator-conversational-core', index: 0, name: 'conversational-core', + dotnetSupport: { webAppSupported: true, functionsSupported: true }, + nodeSupport: { webAppSupported: true, functionsSupported: true }, package: { packageName: 'generator-conversational-core', packageSource: 'npm', @@ -44,10 +46,10 @@ describe('', () => { it('should save conversational core template id', async () => { const component = renderComponent(); - const conversationalCoreBot = await component.findByText('conversational-core'); + const conversationalCoreBot = await component.findByTestId('generator-conversational-core'); fireEvent.click(conversationalCoreBot); const nextButton = await component.findByText('Next'); fireEvent.click(nextButton); - expect(handleCreateNextMock).toBeCalledWith('generator-conversational-core'); + expect(handleCreateNextMock).toBeCalledWith('generator-conversational-core', 'dotnet'); }); }); diff --git a/Composer/packages/client/__tests__/components/CreationFlowV2/index.test.tsx b/Composer/packages/client/__tests__/components/CreationFlowV2/index.test.tsx index 43855c4f1a..11cd4f766c 100644 --- a/Composer/packages/client/__tests__/components/CreationFlowV2/index.test.tsx +++ b/Composer/packages/client/__tests__/components/CreationFlowV2/index.test.tsx @@ -27,6 +27,7 @@ describe('', () => { onboardingAddCoachMarkRef: jest.fn(), fetchRecentProjects: jest.fn(), fetchTemplates: jest.fn(), + fetchTemplatesV2: jest.fn(), setCreationFlowStatus: jest.fn(), navTo: jest.fn(), saveTemplateId: jest.fn(), @@ -71,7 +72,7 @@ describe('', () => { ); - navigate('create/generator-conversational-core'); + navigate('create/dotnet/generator-conversational-core'); const node = await findByText('OK'); act(() => { @@ -94,8 +95,13 @@ describe('', () => { eTag: undefined, preserveRoot: undefined, qnqKbUrls: undefined, + runtimeType: 'webapp', templateDir: undefined, urlSuffix: undefined, + profile: undefined, + qnaKbUrls: undefined, + runtimeLanguage: 'dotnet', + source: undefined, }); }); }); diff --git a/Composer/packages/client/src/components/CreationFlow/CreateOptions.tsx b/Composer/packages/client/src/components/CreationFlow/CreateOptions.tsx index d90b5533bc..d9a3f0c1d1 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreateOptions.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreateOptions.tsx @@ -11,12 +11,10 @@ import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { BotTemplate } from '@bfc/shared'; import { DialogWrapper, DialogTypes } from '@bfc/ui-shared'; import { RouteComponentProps, navigate } from '@reach/router'; -import { useRecoilValue } from 'recoil'; import querystring from 'query-string'; import axios from 'axios'; import { DialogCreationCopy } from '../../constants'; -import { featureFlagsState } from '../../recoilModel'; import { getAliasFromPayload } from '../../utils/electronUtil'; import { CreateBot } from './CreateBot'; @@ -34,11 +32,8 @@ export function CreateOptions(props: CreateOptionsProps) { const [option, setOption] = useState('Create'); const [isOpenCreateModal, setIsOpenCreateModal] = useState(false); const { templates, onDismiss, onNext, onJumpToOpenModal } = props; - const featureFlags = useRecoilValue(featureFlagsState); useEffect(() => { - if (featureFlags.NEW_CREATION_FLOW?.enabled) { - navigate(`/v2/projects/create${props?.location?.search}`); - } + navigate(`/v2/projects/create${props?.location?.search}`); }); useEffect(() => { diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index 082865f9e4..8e0248d60d 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -7,9 +7,8 @@ import Path from 'path'; import React, { useEffect, useRef, Fragment } from 'react'; import { RouteComponentProps, Router, navigate } from '@reach/router'; import { useRecoilValue } from 'recoil'; -import { csharpFeedKey } from '@bfc/shared'; -import { CreationFlowStatus, feedDictionary } from '../../constants'; +import { CreationFlowStatus, firstPartyTemplateFeed } from '../../constants'; import { dispatcherState, creationFlowStatusState, @@ -17,7 +16,6 @@ import { focusedStorageFolderState, currentProjectIdState, userSettingsState, - featureFlagsState, templateProjectsState, } from '../../recoilModel'; import Home from '../../pages/home/Home'; @@ -33,7 +31,6 @@ type CreationFlowProps = RouteComponentProps<{}>; const CreationFlow: React.FC = (props: CreationFlowProps) => { const { - fetchTemplates, fetchTemplatesV2, fetchRecentProjects, fetchStorages, @@ -51,7 +48,6 @@ const CreationFlow: React.FC = (props: CreationFlowProps) => } = useRecoilValue(dispatcherState); const templateProjects = useRecoilValue(templateProjectsState); - const featureFlags = useRecoilValue(featureFlagsState); const creationFlowStatus = useRecoilValue(creationFlowStatusState); const projectId = useRecoilValue(currentProjectIdState); const storages = useRecoilValue(storagesState); @@ -78,7 +74,7 @@ const CreationFlow: React.FC = (props: CreationFlowProps) => } await fetchStorages(); fetchRecentProjects(); - featureFlags.NEW_CREATION_FLOW?.enabled ? fetchTemplatesV2([feedDictionary[csharpFeedKey]]) : fetchTemplates(); + fetchTemplatesV2([firstPartyTemplateFeed]); }; useEffect(() => { diff --git a/Composer/packages/client/src/components/CreationFlow/v2/CreateBot.tsx b/Composer/packages/client/src/components/CreationFlow/v2/CreateBot.tsx index 73d3595317..096011541b 100644 --- a/Composer/packages/client/src/components/CreationFlow/v2/CreateBot.tsx +++ b/Composer/packages/client/src/components/CreationFlow/v2/CreateBot.tsx @@ -4,7 +4,6 @@ /** @jsx jsx */ import { jsx, css } from '@emotion/core'; import { useState, Fragment, useEffect, useMemo } from 'react'; -import find from 'lodash/find'; import formatMessage from 'format-message'; import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; @@ -17,18 +16,18 @@ import { CheckboxVisibility, DetailsRow, } from 'office-ui-fabric-react/lib/DetailsList'; -import { BotTemplate, QnABotTemplateId } from '@bfc/shared'; +import { BotTemplate } from '@bfc/shared'; import { DialogWrapper, DialogTypes, LoadingSpinner } from '@bfc/ui-shared'; import { NeutralColors } from '@uifabric/fluent-theme'; import { WindowLocation } from '@reach/router'; import { IPivotItemProps, Pivot, PivotItem } from 'office-ui-fabric-react/lib/Pivot'; import { Link } from 'office-ui-fabric-react/lib/Link'; import { FontIcon } from 'office-ui-fabric-react/lib/Icon'; -import { csharpFeedKey } from '@botframework-composer/types'; +import { csharpFeedKey, nodeFeedKey } from '@botframework-composer/types'; import { useRecoilState, useRecoilValue } from 'recoil'; import msftIcon from '../../../images/msftIcon.svg'; -import { DialogCreationCopy, feedDictionary } from '../../../constants'; +import { DialogCreationCopy } from '../../../constants'; import { creationFlowTypeState, fetchReadMePendingState, selectedTemplateReadMeState } from '../../../recoilModel'; import TelemetryClient from '../../../telemetry/TelemetryClient'; @@ -112,15 +111,13 @@ const optionKeys = { const templateRequestUrl = 'https://github.com/microsoft/botframework-components/issues/new?assignees=&labels=needs-triage%2C+feature-request&template=-net-sdk-feature-request.md&title=[NewTemplateRequest]'; -const defaultTemplateId = '@microsoft/generator-microsoft-bot-empty'; - // -------------------- CreateOptions -------------------- // type CreateBotProps = { isOpen: boolean; templates: BotTemplate[]; location?: WindowLocation | undefined; onDismiss: () => void; - onNext: (templateName: string, urlData?: string) => void; + onNext: (templateName: string, templateLanguage: string, urlData?: string) => void; fetchTemplates: (feedUrls?: string[]) => Promise; fetchReadMe: (moduleName: string) => {}; }; @@ -129,9 +126,11 @@ export function CreateBotV2(props: CreateBotProps) { const [option] = useState(optionKeys.createFromTemplate); const [disabled] = useState(false); const { isOpen, templates, onDismiss, onNext } = props; - const [currentTemplateId, setCurrentTemplateId] = useState(defaultTemplateId); - const [emptyBotKey, setEmptyBotKey] = useState(''); - const [selectedFeed, setSelectedFeed] = useState<{ props: IPivotItemProps }>({ props: { itemKey: csharpFeedKey } }); + const [currentTemplateId, setCurrentTemplateId] = useState(''); + const [selectedProgLang, setSelectedProgLang] = useState<{ props: IPivotItemProps }>({ + props: { itemKey: csharpFeedKey }, + }); + const [displayedTemplates, setDisplayedTemplates] = useState([]); const [readMe] = useRecoilState(selectedTemplateReadMeState); const fetchReadMePending = useRecoilValue(fetchReadMePendingState); const creationFlowType = useRecoilValue(creationFlowTypeState); @@ -140,6 +139,7 @@ export function CreateBotV2(props: CreateBotProps) { return new Selection({ onSelectionChanged: () => { const t = selectedTemplate.getSelection()[0] as BotTemplate; + if (t) { setCurrentTemplateId(t.id); } @@ -148,21 +148,13 @@ export function CreateBotV2(props: CreateBotProps) { }, []); const handleJumpToNext = () => { - let routeToTemplate = emptyBotKey; - if (option === optionKeys.createFromTemplate) { - routeToTemplate = currentTemplateId; - } - - if (option === optionKeys.createFromQnA) { - routeToTemplate = QnABotTemplateId; - } - - TelemetryClient.track('CreateNewBotProjectNextButton', { template: routeToTemplate }); + TelemetryClient.track('CreateNewBotProjectNextButton', { template: currentTemplateId }); + const runtimeLanguage = selectedProgLang?.props?.itemKey ?? csharpFeedKey; if (location?.search) { - onNext(routeToTemplate, location.search); + onNext(currentTemplateId, runtimeLanguage, location.search); } else { - onNext(routeToTemplate); + onNext(currentTemplateId, runtimeLanguage); } }; @@ -200,27 +192,32 @@ export function CreateBotV2(props: CreateBotProps) { }; const getTemplate = (): BotTemplate | undefined => { - const currentTemplate = templates.find((t) => { - return t.id === currentTemplateId; + const currentTemplate = displayedTemplates.find((t) => { + return t?.id === currentTemplateId; }); return currentTemplate; }; useEffect(() => { - if (templates.length > 1) { - const emptyBotTemplate = find(templates, ['id', defaultTemplateId]); - if (emptyBotTemplate) { - setCurrentTemplateId(emptyBotTemplate.id); - setEmptyBotKey(emptyBotTemplate.id); - } + const itemKey = selectedProgLang.props.itemKey; + if (itemKey === csharpFeedKey) { + const newTemplates = templates.filter((template) => { + return template.dotnetSupport; + }); + setDisplayedTemplates(newTemplates); + } else if (itemKey === nodeFeedKey) { + const newTemplates = templates.filter((template) => { + return template.nodeSupport; + }); + setDisplayedTemplates(newTemplates); } - }, [templates]); + }, [templates, selectedProgLang]); useEffect(() => { - if (selectedFeed?.props?.itemKey) { - props.fetchTemplates([feedDictionary[selectedFeed.props.itemKey]]); + if (displayedTemplates?.[0]?.id) { + setCurrentTemplateId(displayedTemplates[0].id); } - }, [selectedFeed]); + }, [displayedTemplates]); useEffect(() => { if (currentTemplateId) { @@ -238,11 +235,12 @@ export function CreateBotV2(props: CreateBotProps) { defaultSelectedKey={csharpFeedKey} onLinkClick={(item) => { if (item) { - setSelectedFeed(item); + setSelectedProgLang(item); } }} > - + +
@@ -256,7 +254,7 @@ export function CreateBotV2(props: CreateBotProps) { compact={false} getKey={(item) => item.name} isHeaderVisible={false} - items={templates} + items={displayedTemplates} layoutMode={DetailsListLayoutMode.justified} selection={selectedTemplate} selectionMode={disabled ? SelectionMode.none : SelectionMode.single} diff --git a/Composer/packages/client/src/components/CreationFlow/v2/CreateOptions.tsx b/Composer/packages/client/src/components/CreationFlow/v2/CreateOptions.tsx index 72b8c99f2e..9fb03adb6a 100644 --- a/Composer/packages/client/src/components/CreationFlow/v2/CreateOptions.tsx +++ b/Composer/packages/client/src/components/CreationFlow/v2/CreateOptions.tsx @@ -23,7 +23,7 @@ import { CreateBotV2 } from './CreateBot'; type CreateOptionsProps = { templates: BotTemplate[]; onDismiss: () => void; - onNext: (data: string) => void; + onNext: (templateName: string, templateLanguage: string, urlData?: string) => void; onJumpToOpenModal: (search?: string) => void; fetchTemplates: (feedUrls?: string[]) => Promise; fetchReadMe: (moduleName: string) => {}; diff --git a/Composer/packages/client/src/components/CreationFlow/v2/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/v2/CreationFlow.tsx index 5f5f9f31a6..64b4ec01e0 100644 --- a/Composer/packages/client/src/components/CreationFlow/v2/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/v2/CreationFlow.tsx @@ -8,7 +8,7 @@ import { RouteComponentProps, Router, navigate } from '@reach/router'; import { useRecoilValue } from 'recoil'; import { BotTemplate } from '@bfc/shared'; -import { CreationFlowStatus } from '../../../constants'; +import { CreationFlowStatus, firstPartyTemplateFeed } from '../../../constants'; import { dispatcherState, creationFlowStatusState, @@ -75,6 +75,7 @@ const CreationFlowV2: React.FC = () => { await fetchProjectById(cachedProjectId); } await fetchStorages(); + await fetchTemplatesV2([firstPartyTemplateFeed]); fetchRecentProjects(); }; @@ -127,6 +128,8 @@ const CreationFlowV2: React.FC = () => { description: formData.description, location: formData.location, schemaUrl: formData.schemaUrl, + runtimeType: formData.runtimeType, + runtimeLanguage: formData.runtimeLanguage, appLocale, qnaKbUrls, templateDir: formData?.pvaData?.templateDir, @@ -159,11 +162,11 @@ const CreationFlowV2: React.FC = () => { } }; - const handleCreateNext = async (templateName: string, urlData?: string) => { + const handleCreateNext = async (templateName: string, runtimeLanguage: string, urlData?: string) => { setCreationFlowStatus(CreationFlowStatus.NEW_FROM_TEMPLATE); const navString = urlData - ? `./create/${encodeURIComponent(templateName)}${urlData}` - : `./create/${encodeURIComponent(templateName)}`; + ? `./create/${runtimeLanguage}/${encodeURIComponent(templateName)}${urlData}` + : `./create/${runtimeLanguage}/${encodeURIComponent(templateName)}`; navigate(navString); }; @@ -174,7 +177,7 @@ const CreationFlowV2: React.FC = () => { ; @@ -111,13 +114,15 @@ const DefineConversationV2: React.FC = (props) => { } = props; const files = focusedStorageFolder?.children ?? []; const writable = focusedStorageFolder.writable; + const runtimeLanguage = props.runtimeLanguage ? props.runtimeLanguage : csharpFeedKey; + const templateProjects = useRecoilValue(templateProjectsState); // template ID is populated by npm package name which needs to be formatted const normalizeTemplateId = (templateId?: string) => { if (templateId) { // use almost the same patterns as in assetManager.ts return templateId - .replace(/^@microsoft\/generator-microsoft-bot-/, '') // clean up our complex package names + .replace(/^@microsoft\/generator-bot-/, '') // clean up our complex package names .replace(/^generator-/, '') // clean up other package names too .trim() .replace(/-/, '_') @@ -171,10 +176,10 @@ const DefineConversationV2: React.FC = (props) => { description: { required: false, }, - primaryLanguage: { + runtimeLanguage: { required: false, }, - runtimeChoice: { + runtimeType: { required: false, }, schemaUrl: { @@ -201,8 +206,8 @@ const DefineConversationV2: React.FC = (props) => { useEffect(() => { const formData: DefineConversationFormData = { name: getDefaultName(), - primaryLanguage: defaultPrimaryLanguage, - runtimeChoice: defaultRuntime, + runtimeLanguage: runtimeLanguage, + runtimeType: webAppRuntimeKey, description: '', schemaUrl: '', location: @@ -278,7 +283,6 @@ const DefineConversationV2: React.FC = (props) => { dataToSubmit.alias = await getAliasFromPayload(source, payload); } } - onSubmit({ ...dataToSubmit }, templateId || ''); }, [hasErrors, formData] @@ -289,6 +293,30 @@ const DefineConversationV2: React.FC = (props) => { updateField('location', newPath); }; + const getSupportedRuntimesForTemplate = (): IDropdownOption[] => { + const result: IDropdownOption[] = []; + const currentTemplate = templateProjects.find((t) => { + if (t?.id) { + return t.id === templateId; + } + }); + + if (currentTemplate) { + if (runtimeLanguage === csharpFeedKey) { + currentTemplate.dotnetSupport?.functionsSupported && + result.push({ key: functionsRuntimeKey, text: formatMessage('Azure Functions') }); + currentTemplate.dotnetSupport?.webAppSupported && + result.push({ key: webAppRuntimeKey, text: formatMessage('Azure Web App') }); + } else if (runtimeLanguage === nodeFeedKey) { + currentTemplate.nodeSupport?.functionsSupported && + result.push({ key: functionsRuntimeKey, text: formatMessage('Azure Functions') }); + currentTemplate.nodeSupport?.webAppSupported && + result.push({ key: webAppRuntimeKey, text: formatMessage('Azure Web App') }); + } + } + return result; + }; + useEffect(() => { const location = focusedStorageFolder !== null && Object.keys(focusedStorageFolder as Record).length @@ -311,6 +339,7 @@ const DefineConversationV2: React.FC = (props) => { const dialogCopy = isImported ? DialogCreationCopy.IMPORT_BOT_PROJECT : DialogCreationCopy.DEFINE_BOT_PROJECT; return ( + // TODO remove runtime language drop down prior to merging as that data is indicated by the tab chosen
@@ -339,6 +368,17 @@ const DefineConversationV2: React.FC = (props) => { /> + + + updateField('runtimeType', option?.key.toString())} + /> + + {locationSelectContent} diff --git a/Composer/packages/client/src/constants.ts b/Composer/packages/client/src/constants.ts index 8e9084c98f..16226e7ed8 100644 --- a/Composer/packages/client/src/constants.ts +++ b/Composer/packages/client/src/constants.ts @@ -1,7 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { FeedName, TeamsManifest } from '@botframework-composer/types/src'; +import { + webAppRuntimeKey, + functionsRuntimeKey, + csharpFeedKey, + nodeFeedKey, + TeamsManifest, +} from '@botframework-composer/types'; import formatMessage from 'format-message'; import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; @@ -399,25 +405,22 @@ export const triggerNotSupportedWarning = () => 'This trigger type is not supported by the RegEx recognizer. To ensure this trigger is fired, change the recognizer type.' ); -export const feedDictionary: { [key in FeedName]: string } = { - firstPartyCsharp: - 'https://registry.npmjs.org/-/v1/search?text=conversationalcore+scope:microsoft&size=100&from=0&quality=0.65&popularity=0.98&maintenance=0.5', - firstPartyNode: '', -}; +export const firstPartyTemplateFeed = + 'https://registry.npmjs.org/-/v1/search?text=generator+keywords:bf-template+scope:microsoft'; // +maintainer:botframework // TODO: replace language options with available languages pertinent to the selected template (issue #5554) export const defaultPrimaryLanguage = 'english'; -export const mockLanguageOptions: IDropdownOption[] = [ - { key: defaultPrimaryLanguage, text: 'English' }, - { key: 'spanish', text: 'Spanish' }, +export const runtimeLanguageOptions: IDropdownOption[] = [ + { key: nodeFeedKey, text: 'Node' }, + { key: csharpFeedKey, text: 'Dot Net' }, ]; export const defaultRuntime = 'azureWebApp'; export const runtimeOptions: IDropdownOption[] = [ - { key: defaultRuntime, text: 'Azure Web App' }, - { key: 'azureFunctions', text: 'Azure Functions' }, + { key: webAppRuntimeKey, text: 'Azure Web App' }, + { key: functionsRuntimeKey, text: 'Azure Functions' }, ]; export const onboardingDisabled = false; diff --git a/Composer/packages/client/src/pages/botProject/BotProjectSettingsTableView.tsx b/Composer/packages/client/src/pages/botProject/BotProjectSettingsTableView.tsx index 80d0a687e0..81cfcd3976 100644 --- a/Composer/packages/client/src/pages/botProject/BotProjectSettingsTableView.tsx +++ b/Composer/packages/client/src/pages/botProject/BotProjectSettingsTableView.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { RouteComponentProps } from '@reach/router'; import { localBotsDataSelector } from '../../recoilModel/selectors/project'; -import { useFeatureFlag } from '../../utils/hooks'; import { BotProjectInfo } from './BotProjectInfo'; import { SkillHostEndPoint } from './SkillHostEndPoint'; @@ -43,7 +42,6 @@ export const BotProjectSettingsTableView: React.FC b.projectId === projectId); const isRootBot = !!botProject?.isRootBot; - const useAdapters = useFeatureFlag('NEW_CREATION_FLOW'); return (
@@ -52,7 +50,7 @@ export const BotProjectSettingsTableView: React.FC - {isRootBot && useAdapters && } + {isRootBot && }
diff --git a/Composer/packages/client/src/pages/botProject/BotProjectsSettingsTabView.tsx b/Composer/packages/client/src/pages/botProject/BotProjectsSettingsTabView.tsx index 451d11cc28..cc56fbc9fc 100644 --- a/Composer/packages/client/src/pages/botProject/BotProjectsSettingsTabView.tsx +++ b/Composer/packages/client/src/pages/botProject/BotProjectsSettingsTabView.tsx @@ -10,7 +10,6 @@ import { Pivot, PivotItem } from 'office-ui-fabric-react/lib/components/Pivot'; import formatMessage from 'format-message'; import { localBotsDataSelector } from '../../recoilModel/selectors/project'; -import { useFeatureFlag } from '../../utils/hooks'; import { SkillHostEndPoint } from './SkillHostEndPoint'; import { BotProjectInfo } from './BotProjectInfo'; @@ -60,7 +59,6 @@ export const BotProjectSettingsTabView: React.FC b.projectId === projectId); const isRootBot = !!botProject?.isRootBot; - const useAdapters = useFeatureFlag('NEW_CREATION_FLOW'); const [selectedKey, setSelectedKey] = useState(PivotItemKey.Basics); useEffect(() => { @@ -97,7 +95,7 @@ export const BotProjectSettingsTabView: React.FC
- {isRootBot && useAdapters && } + {isRootBot && }
void; @@ -42,7 +39,6 @@ export const CreationModal: React.FC = (props) => { createNewBot, createNewBotV2, openProject, - addNewSkillToBotProject, addExistingSkillToBotProject, fetchTemplatesV2, fetchReadMe, @@ -51,7 +47,6 @@ export const CreationModal: React.FC = (props) => { const templateProjects = useRecoilValue(templateProjectsState); const creationFlowStatus = useRecoilValue(creationFlowStatusState); const creationFlowType = useRecoilValue(creationFlowTypeState); - const featureFlags = useRecoilValue(featureFlagsState); const focusedStorageFolder = useRecoilValue(focusedStorageFolderState); const { appLocale } = useRecoilValue(userSettingsState); const storages = useRecoilValue(storagesState); @@ -98,30 +93,26 @@ export const CreationModal: React.FC = (props) => { appLocale, }; if (creationFlowType === 'Skill') { - if (featureFlags?.NEW_CREATION_FLOW?.enabled) { - const templateVersion = templateProjects.find((template: BotTemplate) => { - return template.id == templateId; - })?.package?.packageVersion; - const newCreationBotData = { - templateId: templateId || '', - templateVersion: templateVersion || '', - name: formData.name, - description: formData.description, - location: formData.location, - schemaUrl: formData.schemaUrl, - appLocale, - templateDir: formData?.pvaData?.templateDir, - eTag: formData?.pvaData?.eTag, - urlSuffix: formData?.pvaData?.urlSuffix, - preserveRoot: formData?.pvaData?.preserveRoot, - alias: formData?.alias, - profile: formData?.profile, - source: formData?.source, - }; - createNewBotV2(newCreationBotData); - } else { - addNewSkillToBotProject(newBotData); - } + const templateVersion = templateProjects.find((template: BotTemplate) => { + return template.id == templateId; + })?.package?.packageVersion; + const newCreationBotData = { + templateId: templateId || '', + templateVersion: templateVersion || '', + name: formData.name, + description: formData.description, + location: formData.location, + schemaUrl: formData.schemaUrl, + appLocale, + templateDir: formData?.pvaData?.templateDir, + eTag: formData?.pvaData?.eTag, + urlSuffix: formData?.pvaData?.urlSuffix, + preserveRoot: formData?.pvaData?.preserveRoot, + alias: formData?.alias, + profile: formData?.profile, + source: formData?.source, + }; + createNewBotV2(newCreationBotData); } else { createNewBot(newBotData); } @@ -159,48 +150,30 @@ export const CreationModal: React.FC = (props) => { }; const renderDefineConversation = () => { - if (featureFlags?.NEW_CREATION_FLOW?.enabled) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + ); }; const renderCreateOptions = () => { - if (featureFlags?.NEW_CREATION_FLOW?.enabled) { - return ( - - ); - } else { - return ; - } + return ( + + ); }; return ( diff --git a/Composer/packages/client/src/pages/home/Home.tsx b/Composer/packages/client/src/pages/home/Home.tsx index 12611eb0af..af5422c006 100644 --- a/Composer/packages/client/src/pages/home/Home.tsx +++ b/Composer/packages/client/src/pages/home/Home.tsx @@ -13,19 +13,13 @@ import { useRecoilValue } from 'recoil'; import { Toolbar, IToolbarItem, defaultToolbarButtonStyles } from '@bfc/ui-shared'; import { CreationFlowStatus } from '../../constants'; -import { dispatcherState, botDisplayNameState, templateProjectsState } from '../../recoilModel'; -import { - recentProjectsState, - templateIdState, - currentProjectIdState, - featureFlagsState, -} from '../../recoilModel/atoms/appState'; +import { dispatcherState, botDisplayNameState } from '../../recoilModel'; +import { recentProjectsState, templateIdState, currentProjectIdState } from '../../recoilModel/atoms/appState'; import TelemetryClient from '../../telemetry/TelemetryClient'; import * as home from './styles'; import { ItemContainer } from './ItemContainer'; import { RecentBotList } from './RecentBotList'; -import { ExampleList } from './ExampleList'; const linksBottom = [ { @@ -74,9 +68,6 @@ const Home: React.FC = () => { setCreationFlowType, } = useRecoilValue(dispatcherState); - const featureFlags = useRecoilValue(featureFlagsState); - const botTemplates = useRecoilValue(templateProjectsState); - const onItemChosen = async (item) => { if (item?.path) { await openProject(item.path, 'default', true, null, (projectId) => { @@ -99,7 +90,7 @@ const Home: React.FC = () => { const onClickNewBot = () => { setCreationFlowType('Bot'); setCreationFlowStatus(CreationFlowStatus.NEW); - featureFlags?.NEW_CREATION_FLOW?.enabled ? navigate(`v2/projects/create`) : navigate(`projects/create`); + navigate(`v2/projects/create`); }; const toolbarItems: IToolbarItem[] = [ @@ -257,17 +248,6 @@ const Home: React.FC = () => {
- {!featureFlags?.NEW_CREATION_FLOW?.enabled && ( -
-

{formatMessage(`Examples`)}

-

- {formatMessage( - "These examples bring together all of the best practices and supporting components we've identified through building of conversational experiences." - )} -

- -
- )}
); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 3f274f2100..0127be9242 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -43,7 +43,6 @@ import { logMessage, setError } from './../dispatchers/shared'; import { checkIfBotExistsInBotProjectFile, createNewBotFromTemplate, - createNewBotFromTemplateV2, fetchProjectDataById, flushExistingTasks, getSkillNameIdentifier, @@ -386,11 +385,13 @@ export const projectDispatcher = () => { preserveRoot, profile, source, + runtimeType, + runtimeLanguage, } = newProjectData; // starts the creation process and stores the jobID in state for tracking - const response = await createNewBotFromTemplateV2( - callbackHelpers, + const response = await httpClient.post(`/v2/projects`, { + storageId: 'default', templateId, templateVersion, name, @@ -401,8 +402,10 @@ export const projectDispatcher = () => { templateDir, eTag, alias, - preserveRoot - ); + preserveRoot, + runtimeType, + runtimeLanguage, + }); if (response.data.jobId) { dispatcher.updateCreationMessage(response.data.jobId, templateId, urlSuffix, profile, source); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index fa4174ba6b..2689f89173 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -635,37 +635,6 @@ export const createNewBotFromTemplate = async ( return { projectId, mainDialog }; }; -export const createNewBotFromTemplateV2 = async ( - callbackHelpers, - templateId: string, - templateVersion: 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, - templateVersion, - 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[]) => { diff --git a/Composer/packages/client/src/recoilModel/selectors/extensions.ts b/Composer/packages/client/src/recoilModel/selectors/extensions.ts index e11b49deff..32ab01aadb 100644 --- a/Composer/packages/client/src/recoilModel/selectors/extensions.ts +++ b/Composer/packages/client/src/recoilModel/selectors/extensions.ts @@ -3,7 +3,7 @@ import { selector } from 'recoil'; -import { extensionsState, featureFlagsState } from '../atoms/appState'; +import { extensionsState } from '../atoms/appState'; import { ExtensionPageConfig } from '../../utils/pageLinks'; export const enabledExtensionsSelector = selector({ @@ -19,16 +19,11 @@ export const pluginPagesSelector = selector({ key: 'pluginPagesSelector', get: ({ get }) => { const extensions = get(enabledExtensionsSelector); - const featureFlags = get(featureFlagsState); return extensions.reduce((pages, p) => { const pagesConfig = p.contributes?.views?.pages; if (Array.isArray(pagesConfig) && pagesConfig.length > 0) { - // TODO: This code is present to enable the package manager to be behind a feature flag. - // When this is no longer necessary, we should remove this conditional! - if (p.id !== 'package-manager' || featureFlags?.NEW_CREATION_FLOW?.enabled === true) { - pages.push(...pagesConfig.map((page) => ({ ...page, id: p.id }))); - } + pages.push(...pagesConfig.map((page) => ({ ...page, id: p.id }))); } return pages; }, [] as ExtensionPageConfig[]); diff --git a/Composer/packages/lib/shared/src/featureFlagUtils/index.ts b/Composer/packages/lib/shared/src/featureFlagUtils/index.ts index 98937c2131..62d99cad72 100644 --- a/Composer/packages/lib/shared/src/featureFlagUtils/index.ts +++ b/Composer/packages/lib/shared/src/featureFlagUtils/index.ts @@ -5,15 +5,6 @@ import formatMessage from 'format-message'; import { FeatureFlagMap } from '@botframework-composer/types'; export const getDefaultFeatureFlags = (): FeatureFlagMap => ({ - NEW_CREATION_FLOW: { - displayName: formatMessage('New Creation Experience'), - description: formatMessage( - 'Preview the new bot creation experience. Create new bots that use the Adaptive Runtime, and can be enhanced using Package Manager.' - ), - isHidden: false, - enabled: false, - documentationLink: 'https://aka.ms/ComponentTemplateDocumentation', - }, FORM_DIALOG: { displayName: formatMessage('Form dialogs'), description: formatMessage( diff --git a/Composer/packages/server-workers/src/workers/templateInstallation.worker.ts b/Composer/packages/server-workers/src/workers/templateInstallation.worker.ts index 0f7bb03d83..6ad7aa5654 100644 --- a/Composer/packages/server-workers/src/workers/templateInstallation.worker.ts +++ b/Composer/packages/server-workers/src/workers/templateInstallation.worker.ts @@ -33,15 +33,16 @@ const instantiateRemoteTemplate = async ( yeomanEnv: yeoman, generatorName: string, dstDir: string, - projectName: string + projectName: string, + runtimeType: string, + runtimeLanguage: string ): Promise => { log('About to instantiate a template!', dstDir, generatorName, projectName); yeomanEnv.cwd = dstDir; - try { // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore @types/yeoman-environment is outdated - await yeomanEnv.run([generatorName, projectName]); + await yeomanEnv.run([generatorName, projectName, '-p', runtimeLanguage, '-i', runtimeType]); log('Template successfully instantiated', dstDir, generatorName, projectName); } catch (err) { log('Template failed to instantiate', dstDir, generatorName, projectName); @@ -54,7 +55,9 @@ const yeomanWork = async ( templateVersion: string, dstDir: string, projectName: string, - templateGeneratorPath: string + templateGeneratorPath: string, + runtimeType: string, + runtimeLanguage: string ) => { const generatorName = npmPackageName.toLowerCase().replace('generator-', ''); // create yeoman environment @@ -82,7 +85,7 @@ const yeomanWork = async ( log('Instantiating Yeoman template'); parentPort?.postMessage({ status: 'Instantiating Yeoman template' }); - await instantiateRemoteTemplate(yeomanEnv, generatorName, dstDir, projectName); + await instantiateRemoteTemplate(yeomanEnv, generatorName, dstDir, projectName, runtimeType, runtimeLanguage); } else { // handle error throw new Error(`error hit when installing remote template`); @@ -95,6 +98,8 @@ export type TemplateInstallationArgs = { dstDir: string; projectName: string; templateGeneratorPath: string; + runtimeType: string; + runtimeLanguage: string; }; if (!isMainThread) { @@ -103,7 +108,9 @@ if (!isMainThread) { workerData.templateVersion, workerData.dstDir, workerData.projectName, - workerData.templateGeneratorPath + workerData.templateGeneratorPath, + workerData.runtimeType, + workerData.runtimeLanguage ) .then(() => { process.exit(0); diff --git a/Composer/packages/server/src/controllers/asset.ts b/Composer/packages/server/src/controllers/asset.ts index 9005ea7718..9d048db6d0 100644 --- a/Composer/packages/server/src/controllers/asset.ts +++ b/Composer/packages/server/src/controllers/asset.ts @@ -41,6 +41,14 @@ export async function getProjTemplatesV2(req: any, res: any) { id: QnABotTemplateId, name: 'QNA', description: formatMessage('Empty bot template that routes to qna configuration'), + dotnetSupport: { + functionsSupported: true, + webAppSupported: true, + }, + nodeSupport: { + functionsSupported: true, + webAppSupported: true, + }, package: { packageName: emptyBotNpmTemplateName, packageSource: 'npm', diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index 535bbea45c..f70e3a3fb2 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -410,6 +410,12 @@ "azure_bot_service_channels_5b358c1c": { "message": "Azure Bot Service channels" }, + "azure_functions_5e23be5c": { + "message": "Azure Functions" + }, + "azure_web_app_d834cb4c": { + "message": "Azure Web App" + }, "back_2900f52a": { "message": "Back" }, @@ -1505,9 +1511,6 @@ "events_cf7a8c50": { "message": "Events" }, - "example_bot_list_9be1d563": { - "message": "Example bot list" - }, "examples_c435f08c": { "message": "Examples" }, @@ -2402,9 +2405,6 @@ "new_13daf639": { "message": "New" }, - "new_creation_experience_29591aca": { - "message": "New Creation Experience" - }, "new_template_49e6f0f2": { "message": "New template" }, @@ -2705,9 +2705,6 @@ "preview_features_e279bac5": { "message": "Preview features" }, - "preview_the_new_bot_creation_experience_create_new_e0b7150f": { - "message": "Preview the new bot creation experience. Create new bots that use the Adaptive Runtime, and can be enhanced using Package Manager." - }, "previous_bd2ac015": { "message": "Previous" }, @@ -3089,6 +3086,9 @@ "runtime_config_a2904ff9": { "message": "Runtime Config" }, + "runtime_type_f9e2419b": { + "message": "Runtime type" + }, "sample_phrases_5d78fa35": { "message": "Sample Phrases" }, @@ -3593,9 +3593,6 @@ "there_was_error_creating_your_kb_53b31ff3": { "message": "There was error creating your KB" }, - "these_examples_bring_together_all_of_the_best_prac_ca1b89c7": { - "message": "These examples bring together all of the best practices and supporting components we''ve identified through building of conversational experiences." - }, "these_tasks_will_be_used_to_generate_the_manifest__2791be0e": { "message": "These tasks will be used to generate the manifest and describe the capabilities of this skill to those who may want to use it." }, diff --git a/Composer/packages/server/src/models/asset/__tests__/assetManager.test.ts b/Composer/packages/server/src/models/asset/__tests__/assetManager.test.ts index a67e38b939..00cba711c2 100644 --- a/Composer/packages/server/src/models/asset/__tests__/assetManager.test.ts +++ b/Composer/packages/server/src/models/asset/__tests__/assetManager.test.ts @@ -154,7 +154,9 @@ describe('assetManager', () => { '1.0.3', 'sampleConversationalCore', mockLocRef, - '0' + '0', + 'webapp', + 'dotnet' ); expect(newBotLocationRef).toStrictEqual({ path: '/path/to/npmbot', diff --git a/Composer/packages/server/src/models/asset/assetManager.ts b/Composer/packages/server/src/models/asset/assetManager.ts index bd135729fd..bdda81da48 100644 --- a/Composer/packages/server/src/models/asset/assetManager.ts +++ b/Composer/packages/server/src/models/asset/assetManager.ts @@ -5,10 +5,11 @@ import fs from 'fs'; import path from 'path'; import find from 'lodash/find'; -import { UserIdentity, FileExtensions, FeedType } from '@bfc/extension'; +import { UserIdentity, FileExtensions, FeedType, RuntimeType } from '@bfc/extension'; import { mkdirSync, readFile } from 'fs-extra'; -import { BotTemplate, emptyBotNpmTemplateName, QnABotTemplateId } from '@bfc/shared'; +import { BotTemplate, emptyBotNpmTemplateName, FeedName, QnABotTemplateId } from '@bfc/shared'; import { ServerWorker } from '@bfc/server-workers'; +import isArray from 'lodash/isArray'; import { ExtensionContext } from '../extension/extensionContext'; import log from '../../logger'; @@ -96,6 +97,8 @@ export class AssetManager { projectName: string, ref: LocationRef, jobId: string, + runtimeType: RuntimeType, + runtimeLanguage: FeedName, user?: UserIdentity ): Promise { try { @@ -120,12 +123,13 @@ export class AssetManager { dstDir, projectName, templateGeneratorPath, - } as any, + runtimeType, + runtimeLanguage, + }, (status, msg) => { BackgroundProcessManager.updateProcess(jobId, status, msg); } ); - return ref; } catch (err) { if (err?.message.match(/npm/)) { @@ -262,11 +266,12 @@ export class AssetManager { const res = await fetch(feedUrl); const data = await res.json(); const feedType = this.getFeedType(); + if (feedType === 'npm') { return data.objects.map((result) => { - const { name, version, description = '' } = result.package; + const { name, version, keywords, description = '' } = result.package; const displayName = this.getPackageDisplayName(name); - return { + const templateToReturn = { id: name, name: displayName, description: description, @@ -276,6 +281,21 @@ export class AssetManager { packageVersion: version, }, } as BotTemplate; + if (isArray(keywords)) { + if (keywords.includes('bf-dotnet-functions') || keywords.includes('bf-dotnet-webapp')) { + templateToReturn.dotnetSupport = { + functionsSupported: keywords.includes('bf-dotnet-functions'), + webAppSupported: keywords.includes('bf-dotnet-webapp'), + }; + } + if (keywords.includes('bf-js-functions') || keywords.includes('bf-js-webapp')) { + templateToReturn.nodeSupport = { + functionsSupported: keywords.includes('bf-js-functions'), + webAppSupported: keywords.includes('bf-js-webapp'), + }; + } + } + return templateToReturn; }); } else if (feedType === 'nuget') { // TODO: handle nuget processing diff --git a/Composer/packages/server/src/services/project.ts b/Composer/packages/server/src/services/project.ts index f13621c20a..dde247c8b1 100644 --- a/Composer/packages/server/src/services/project.ts +++ b/Composer/packages/server/src/services/project.ts @@ -449,7 +449,10 @@ export class BotProjectService { alias, locale, schemaUrl, + runtimeType, + runtimeLanguage, } = req.body; + // get user from request const user = await ExtensionContext.getUserFromRequest(req); @@ -476,6 +479,8 @@ export class BotProjectService { name, locationRef, jobId, + runtimeType, + runtimeLanguage, user ); diff --git a/Composer/packages/server/src/utility/__tests__/creation.test.ts b/Composer/packages/server/src/utility/__tests__/creation.test.ts index 8c40d4bad9..7deff6fae7 100644 --- a/Composer/packages/server/src/utility/__tests__/creation.test.ts +++ b/Composer/packages/server/src/utility/__tests__/creation.test.ts @@ -8,7 +8,7 @@ import { sortTemplates, templateSortOrder } from '../creation'; describe('templateSort', () => { const templates: BotTemplate[] = [ { - id: '@microsoft/generator-microsoft-bot-calendar-assistant', + id: '@microsoft/generator-bot-assistant-core', name: ' Calendar Assistant', description: 'Preview Calendar Assistant template for TESTING ONLY', package: { @@ -18,7 +18,27 @@ describe('templateSort', () => { }, }, { - id: '@microsoft/generator-microsoft-bot-calendar', + id: '@microsoft/generator-bot-enterprise-assistant', + name: ' Calendar Assistant', + description: 'Preview Calendar Assistant template for TESTING ONLY', + package: { + packageName: '@microsoft/generator-microsoft-bot-calendar-assistant', + packageSource: 'npm', + packageVersion: '0.0.1-preview-20210302.2eaae0d', + }, + }, + { + id: '@microsoft/generator-bot-people', + name: ' Calendar Assistant', + description: 'Preview Calendar Assistant template for TESTING ONLY', + package: { + packageName: '@microsoft/generator-microsoft-bot-calendar-assistant', + packageSource: 'npm', + packageVersion: '0.0.1-preview-20210302.2eaae0d', + }, + }, + { + id: '@microsoft/generator-bot-calendar', name: ' Calendar', description: 'Preview calendar bot for TESTING ONLY', package: { @@ -28,7 +48,7 @@ describe('templateSort', () => { }, }, { - id: '@microsoft/generator-microsoft-bot-conversational-core', + id: '@microsoft/generator-bot-conversational-core', name: ' Conversational Core', description: 'Preview conversational core package for TESTING ONLY', package: { @@ -38,7 +58,7 @@ describe('templateSort', () => { }, }, { - id: '@microsoft/generator-microsoft-bot-empty', + id: '@microsoft/generator-bot-empty', name: ' Empty', description: 'Instantiates a Bot Framework bot using the [component model](https://aka.ms/ComponentTemplateDocumentation). This template instantiates an empty bot with no dependent packages.', @@ -61,6 +81,7 @@ describe('templateSort', () => { ]; it('should return sorted templates per sortOrder obj', async () => { + // note - the list in templates has to include all the same items in creation.ts const sortedTemplateList = sortTemplates(templates); templateSortOrder.forEach((templateSortEntry, index) => { if ( diff --git a/Composer/packages/server/src/utility/creation.ts b/Composer/packages/server/src/utility/creation.ts index 03e019434e..7dd748b9a2 100644 --- a/Composer/packages/server/src/utility/creation.ts +++ b/Composer/packages/server/src/utility/creation.ts @@ -5,8 +5,12 @@ import { BotTemplate, QnABotTemplateId } from '@bfc/shared'; import union from 'lodash/union'; export const templateSortOrder = [ - { generatorName: '@microsoft/generator-microsoft-bot-empty', displayName: 'Blank bot' }, - { generatorName: '@microsoft/generator-microsoft-bot-conversational-core', displayName: 'Basic conversational bot' }, + { generatorName: '@microsoft/generator-bot-empty', displayName: 'Blank bot' }, + { generatorName: '@microsoft/generator-bot-conversational-core', displayName: 'Basic conversational bot' }, + { generatorName: '@microsoft/generator-bot-assistant-core', displayName: 'Basic assistant' }, + { generatorName: '@microsoft/generator-bot-enterprise-assistant', displayName: 'Enterprise assistant' }, + { generatorName: '@microsoft/generator-bot-people', displayName: 'People' }, + { generatorName: '@microsoft/generator-bot-calendar', displayName: 'Calendar' }, { generatorName: QnABotTemplateId, displayName: 'QnAMaker bot' }, ]; diff --git a/Composer/packages/types/src/creation.ts b/Composer/packages/types/src/creation.ts index b06ed75ae6..c24caaf5a4 100644 --- a/Composer/packages/types/src/creation.ts +++ b/Composer/packages/types/src/creation.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export const csharpFeedKey = 'firstPartyCsharp'; -export const nodeFeedKey = 'firstPartyNode'; +export const csharpFeedKey = 'dotnet'; +export const nodeFeedKey = 'js'; export const defaultFeeds = [nodeFeedKey, csharpFeedKey] as const; export type FeedName = typeof defaultFeeds[number]; export type FeedType = 'npm' | 'nuget'; @@ -64,3 +64,8 @@ export type TeamsManifest = { webApplicationInfo?: WebApplicationInfo; devicePermissions?: string[]; }; + +export const webAppRuntimeKey = 'webapp'; +export const functionsRuntimeKey = 'functions'; +export const availableRunTimes = [webAppRuntimeKey, functionsRuntimeKey] as const; +export type RuntimeType = typeof availableRunTimes[number]; diff --git a/Composer/packages/types/src/featureFlags.ts b/Composer/packages/types/src/featureFlags.ts index 81f04ec290..5542e657f3 100644 --- a/Composer/packages/types/src/featureFlags.ts +++ b/Composer/packages/types/src/featureFlags.ts @@ -14,6 +14,6 @@ export type FeatureFlag = { enabled: boolean; }; -export type FeatureFlagKey = 'FORM_DIALOG' | 'NEW_CREATION_FLOW'; +export type FeatureFlagKey = 'FORM_DIALOG'; export type FeatureFlagMap = Record; diff --git a/Composer/packages/types/src/runtime.ts b/Composer/packages/types/src/runtime.ts index 00a90ebc93..ccc53a816b 100644 --- a/Composer/packages/types/src/runtime.ts +++ b/Composer/packages/types/src/runtime.ts @@ -36,6 +36,8 @@ export type BotTemplate = { id: string; name: string; description: string; + nodeSupport?: EnvSupport; + dotnetSupport?: EnvSupport; /* absolute path */ path?: string; /* tags for further grouping and search secenario */ @@ -50,6 +52,11 @@ export type BotTemplate = { index?: number; }; +export type EnvSupport = { + webAppSupported: boolean; + functionsSupported: boolean; +}; + export type RuntimeTemplate = { /** method used to eject the runtime into a project. returns resulting path of runtime! */ eject?: (project: IBotProject, localDisk?: any, isReplace?: boolean) => Promise; diff --git a/azure-pipelines.yml b/azure-pipelines.yml index efc97b3d7c..dd97c701e2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,6 +1,3 @@ -trigger: - - main - pr: autoCancel: true branches: @@ -20,17 +17,23 @@ jobs: vmImage: ubuntu-latest steps: + + - task: UseDotNet@2 + inputs: + packageType: 'runtime' + version: '3.1.x' - task: NodeTool@0 displayName: "Use Node 14.15.5" inputs: versionSpec: 14.15.5 - # - task: Cache@2 - # inputs: - # key: yarn | $(Agent.OS) | Composer/yarn.lock - # path: $(YARN_CACHE_FOLDER) - # restoreKeys: | - # yarn | $(Agent.OS) - # displayName: Cache Yarn packages + - task: Bash@3 + inputs: + targetType: 'inline' + script: + # Write your commands here + dotnet nuget add source https://botbuilder.myget.org/F/botbuilder-v4-dotnet-daily/api/v3/index.json + + npm install -g @microsoft/generator-bot-empty - task: Cache@2 displayName: Cache Cypress binary inputs: diff --git a/extensions/azurePublish/src/components/azureProvisionDialog.tsx b/extensions/azurePublish/src/components/azureProvisionDialog.tsx index 30afb1bcd7..970687b628 100644 --- a/extensions/azurePublish/src/components/azureProvisionDialog.tsx +++ b/extensions/azurePublish/src/components/azureProvisionDialog.tsx @@ -391,6 +391,9 @@ export const AzureProvisionDialog: React.FC = () => { expiration: (decoded.exp || 0) * 1000, // convert to ms, sessionExpired: false, }); + setPage(PageTypes.ConfigProvision); + setTitle(DialogTitle.CONFIG_RESOURCES); + setLoginErrorMsg(undefined); } } else { getTenants().then((tenants) => { @@ -1018,51 +1021,55 @@ export const AzureProvisionDialog: React.FC = () => { } return ( -
-
- {page === PageTypes.ConfigProvision && PageFormConfig} - {page === PageTypes.AddResources && PageAddResources()} - {page === PageTypes.ReviewResource && PageReview} - {page === PageTypes.EditJson && ( - { - setEditorError(false); - setImportConfig(value); - }} - onError={() => { - setEditorError(true); - }} - /> - )} -
-
- {PageFooter} -
+
+
+
+ {page === PageTypes.ConfigProvision && PageFormConfig} + {page === PageTypes.AddResources && PageAddResources()} + {page === PageTypes.ReviewResource && PageReview} + {page === PageTypes.EditJson && ( + { + setEditorError(false); + setImportConfig(value); + }} + onError={() => { + setEditorError(true); + }} + /> + )} +
+
+ {PageFooter} +
+
+ ); }; diff --git a/extensions/azurePublish/src/components/images/QnA-Maker.svg b/extensions/azurePublish/src/components/images/QnA-Maker.svg index 60469407ef..d0f8d7412b 100644 --- a/extensions/azurePublish/src/components/images/QnA-Maker.svg +++ b/extensions/azurePublish/src/components/images/QnA-Maker.svg @@ -1,17 +1,41 @@ - + - + - - - + + + - \ No newline at end of file +; diff --git a/extensions/azurePublish/src/node/deploy.ts b/extensions/azurePublish/src/node/deploy.ts index bc57c5906f..e2eb58739c 100644 --- a/extensions/azurePublish/src/node/deploy.ts +++ b/extensions/azurePublish/src/node/deploy.ts @@ -147,6 +147,7 @@ export class BotProjectDeploy { } private async zipDirectory(source: string, out: string) { + console.log(`Zip the files in ${source} into a zip file ${out}`); try { const archive = archiver('zip', { zlib: { level: 9 } }); // eslint-disable-next-line security/detect-non-literal-fs-filename @@ -156,7 +157,7 @@ export class BotProjectDeploy { .glob('**/*', { cwd: source, dot: true, - ignore: ['**/code.zip', 'node_modules/**/*'], + ignore: ['**/code.zip'], // , 'node_modules/**/*' }) .on('error', (err) => reject(err)) .pipe(stream); @@ -174,7 +175,7 @@ export class BotProjectDeploy { private async deployZip(token: string, zipPath: string, name: string, env: string, hostname?: string) { this.logger({ status: BotProjectDeployLoggerType.DEPLOY_INFO, - message: 'Uploading zip file...', + message: `Uploading zip file... to ${hostname ? hostname : name + (env ? '-' + env : '')}`, }); const publishEndpoint = `https://${ diff --git a/extensions/packageManager/package.json b/extensions/packageManager/package.json index aa12ed0535..c928a39a5a 100644 --- a/extensions/packageManager/package.json +++ b/extensions/packageManager/package.json @@ -50,7 +50,7 @@ "@bfc/extension-client": "file:../../Composer/packages/extension-client", "@bfc/ui-shared": "file:../../Composer/packages/lib/ui-shared", "@emotion/core": "^10.0.35", - "@microsoft/bf-dialog": "4.11.0", + "@microsoft/bf-dialog": "4.13.0-dev.20210401.8dc6ffe", "@uifabric/fluent-theme": "^7.1.4", "axios": "^0.19.2", "date-fns": "^2.16.1", diff --git a/extensions/packageManager/src/node/index.ts b/extensions/packageManager/src/node/index.ts index d92f7559dc..38a8bcf60e 100644 --- a/extensions/packageManager/src/node/index.ts +++ b/extensions/packageManager/src/node/index.ts @@ -129,6 +129,20 @@ export default async (composer: IExtensionRegistration): Promise => { }, type: PackageSourceType.NuGet, }, + { + key: 'npm', + text: formatMessage('npm'), + url: `https://registry.npmjs.org/-/v1/search?text=keywords:${botComponentTag}+scope:microsoft&size=100&from=0`, + searchUrl: `https://registry.npmjs.org/-/v1/search?text={{keyword}}+keywords:${botComponentTag}&size=100&from=0`, + readonly: true, + }, + { + key: 'npm-community', + text: formatMessage('JS community packages'), + url: `https://registry.npmjs.org/-/v1/search?text=keywords:${botComponentTag}&size=100&from=0`, + searchUrl: `https://registry.npmjs.org/-/v1/search?text={{keyword}}+keywords:${botComponentTag}&size=100&from=0`, + readonly: true, + }, ]; // If there are package sources stored in the user profile @@ -201,6 +215,8 @@ export default async (composer: IExtensionRegistration): Promise => { query: 'tags:msbot-component', }; + composer.log('GETTING FEED', packageSource, packageSource.defaultQuery ?? packageQuery); + const packages = await feed.getPackages(packageSource.defaultQuery ?? packageQuery); if (Array.isArray(packages)) { @@ -348,6 +364,13 @@ export default async (composer: IExtensionRegistration): Promise => { ); const mergeResults = await realMerge.merge(); + + composer.log( + 'MERGE RESULTS', + path.join(currentProject.dataDir, 'dialogs/imported'), + JSON.stringify(mergeResults, null, 2) + ); + const installedComponents = await loadPackageAssets(mergeResults.components.filter(isAdaptiveComponent)); if (mergeResults) { res.json({ @@ -356,7 +379,10 @@ export default async (composer: IExtensionRegistration): Promise => { }); let runtimeLanguage = 'c#'; - if (currentProject.settings.runtime.key === 'node-azurewebapp') { + if ( + currentProject.settings.runtime.key === 'node-azurewebapp' || + currentProject.settings.runtime.key.startsWith('adaptive-runtime-js') + ) { runtimeLanguage = 'js'; } diff --git a/extensions/packageManager/src/pages/Library.tsx b/extensions/packageManager/src/pages/Library.tsx index 8a46c587cd..5af9ff841f 100644 --- a/extensions/packageManager/src/pages/Library.tsx +++ b/extensions/packageManager/src/pages/Library.tsx @@ -210,7 +210,7 @@ const Library: React.FC = () => { setEjectedRuntime(true); // detect programming language. // should one day be a dynamic property of the runtime or at least stored in the settings? - if (settings.runtime.key === 'node-azurewebapp') { + if (settings.runtime.key === 'node-azurewebapp' || settings.runtime.key.startsWith('adaptive-runtime-js')) { setRuntimeLanguage('js'); } else { setRuntimeLanguage('c#'); diff --git a/extensions/packageManager/yarn.lock b/extensions/packageManager/yarn.lock index 0f858ee7de..76c1cae7fb 100644 --- a/extensions/packageManager/yarn.lock +++ b/extensions/packageManager/yarn.lock @@ -253,10 +253,10 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== -"@microsoft/bf-cli-command@4.11.0": - version "4.11.0" - resolved "https://registry.yarnpkg.com/@microsoft/bf-cli-command/-/bf-cli-command-4.11.0.tgz#3e32c93ea2ef139fb68c51d9e460253ab2e3af45" - integrity sha512-rR8bSEvbwpvdRhi7j7GI3pi6uAuX2I0Tehc7FIqDyzv6m9QwOqJ2JfN1Q6ArhLRBI2kVKsf2JCJt4+067SG2Qg== +"@microsoft/bf-cli-command@4.13.0-dev.20210401.8dc6ffe": + version "4.13.0-dev.20210401.8dc6ffe" + resolved "https://registry.yarnpkg.com/@microsoft/bf-cli-command/-/bf-cli-command-4.13.0-dev.20210401.8dc6ffe.tgz#c391d6a4cda03063a74fff186c4b2ee247cb0afa" + integrity sha512-daGbfwXcBwNWkBAR23MsFags18eMNUCpH8BNfZKEv0El3bLifVhfbGILQhpsM0BFCzcgVyooLA6cnBAUPopZ0Q== dependencies: "@oclif/command" "~1.5.19" "@oclif/config" "~1.13.3" @@ -268,13 +268,13 @@ fs-extra "^7.0.1" tslib "^2.0.3" -"@microsoft/bf-dialog@4.11.0": - version "4.11.0" - resolved "https://registry.yarnpkg.com/@microsoft/bf-dialog/-/bf-dialog-4.11.0.tgz#5636c4fc6db2405204542d1736ab7a0f04e38e85" - integrity sha512-ucxNytF8GqcFv+ts9ChpSbIU8ZZrYT9KqkNGJLyBdAWdixGqepBP8APW2BF3G36bMYPGJuWywZ4HEwPIn8Hw9A== +"@microsoft/bf-dialog@4.13.0-dev.20210401.8dc6ffe": + version "4.13.0-dev.20210401.8dc6ffe" + resolved "https://registry.yarnpkg.com/@microsoft/bf-dialog/-/bf-dialog-4.13.0-dev.20210401.8dc6ffe.tgz#160ec22f79e1049e48e8e40e0eaf13206a29b267" + integrity sha512-zg4z3Svvq0KFhO8GEjdMPC5dNUjUSm/7lm0Qmq1rxFgVNQJscBpaOwV2e6dwnJJmKgq5sSUjQSIAdkzLKufMDw== dependencies: "@apidevtools/json-schema-ref-parser" "^9.0.1" - "@microsoft/bf-cli-command" "4.11.0" + "@microsoft/bf-cli-command" "4.13.0-dev.20210401.8dc6ffe" "@oclif/command" "~1.5.19" "@oclif/config" "~1.13.3" "@oclif/errors" "~1.2.2" @@ -292,6 +292,7 @@ os "~0.1.1" path "^0.12.7" seedrandom "~3.0.5" + semver "^7.3.4" tslib "^2.0.3" xml2js "^0.4.19" diff --git a/extensions/runtimes/src/index.ts b/extensions/runtimes/src/index.ts index 7b3777666a..2b91b0b996 100644 --- a/extensions/runtimes/src/index.ts +++ b/extensions/runtimes/src/index.ts @@ -17,6 +17,10 @@ const removeDirAndFiles = promisify(rimraf); export default async (composer: any): Promise => { const dotnetTemplatePath = path.resolve(__dirname, '../../../runtime/dotnet'); const nodeTemplatePath = path.resolve(__dirname, '../../../runtime/node'); + + /** + * these are the old 1.0 adaptive runtime definitions + */ // register the bundled c# runtime used by the local publisher with the eject feature composer.addRuntimeTemplate({ key: 'csharp-azurewebapp', @@ -321,11 +325,149 @@ export default async (composer: any): Promise => { ) => {}, }); + /** + * these are the new 2.0 adaptive runtime definitions + */ composer.addRuntimeTemplate({ key: 'adaptive-runtime-dotnet-webapp', - name: 'C#', - startCommand: 'dotnet run', - path: dotnetTemplatePath, + name: 'C# - Web App', + build: async (runtimePath: string, _project: any) => { + composer.log(`BUILD THIS C# PROJECT! at ${runtimePath}...`); + composer.log('Run dotnet user-secrets init...'); + + // TODO: capture output of this and store it somewhere useful + const { stderr: initErr } = await execAsync(`dotnet user-secrets init --project ${_project.name}.csproj`, { + cwd: runtimePath, + }); + if (initErr) { + throw new Error(initErr); + } + + composer.log('Run dotnet build...'); + const { stderr: buildErr } = await execAsync(`dotnet build ${_project.name}.csproj`, { cwd: runtimePath }); + if (buildErr) { + throw new Error(buildErr); + } + composer.log('FINISHED BUILDING!'); + }, + installComponent: async ( + runtimePath: string, + packageName: string, + version: string, + source: string, + _project: any + ): Promise => { + // run dotnet install on the project + const command = `dotnet add ${_project.name}.csproj package "${packageName}"${ + version ? ' --version="' + version + '"' : '' + }${source ? ' --source="' + source + '"' : ''}`; + composer.log('EXEC:', command); + const { stderr: installError, stdout: installOutput } = await execAsync(command, { + cwd: path.join(runtimePath), + }); + if (installError) { + throw new Error(installError); + } + return installOutput; + }, + uninstallComponent: async (runtimePath: string, packageName: string, _project: any): Promise => { + // run dotnet install on the project + composer.log(`EXECUTE: dotnet remove ${_project.name}.csproj package ${packageName}`); + const { stderr: installError, stdout: installOutput } = await execAsync( + `dotnet remove ${_project.name}.csproj package ${packageName}`, + { + cwd: path.join(runtimePath), + } + ); + if (installError) { + throw new Error(installError); + } + return installOutput; + }, + identifyManifest: (runtimePath: string, projName?: string): string => { + return path.join(runtimePath, `${projName}.csproj`); + }, + run: async (project: any, localDisk: IFileStorage) => { + composer.log('RUN THIS C# PROJECT!'); + }, + buildDeploy: async (runtimePath: string, project: any, settings: any, profileName: string): Promise => { + composer.log('BUILD FOR DEPLOY TO AZURE!'); + + // find publishing profile in list + const profile = project.settings.publishTargets.find((p) => p.name === profileName); + + const csproj = `${project.name}.csproj`; + const publishFolder = path.join(runtimePath, 'bin', 'release', 'publishTarget'); + const deployFilePath = path.join(runtimePath, '.deployment'); + const dotnetProjectPath = path.join(runtimePath, csproj); + + // Check for existing .deployment file, if missing, write it. + if (!(await fs.pathExists(deployFilePath))) { + const data = `[config]\nproject = ${csproj}`; + + await fs.writeFile(deployFilePath, data); + } + + // do the dotnet publish + try { + const configuration = JSON.parse(profile.configuration); + const runtimeIdentifier = configuration.runtimeIdentifier; + + // if runtime identifier set, make dotnet runtime to self contained, default runtime identifier is win-x64, please refer to https://docs.microsoft.com/en-us/dotnet/core/rid-catalog + const buildCommand = `dotnet publish "${dotnetProjectPath}" -c release -o "${publishFolder}" -v q --self-contained true -r ${ + runtimeIdentifier ?? 'win-x64' + }`; + // } + const { stdout, stderr } = await execAsync(buildCommand, { + cwd: runtimePath, + }); + composer.log('OUTPUT FROM BUILD', stdout); + if (stderr) { + composer.log('ERR FROM BUILD: ', stderr); + } + } catch (err) { + composer.log('Error doing dotnet publish', err); + throw err; + return; + } + + // write settings to disk in the appropriate location + const settingsPath = path.join(publishFolder, 'settings', 'appsettings.json'); + if (!(await fs.pathExists(path.dirname(settingsPath)))) { + await fs.mkdirp(path.dirname(settingsPath)); + } + await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); + + // return the location of the build artifiacts + return publishFolder; + }, + setSkillManifest: async ( + dstRuntimePath: string, + dstStorage: IFileStorage, + srcManifestDir: string, + srcStorage: IFileStorage, + mode = 'azurewebapp' // set default as azurewebapp + ) => { + // update manifst into runtime wwwroot + if (mode === 'azurewebapp') { + const manifestDstDir = path.resolve(dstRuntimePath, 'azurewebapp', 'wwwroot', 'manifests'); + + if (await fs.pathExists(manifestDstDir)) { + await removeDirAndFiles(manifestDstDir); + } + + if (await fs.pathExists(srcManifestDir)) { + await copyDir(srcManifestDir, srcStorage, manifestDstDir, dstStorage); + } + } + }, + }); + + composer.addRuntimeTemplate({ + key: 'adaptive-runtime-dotnet-functions', + name: 'C# - Functions', + // startCommand: 'dotnet run', + // path: dotnetTemplatePath, build: async (runtimePath: string, _project: any) => { composer.log(`BUILD THIS C# PROJECT! at ${runtimePath}...`); composer.log('Run dotnet user-secrets init...'); @@ -390,11 +532,6 @@ export default async (composer: any): Promise => { // find publishing profile in list const profile = project.settings.publishTargets.find((p) => p.name === profileName); - // if (profile.type === 'azurePublish') { - // csproj = 'Microsoft.BotFramework.Composer.WebApp.csproj'; - // } else if (profile.type === 'azureFunctionsPublish') { - // csproj = 'Microsoft.BotFramework.Composer.Functions.csproj'; - // } const csproj = `${project.name}.csproj`; const publishFolder = path.join(runtimePath, 'bin', 'release', 'publishTarget'); const deployFilePath = path.join(runtimePath, '.deployment'); @@ -412,11 +549,9 @@ export default async (composer: any): Promise => { const configuration = JSON.parse(profile.configuration); const runtimeIdentifier = configuration.runtimeIdentifier; + // TODO: swap these lines??? ben to confirm // Don't set self-contained and runtimeIdentifier for AzureFunctions. // let buildCommand = `dotnet publish "${dotnetProjectPath}" -c release -o "${publishFolder}" -v q`; - - // if (profile.type === 'azurePublish') - // { // if runtime identifier set, make dotnet runtime to self contained, default runtime identifier is win-x64, please refer to https://docs.microsoft.com/en-us/dotnet/core/rid-catalog const buildCommand = `dotnet publish "${dotnetProjectPath}" -c release -o "${publishFolder}" -v q --self-contained true -r ${ runtimeIdentifier ?? 'win-x64' @@ -434,13 +569,6 @@ export default async (composer: any): Promise => { throw err; return; } - // Then, copy the declarative assets into the build artifacts folder. - // const remoteBotPath = path.join(publishFolder, 'ComposerDialogs'); - // const localBotPath = path.join(runtimePath, 'ComposerDialogs'); - // await fs.copy(localBotPath, remoteBotPath, { - // overwrite: true, - // recursive: true, - // }); // write settings to disk in the appropriate location const settingsPath = path.join(publishFolder, 'settings', 'appsettings.json'); @@ -473,4 +601,171 @@ export default async (composer: any): Promise => { } }, }); + + /** + * This is support for the new javascript runtime + */ + composer.addRuntimeTemplate({ + key: 'adaptive-runtime-js-webapp', + name: 'JS - Web App (preview)', + // startCommand: 'node ./lib/webapp.js', + // path: nodeTemplatePath, + build: async (runtimePath: string, _project: any) => { + // do stuff + composer.log('BUILD THIS JS PROJECT'); + // install dev dependencies in production, make sure typescript is installed + const { stderr: installErr } = await execAsync('npm install && npm install --only=dev', { + cwd: runtimePath, + timeout: 120000, + }); + if (installErr) { + // in order to not throw warning, we just log all warning and error message + composer.log(`npm install timeout, ${installErr}`); + } + + composer.log('BUILD COMPLETE'); + }, + installComponent: async ( + runtimePath: string, + packageName: string, + version: string, + source: string, + _project: any + ): Promise => { + // run dotnet install on the project + const { stderr: installError, stdout: installOutput } = await execAsync( + `npm install --loglevel=error --save ${packageName}${version ? '@' + version : ''}`, + { + cwd: path.join(runtimePath), + } + ); + if (installError) { + throw new Error(installError); + } + return installOutput; + }, + uninstallComponent: async (runtimePath: string, packageName: string): Promise => { + // run dotnet install on the project + const { stderr: installError, stdout: installOutput } = await execAsync( + `npm uninstall --loglevel=error --save ${packageName}`, + { + cwd: path.join(runtimePath), + } + ); + if (installError) { + throw new Error(installError); + } + return installOutput; + }, + identifyManifest: (runtimePath: string, projName?: string): string => { + return path.join(runtimePath, 'package.json'); + }, + buildDeploy: async (runtimePath: string, project: any, settings: any, profileName: string): Promise => { + // do stuff + composer.log(`BUILD THIS JS PROJECT in ${runtimePath}`); + const { stderr: installErr } = await execAsync('npm install', { + cwd: path.resolve(runtimePath, '.'), + }); + if (installErr) { + composer.log(installErr); + } + // write settings to disk in the appropriate location + const settingsPath = path.join(runtimePath, 'settings', 'appsettings.json'); + if (!(await fs.pathExists(path.dirname(settingsPath)))) { + await fs.mkdirp(path.dirname(settingsPath)); + } + await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); + + composer.log('BUILD COMPLETE'); + return path.resolve(runtimePath, '.'); + }, + setSkillManifest: async ( + dstRuntimePath: string, + dstStorage: IFileStorage, + srcManifestDir: string, + srcStorage: IFileStorage, + mode = 'azurewebapp' + ) => {}, + }); + + composer.addRuntimeTemplate({ + key: 'adaptive-runtime-js-functions', + name: 'JS - Functions (preview)', + build: async (runtimePath: string, _project: any) => { + // do stuff + composer.log('BUILD THIS JS PROJECT'); + // install dev dependencies in production, make sure typescript is installed + const { stderr: installErr } = await execAsync('npm install && npm install --only=dev', { + cwd: runtimePath, + timeout: 120000, + }); + if (installErr) { + // in order to not throw warning, we just log all warning and error message + composer.log(`npm install timeout, ${installErr}`); + } + + composer.log('BUILD COMPLETE'); + }, + installComponent: async ( + runtimePath: string, + packageName: string, + version: string, + source: string, + _project: any + ): Promise => { + // run dotnet install on the project + const { stderr: installError, stdout: installOutput } = await execAsync( + `npm install --loglevel=error --save ${packageName}${version ? '@' + version : ''}`, + { + cwd: path.join(runtimePath), + } + ); + if (installError) { + throw new Error(installError); + } + return installOutput; + }, + uninstallComponent: async (runtimePath: string, packageName: string): Promise => { + // run dotnet install on the project + const { stderr: installError, stdout: installOutput } = await execAsync( + `npm uninstall --loglevel=error --save ${packageName}`, + { + cwd: path.join(runtimePath), + } + ); + if (installError) { + throw new Error(installError); + } + return installOutput; + }, + identifyManifest: (runtimePath: string, projName?: string): string => { + return path.join(runtimePath, 'package.json'); + }, + buildDeploy: async (runtimePath: string, project: any, settings: any, profileName: string): Promise => { + // do stuff + composer.log(`BUILD THIS JS PROJECT in ${runtimePath}`); + const { stderr: installErr } = await execAsync('npm ci', { + cwd: path.resolve(runtimePath, '.'), + }); + if (installErr) { + composer.log(installErr); + } + // write settings to disk in the appropriate location + const settingsPath = path.join(runtimePath, 'settings', 'appsettings.json'); + if (!(await fs.pathExists(path.dirname(settingsPath)))) { + await fs.mkdirp(path.dirname(settingsPath)); + } + await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); + + composer.log('BUILD COMPLETE'); + return path.resolve(runtimePath, '.'); + }, + setSkillManifest: async ( + dstRuntimePath: string, + dstStorage: IFileStorage, + srcManifestDir: string, + srcStorage: IFileStorage, + mode = 'azurewebapp' + ) => {}, + }); };