From e6536a5e61309db5c1cd64defbd82ae23d153eec Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Wed, 25 Sep 2019 14:53:45 -0700 Subject: [PATCH] Better error handling/loading of templates (#1509) --- extension.bundle.ts | 4 +- src/commands/addBinding/BindingListStep.ts | 17 +- src/commands/addBinding/addBinding.ts | 6 +- .../createFunction/FunctionListStep.ts | 14 +- .../dotnetSteps/DotnetFunctionCreateStep.ts | 2 +- .../javaSteps/JavaFunctionCreateStep.ts | 5 +- .../IScriptFunctionWizardContext.ts | 2 +- .../scriptSteps/ScriptFunctionCreateStep.ts | 2 +- .../scriptSteps/ScriptFunctionNameStep.ts | 2 +- .../DotnetProjectCreateStep.ts | 2 +- src/extension.ts | 4 +- src/extensionVariables.ts | 11 +- src/templates/CentralTemplateProvider.ts | 341 ++++++++---------- src/templates/ITemplates.ts | 12 + src/templates/TemplateProviderBase.ts | 75 +--- .../dotnet/DotnetTemplateProvider.ts | 49 +-- .../dotnet/executeDotnetTemplateCommand.ts | 8 +- src/templates/dotnet/parseDotnetTemplates.ts | 51 ++- src/templates/java/JavaTemplateProvider.ts | 80 ++-- .../script/ScriptTemplateProvider.ts | 53 +-- src/templates/script/parseScriptTemplates.ts | 21 +- src/utils/getCliFeedJson.ts | 12 +- src/utils/mavenUtils.ts | 20 +- .../verifyVSCodeConfigOnActivate.ts | 24 +- test/createFunction/FunctionTesterBase.ts | 7 +- test/getResourcesPath.test.ts | 6 +- test/global.test.ts | 64 ++-- test/templateCount.test.ts | 40 +- 28 files changed, 464 insertions(+), 470 deletions(-) create mode 100644 src/templates/ITemplates.ts diff --git a/extension.bundle.ts b/extension.bundle.ts index b14fa4b2..fa727f90 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -26,9 +26,9 @@ export * from './src/constants'; export * from './src/extensionVariables'; export * from './src/funcConfig/function'; export * from './src/vsCodeConfig/settings'; +export * from './src/templates/CentralTemplateProvider'; export * from './src/templates/IFunctionTemplate'; -export * from './src/templates/ScriptTemplateRetriever'; -export * from './src/templates/TemplateProvider'; +export * from './src/templates/script/getScriptResourcesPath'; export * from './src/tree/AzureAccountTreeItemWithProjects'; export * from './src/utils/fs'; export * from './src/utils/delay'; diff --git a/src/commands/addBinding/BindingListStep.ts b/src/commands/addBinding/BindingListStep.ts index bc4b6bf9..90261d0c 100644 --- a/src/commands/addBinding/BindingListStep.ts +++ b/src/commands/addBinding/BindingListStep.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AzureWizardPromptStep, IAzureQuickPickItem, IWizardOptions } from 'vscode-azureextensionui'; +import { ProjectLanguage, ProjectRuntime } from '../../constants'; import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; import { IBindingTemplate } from '../../templates/IBindingTemplate'; @@ -17,7 +18,7 @@ export class BindingListStep extends AzureWizardPromptStep { const direction: string = nonNullProp(context, 'bindingDirection'); const placeHolder: string = localize('selectBinding', 'Select binding with direction "{0}"', direction); - context.bindingTemplate = (await ext.ui.showQuickPick(this.getPicks(direction), { placeHolder })).data; + context.bindingTemplate = (await ext.ui.showQuickPick(this.getPicks(context, direction), { placeHolder })).data; } public shouldPrompt(context: IBindingWizardContext): boolean { @@ -35,13 +36,13 @@ export class BindingListStep extends AzureWizardPromptStep[]> { - // wait for template provider task to signal that bindings have been defined - await ext.templateProviderTask; - - const bindings: IBindingTemplate[] = ext.scriptBindings + private async getPicks(context: IBindingWizardContext, direction: string): Promise[]> { + const language: ProjectLanguage = nonNullProp(context, 'language'); + const runtime: ProjectRuntime = nonNullProp(context, 'runtime'); + const templates: IBindingTemplate[] = await ext.templateProvider.getBindingTemplates(context, language, runtime); + return templates .filter(b => b.direction.toLowerCase() === direction.toLowerCase()) - .sort((a, b) => a.displayName.localeCompare(b.displayName)); - return bindings.map(b => { return { label: b.displayName, data: b }; }); + .sort((a, b) => a.displayName.localeCompare(b.displayName)) + .map(b => { return { label: b.displayName, data: b }; }); } } diff --git a/src/commands/addBinding/addBinding.ts b/src/commands/addBinding/addBinding.ts index 57d52c0b..97858da4 100644 --- a/src/commands/addBinding/addBinding.ts +++ b/src/commands/addBinding/addBinding.ts @@ -5,9 +5,11 @@ import { Uri, WorkspaceFolder } from "vscode"; import { AzureWizard, IActionContext } from "vscode-azureextensionui"; +import { ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting } from "../../constants"; import { LocalBindingsTreeItem } from "../../tree/localProject/LocalBindingsTreeItem"; import { nonNullValue } from "../../utils/nonNull"; import { getContainingWorkspace } from "../../utils/workspace"; +import { getWorkspaceSetting } from "../../vsCodeConfig/settings"; import { createChildNode } from "../createChildNode"; import { tryGetFunctionProjectRoot } from "../createNewProject/verifyIsProject"; import { createBindingWizard } from "./createBindingWizard"; @@ -19,8 +21,10 @@ export async function addBinding(context: IActionContext, data: Uri | LocalBindi const workspaceFolder: WorkspaceFolder = nonNullValue(getContainingWorkspace(functionJsonPath), 'workspaceFolder'); const workspacePath: string = workspaceFolder.uri.fsPath; const projectPath: string | undefined = await tryGetFunctionProjectRoot(workspacePath) || workspacePath; + const language: ProjectLanguage | undefined = getWorkspaceSetting(projectLanguageSetting, projectPath); + const runtime: ProjectRuntime | undefined = getWorkspaceSetting(projectRuntimeSetting, projectPath); - const wizardContext: IBindingWizardContext = Object.assign(context, { functionJsonPath: data.fsPath, workspacePath, projectPath, workspaceFolder }); + const wizardContext: IBindingWizardContext = Object.assign(context, { functionJsonPath: data.fsPath, workspacePath, projectPath, workspaceFolder, language, runtime }); const wizard: AzureWizard = createBindingWizard(wizardContext); await wizard.prompt(); await wizard.execute(); diff --git a/src/commands/createFunction/FunctionListStep.ts b/src/commands/createFunction/FunctionListStep.ts index 0b84870b..30083adc 100644 --- a/src/commands/createFunction/FunctionListStep.ts +++ b/src/commands/createFunction/FunctionListStep.ts @@ -10,7 +10,6 @@ import { ext } from '../../extensionVariables'; import { getAzureWebJobsStorage } from '../../funcConfig/local.settings'; import { localize } from '../../localize'; import { IFunctionTemplate } from '../../templates/IFunctionTemplate'; -import { TemplateProvider } from '../../templates/TemplateProvider'; import { nonNullProp } from '../../utils/nonNull'; import { getWorkspaceSetting, updateWorkspaceSetting } from '../../vsCodeConfig/settings'; import { addBindingSettingSteps } from '../addBinding/settingSteps/addBindingSettingSteps'; @@ -44,8 +43,7 @@ export class FunctionListStep extends AzureWizardPromptStep t.id === options.templateId); if (foundTemplate) { context.functionTemplate = foundTemplate; @@ -143,12 +141,10 @@ export class FunctionListStep extends AzureWizardPromptStep[]> { const language: ProjectLanguage = nonNullProp(context, 'language'); const runtime: ProjectRuntime = nonNullProp(context, 'runtime'); - - const provider: TemplateProvider = await ext.templateProviderTask; - let templates: IFunctionTemplate[] = await provider.getTemplates(language, runtime, context.projectPath, templateFilter, context.telemetry.properties); - templates = templates.sort((a, b) => sortTemplates(a, b, templateFilter)); - - const picks: IAzureQuickPickItem[] = templates.map(t => { return { label: t.name, data: t }; }); + const templates: IFunctionTemplate[] = await ext.templateProvider.getFunctionTemplates(context, language, runtime, templateFilter); + const picks: IAzureQuickPickItem[] = templates + .sort((a, b) => sortTemplates(a, b, templateFilter)) + .map(t => { return { label: t.name, data: t }; }); if (this._isProjectWizard) { picks.unshift({ diff --git a/src/commands/createFunction/dotnetSteps/DotnetFunctionCreateStep.ts b/src/commands/createFunction/dotnetSteps/DotnetFunctionCreateStep.ts index e132eb5b..0103f539 100644 --- a/src/commands/createFunction/dotnetSteps/DotnetFunctionCreateStep.ts +++ b/src/commands/createFunction/dotnetSteps/DotnetFunctionCreateStep.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import { IActionContext } from 'vscode-azureextensionui'; import { ProjectRuntime } from '../../../constants'; -import { executeDotnetTemplateCommand } from '../../../templates/executeDotnetTemplateCommand'; +import { executeDotnetTemplateCommand } from '../../../templates/dotnet/executeDotnetTemplateCommand'; import { IFunctionTemplate } from '../../../templates/IFunctionTemplate'; import { cpUtils } from '../../../utils/cpUtils'; import { dotnetUtils } from '../../../utils/dotnetUtils'; diff --git a/src/commands/createFunction/javaSteps/JavaFunctionCreateStep.ts b/src/commands/createFunction/javaSteps/JavaFunctionCreateStep.ts index 02feac8f..c50788a8 100644 --- a/src/commands/createFunction/javaSteps/JavaFunctionCreateStep.ts +++ b/src/commands/createFunction/javaSteps/JavaFunctionCreateStep.ts @@ -6,7 +6,6 @@ import { IActionContext } from 'vscode-azureextensionui'; import { ext } from '../../../extensionVariables'; import { IFunctionTemplate } from '../../../templates/IFunctionTemplate'; -import { removeLanguageFromId } from "../../../templates/TemplateProvider"; import { mavenUtils } from "../../../utils/mavenUtils"; import { nonNullProp } from '../../../utils/nonNull'; import { getJavaFunctionFilePath, IJavaProjectWizardContext } from '../../createNewProject/javaSteps/IJavaProjectWizardContext'; @@ -52,3 +51,7 @@ export class JavaFunctionCreateStep extends FunctionCreateStepBase vscode.commands.executeCommand('azure-account.selectSubscriptions')); registerCommand('azureFunctions.refresh', async (_actionContext: IActionContext, node?: AzureTreeItem) => await ext.tree.refresh(node)); diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 1d9b8376..df3b8e5c 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -6,8 +6,7 @@ import { ExtensionContext } from "vscode"; import { AzExtTreeDataProvider, IAzExtOutputChannel, IAzureUserInput, ITelemetryReporter } from "vscode-azureextensionui"; import { func } from "./constants"; -import { IBindingTemplate } from "./templates/IBindingTemplate"; -import { TemplateProvider } from "./templates/TemplateProvider"; +import { CentralTemplateProvider } from "./templates/CentralTemplateProvider"; import { AzureAccountTreeItemWithProjects } from "./tree/AzureAccountTreeItemWithProjects"; /** @@ -19,17 +18,15 @@ export namespace ext { export let azureAccountTreeItem: AzureAccountTreeItemWithProjects; export let outputChannel: IAzExtOutputChannel; export let ui: IAzureUserInput; - export let templateProviderTask: Promise; + export let templateProvider: CentralTemplateProvider; export let reporter: ITelemetryReporter; export let funcCliPath: string = func; - export let templateSource: TemplateSource | undefined; - export let scriptBindings: IBindingTemplate[]; // tslint:disable-next-line: strict-boolean-expressions export let ignoreBundle: boolean = !/^(false|0)?$/i.test(process.env.AZCODE_FUNCTIONS_IGNORE_BUNDLE || ''); } export enum TemplateSource { Backup = 'Backup', - CliFeed = 'CliFeed', - StagingCliFeed = 'StagingCliFeed' + Latest = 'Latest', + Staging = 'Staging' } diff --git a/src/templates/CentralTemplateProvider.ts b/src/templates/CentralTemplateProvider.ts index e048929f..02de4ef5 100644 --- a/src/templates/CentralTemplateProvider.ts +++ b/src/templates/CentralTemplateProvider.ts @@ -3,208 +3,185 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; -import { callWithTelemetryAndErrorHandling, IActionContext, parseError, TelemetryProperties } from 'vscode-azureextensionui'; -import { ProjectLanguage, ProjectRuntime, TemplateFilter, templateVersionSetting } from '../constants'; +import { IActionContext, parseError } from 'vscode-azureextensionui'; +import { ProjectLanguage, ProjectRuntime, TemplateFilter } from '../constants'; import { ext, TemplateSource } from '../extensionVariables'; import { localize } from '../localize'; -import { dotnetUtils } from '../utils/dotnetUtils'; -import { cliFeedJsonResponse, getFeedRuntime, tryGetCliFeedJson } from '../utils/getCliFeedJson'; -import { getWorkspaceSetting, updateGlobalSetting } from '../vsCodeConfig/settings'; -import { DotnetTemplateRetriever, getDotnetVerifiedTemplateIds } from './DotnetTemplateRetriever'; -import { IBindingSetting } from './IBindingTemplate'; +import { cliFeedJsonResponse, getCliFeedJson, getFeedRuntime } from '../utils/getCliFeedJson'; +import { DotnetTemplateProvider } from './dotnet/DotnetTemplateProvider'; +import { getDotnetVerifiedTemplateIds } from './dotnet/getDotnetVerifiedTemplateIds'; +import { IBindingTemplate } from './IBindingTemplate'; import { IFunctionTemplate, TemplateCategory } from './IFunctionTemplate'; -import { parseJavaTemplates } from './parseJavaTemplates'; -import { getScriptVerifiedTemplateIds, ScriptTemplateRetriever } from './ScriptTemplateRetriever'; -import { TemplateRetriever } from './TemplateRetriever'; +import { ITemplates } from './ITemplates'; +import { JavaTemplateProvider } from './java/JavaTemplateProvider'; +import { getScriptVerifiedTemplateIds } from './script/getScriptVerifiedTemplateIds'; +import { ScriptTemplateProvider } from './script/ScriptTemplateProvider'; +import { TemplateProviderBase } from './TemplateProviderBase'; -export class TemplateProvider { - private readonly _templatesMap: { [runtime: string]: IFunctionTemplate[] | undefined } = {}; - // if there are no templates, then there is likely no internet or a problem with the clifeed url - private readonly _noInternetErrMsg: string = localize('retryInternet', 'There was an error in retrieving the templates. Recheck your internet connection and try again.'); - private _javaTemplates: IFunctionTemplate[] | undefined; +export class CentralTemplateProvider { + public readonly templateSource: TemplateSource | undefined; + private readonly _templatesTaskMap: { [key: string]: Promise | undefined } = {}; - constructor(templatesMap: { [runtime: string]: IFunctionTemplate[] | undefined }) { - this._templatesMap = templatesMap; - this.copyCSharpSettingsFromJS(); + public constructor(templateSource?: TemplateSource) { + this.templateSource = templateSource; } - public async getTemplates(language: string, runtime: string, functionAppPath: string, templateFilter?: string, telemetryProperties?: TelemetryProperties): Promise { - const templates: IFunctionTemplate[] | undefined = this._templatesMap[runtime]; - if (!templates) { - throw new Error(this._noInternetErrMsg); + public async getFunctionTemplates(context: IActionContext, language: ProjectLanguage, runtime: ProjectRuntime, templateFilter?: TemplateFilter): Promise { + const templates: ITemplates = await this.getTemplates(context, language, runtime); + const functionTemplates: IFunctionTemplate[] = templates.functionTemplates.filter((t: IFunctionTemplate) => t.language.toLowerCase() === language.toLowerCase()); + switch (templateFilter) { + case TemplateFilter.All: + return functionTemplates; + case TemplateFilter.Core: + return functionTemplates.filter((t: IFunctionTemplate) => t.categories.find((c: TemplateCategory) => c === TemplateCategory.Core) !== undefined); + case TemplateFilter.Verified: + default: + const verifiedTemplateIds: string[] = getScriptVerifiedTemplateIds(runtime).concat(getDotnetVerifiedTemplateIds(runtime)); + return functionTemplates.filter((t: IFunctionTemplate) => verifiedTemplateIds.find((vt: string) => vt === t.id)); } + } - if (language === ProjectLanguage.Java) { - if (!this._javaTemplates) { - this._javaTemplates = await parseJavaTemplates(templates, functionAppPath, telemetryProperties); - } - return this._javaTemplates; + public async getBindingTemplates(context: IActionContext, language: ProjectLanguage, runtime: ProjectRuntime): Promise { + const templates: ITemplates = await this.getTemplates(context, language, runtime); + if (!templates.bindingTemplates) { + throw new Error(localize('bindingTemplatesError', 'Binding templates are not supported for language "{0}" and runtime "{1}"', language, runtime)); } else { - let filterTemplates: IFunctionTemplate[] = templates.filter((t: IFunctionTemplate) => t.language.toLowerCase() === language.toLowerCase()); - switch (templateFilter) { - case TemplateFilter.All: - break; - case TemplateFilter.Core: - filterTemplates = filterTemplates.filter((t: IFunctionTemplate) => t.categories.find((c: TemplateCategory) => c === TemplateCategory.Core) !== undefined); - break; - case TemplateFilter.Verified: - default: - const verifiedTemplateIds: string[] = getScriptVerifiedTemplateIds(runtime).concat(getDotnetVerifiedTemplateIds(runtime)); - filterTemplates = filterTemplates.filter((t: IFunctionTemplate) => verifiedTemplateIds.find((vt: string) => vt === t.id)); - } - - return filterTemplates; + return templates.bindingTemplates; } } /** - * The dotnet templates do not provide the validation and resourceType information that we desire - * As a workaround, we can check for the exact same JavaScript template/setting and leverage that information + * Ensures we only have one task going at a time for refreshing templates */ - private copyCSharpSettingsFromJS(): void { - for (const key of Object.keys(this._templatesMap)) { - const templates: IFunctionTemplate[] | undefined = this._templatesMap[key]; - if (templates) { - const jsTemplates: IFunctionTemplate[] = templates.filter((t: IFunctionTemplate) => t.language.toLowerCase() === ProjectLanguage.JavaScript.toLowerCase()); - const csharpTemplates: IFunctionTemplate[] = templates.filter((t: IFunctionTemplate) => t.language.toLowerCase() === ProjectLanguage.CSharp.toLowerCase()); - for (const csharpTemplate of csharpTemplates) { - const jsTemplate: IFunctionTemplate | undefined = jsTemplates.find((t: IFunctionTemplate) => normalizeId(t.id) === normalizeId(csharpTemplate.id)); - if (jsTemplate) { - for (const cSharpSetting of csharpTemplate.userPromptedSettings) { - const jsSetting: IBindingSetting | undefined = jsTemplate.userPromptedSettings.find((t: IBindingSetting) => normalizeName(t.name) === normalizeName(cSharpSetting.name)); - if (jsSetting) { - cSharpSetting.resourceType = jsSetting.resourceType; - cSharpSetting.validateSetting = jsSetting.validateSetting; - } - } - } - } - } + private async getTemplates(context: IActionContext, language: ProjectLanguage, runtime: ProjectRuntime): Promise { + let provider: TemplateProviderBase; + switch (language) { + case ProjectLanguage.CSharp: + case ProjectLanguage.FSharp: + provider = new DotnetTemplateProvider(runtime); + break; + case ProjectLanguage.Java: + provider = new JavaTemplateProvider(runtime); + break; + default: + provider = new ScriptTemplateProvider(runtime); + break; } - } -} -/** - * Converts ids like "Azure.Function.CSharp.QueueTrigger.2.x" or "QueueTrigger-JavaScript" to "queuetrigger" - */ -function normalizeId(id: string): string { - const match: RegExpMatchArray | null = id.match(/[a-z]+Trigger/i); - return normalizeName(match ? match[0] : id); -} - -function normalizeName(name: string): string { - return name.toLowerCase().replace(/\s/g, ''); -} - -export async function getTemplateProvider(): Promise { - const templatesMap: { [runtime: string]: IFunctionTemplate[] | undefined } = {}; - const cliFeedJson: cliFeedJsonResponse | undefined = await tryGetCliFeedJson(); - - const templateRetrievers: TemplateRetriever[] = [new ScriptTemplateRetriever()]; - if (await dotnetUtils.isDotnetInstalled()) { - templateRetrievers.push(new DotnetTemplateRetriever()); - } - - for (const templateRetriever of templateRetrievers) { - for (const key of Object.keys(ProjectRuntime)) { - const runtime: ProjectRuntime = ProjectRuntime[key]; - - await callWithTelemetryAndErrorHandling('azureFunctions.getFunctionTemplates', async (context: IActionContext) => { - context.errorHandling.suppressDisplay = true; - context.telemetry.properties.isActivationEvent = 'true'; - context.telemetry.properties.runtime = runtime; - context.telemetry.properties.templateType = templateRetriever.templateType; - const templateVersion: string | undefined = await tryGetTemplateVersionSetting(context, cliFeedJson, runtime); - let templates: IFunctionTemplate[] | undefined; - - // 1. Use the cached templates if they match templateVersion - // tslint:disable-next-line:strict-boolean-expressions - if (!ext.templateSource && ext.context.globalState.get(templateRetriever.getCacheKey(TemplateRetriever.templateVersionKey, runtime)) === templateVersion) { - templates = await templateRetriever.tryGetTemplatesFromCache(context, runtime); - context.telemetry.properties.templateSource = 'matchingCache'; - } - - // 2. Download templates from the cli-feed if the cache doesn't match templateVersion - // tslint:disable-next-line:strict-boolean-expressions - if ((!ext.templateSource || ext.templateSource === TemplateSource.CliFeed || ext.templateSource === TemplateSource.StagingCliFeed) && !templates && cliFeedJson && templateVersion) { - templates = await templateRetriever.tryGetTemplatesFromCliFeed(context, cliFeedJson, templateVersion, runtime); - context.telemetry.properties.templateSource = 'cliFeed'; - } - - // 3. Use the cached templates, even if they don't match templateVersion - // tslint:disable-next-line:strict-boolean-expressions - if (!ext.templateSource && !templates) { - templates = await templateRetriever.tryGetTemplatesFromCache(context, runtime); - context.telemetry.properties.templateSource = 'mismatchCache'; - } - - // 4. Use backup templates shipped with the extension - // tslint:disable-next-line:strict-boolean-expressions - if ((!ext.templateSource || ext.templateSource === TemplateSource.Backup) && !templates) { - templates = await templateRetriever.tryGetTemplatesFromBackup(context, runtime); - context.telemetry.properties.templateSource = 'backupFromExtension'; - } - - if (templates) { - // tslint:disable-next-line:strict-boolean-expressions - templatesMap[runtime] = (templatesMap[runtime] || []).concat(templates); - } else { - // Failed to get templates for this runtime - context.telemetry.properties.templateSource = 'None'; - } - }); - } - } - - return new TemplateProvider(templatesMap); -} - -export function removeLanguageFromId(id: string): string { - return id.split('-')[0]; -} - -async function tryGetTemplateVersionSetting(context: IActionContext, cliFeedJson: cliFeedJsonResponse | undefined, runtime: ProjectRuntime): Promise { - const feedRuntime: string = getFeedRuntime(runtime); - const userTemplateVersion: string | undefined = getWorkspaceSetting(templateVersionSetting); - try { - if (userTemplateVersion) { - context.telemetry.properties.userTemplateVersion = userTemplateVersion; - } - let templateVersion: string; - if (cliFeedJson) { - templateVersion = userTemplateVersion ? userTemplateVersion : cliFeedJson.tags[feedRuntime].release; - // tslint:disable-next-line:strict-boolean-expressions - if (!cliFeedJson.releases[templateVersion]) { - const invalidVersion: string = localize('invalidTemplateVersion', 'Failed to retrieve Azure Functions templates for version "{0}".', templateVersion); - const selectVersion: vscode.MessageItem = { title: localize('selectVersion', 'Select version') }; - const useLatest: vscode.MessageItem = { title: localize('useLatest', 'Use latest') }; - const warningInput: vscode.MessageItem = await ext.ui.showWarningMessage(invalidVersion, selectVersion, useLatest); - if (warningInput === selectVersion) { - const releaseQuickPicks: vscode.QuickPickItem[] = []; - for (const rel of Object.keys(cliFeedJson.releases)) { - releaseQuickPicks.push({ - label: rel, - description: '' - }); - } - const input: vscode.QuickPickItem | undefined = await ext.ui.showQuickPick(releaseQuickPicks, { placeHolder: invalidVersion }); - templateVersion = input.label; - await updateGlobalSetting(templateVersionSetting, input.label); - } else { - templateVersion = cliFeedJson.tags[feedRuntime].release; - // reset user setting so that it always gets latest - await updateGlobalSetting(templateVersionSetting, ''); - } - } + const key: string = provider.templateType + provider.runtime; + let templatesTask: Promise | undefined = this._templatesTaskMap[key]; + if (templatesTask) { + return await templatesTask; } else { - return undefined; + templatesTask = this.refreshTemplates(context, provider); + this._templatesTaskMap[key] = templatesTask; + try { + return await templatesTask; + } catch (error) { + // If an error occurs, we want to start from scratch next time we try to get templates so remove this task from the map + delete this._templatesTaskMap[key]; + throw error; + } + } + } + + private async refreshTemplates(context: IActionContext, provider: TemplateProviderBase): Promise { + context.telemetry.properties.runtime = provider.runtime; + context.telemetry.properties.templateType = provider.templateType; + + let result: ITemplates | undefined; + let latestTemplatesError: unknown; + try { + const cliFeedJson: cliFeedJsonResponse = await getCliFeedJson(); + const feedRuntime: string = getFeedRuntime(provider.runtime); + const templateVersion: string = cliFeedJson.tags[feedRuntime].release; + context.telemetry.properties.templateVersion = templateVersion; + const cachedTemplateVersion: string | undefined = ext.context.globalState.get(provider.getCacheKey(TemplateProviderBase.templateVersionKey)); + context.telemetry.properties.cachedTemplateVersion = cachedTemplateVersion; + + // 1. Use the cached templates if they match templateVersion + if (cachedTemplateVersion === templateVersion) { + result = await this.tryGetCachedTemplates(context, provider); + } + + // 2. Download templates from the cli-feed if the cache doesn't match templateVersion + if (!result) { + result = await this.getLatestTemplates(context, provider, cliFeedJson, templateVersion); + } + } catch (error) { + // This error should be the most actionable to the user, so save it and throw later if cache/backup doesn't work + latestTemplatesError = error; + const errorMessage: string = parseError(error).message; + ext.outputChannel.appendLog(localize('latestTemplatesError', 'Failed to get latest templates: {0}', errorMessage)); + context.telemetry.properties.latestTemplatesError = errorMessage; + } + + // 3. Use the cached templates, even if they don't match templateVersion + if (!result) { + result = await this.tryGetCachedTemplates(context, provider); + } + + // 4. Use backup templates shipped with the extension + if (!result) { + result = await this.tryGetBackupTemplates(context, provider); + } + + if (result) { + return result; + } else if (latestTemplatesError !== undefined) { + throw latestTemplatesError; + } else { + // This should only happen for dev/test scenarios where we explicitly set templateSource + throw new Error(localize('templateSourceError', 'Internal error: Failed to get templates for source "{0}".', this.templateSource)); + } + } + + private async getLatestTemplates(context: IActionContext, provider: TemplateProviderBase, cliFeedJson: cliFeedJsonResponse, templateVersion: string): Promise { + // tslint:disable-next-line:strict-boolean-expressions + if (!this.templateSource || this.templateSource === TemplateSource.Latest || this.templateSource === TemplateSource.Staging) { + context.telemetry.properties.templateSource = 'latest'; + const result: ITemplates = await provider.getLatestTemplates(cliFeedJson, templateVersion, context); + ext.context.globalState.update(provider.getCacheKey(TemplateProviderBase.templateVersionKey), templateVersion); + await provider.cacheTemplates(); + return result; + } + + return undefined; + } + + private async tryGetCachedTemplates(context: IActionContext, provider: TemplateProviderBase): Promise { + // tslint:disable-next-line:strict-boolean-expressions + if (!this.templateSource) { + try { + context.telemetry.properties.templateSource = 'cache'; + return await provider.getCachedTemplates(); + } catch (error) { + const errorMessage: string = parseError(error).message; + ext.outputChannel.appendLog(localize('cachedTemplatesError', 'Failed to get cached templates: {0}', errorMessage)); + context.telemetry.properties.cachedTemplatesError = errorMessage; + } + } + + return undefined; + } + + private async tryGetBackupTemplates(context: IActionContext, provider: TemplateProviderBase): Promise { + // tslint:disable-next-line:strict-boolean-expressions + if (!this.templateSource || this.templateSource === TemplateSource.Backup) { + try { + context.telemetry.properties.templateSource = 'backup'; + const backupTemplateVersion: string = provider.getBackupVersion(); + const result: ITemplates = await provider.getBackupTemplates(); + ext.context.globalState.update(provider.getCacheKey(TemplateProviderBase.templateVersionKey), backupTemplateVersion); + await provider.cacheTemplates(); + return result; + } catch (error) { + const errorMessage: string = parseError(error).message; + ext.outputChannel.appendLog(localize('backupTemplatesError', 'Failed to get backup templates: {0}', errorMessage)); + context.telemetry.properties.backupTemplatesError = errorMessage; + } } - return templateVersion; - } catch (error) { - // if cliJson does not have the template version being searched for, it will throw an error - context.telemetry.properties.userTemplateVersion = parseError(error).message; return undefined; } } diff --git a/src/templates/ITemplates.ts b/src/templates/ITemplates.ts new file mode 100644 index 00000000..3c9771e3 --- /dev/null +++ b/src/templates/ITemplates.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IBindingTemplate } from "./IBindingTemplate"; +import { IFunctionTemplate } from "./IFunctionTemplate"; + +export interface ITemplates { + functionTemplates: IFunctionTemplate[]; + bindingTemplates?: IBindingTemplate[]; +} diff --git a/src/templates/TemplateProviderBase.ts b/src/templates/TemplateProviderBase.ts index 61bdf36f..9afcf1f7 100644 --- a/src/templates/TemplateProviderBase.ts +++ b/src/templates/TemplateProviderBase.ts @@ -4,76 +4,37 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { IActionContext, parseError } from 'vscode-azureextensionui'; +import { IActionContext } from 'vscode-azureextensionui'; import { ProjectRuntime } from '../constants'; -import { ext } from '../extensionVariables'; import { localize } from '../localize'; import { cliFeedJsonResponse } from '../utils/getCliFeedJson'; -import { IFunctionTemplate } from './IFunctionTemplate'; +import { ITemplates } from './ITemplates'; const v2BackupTemplatesVersion: string = '2.18.1'; const v1BackupTemplatesVersion: string = '1.8.0'; export enum TemplateType { Script = 'Script', - Dotnet = '.NET' + Dotnet = '.NET', + Java = 'Java' } -export abstract class TemplateRetriever { +export abstract class TemplateProviderBase { public static templateVersionKey: string = 'templateVersion'; public abstract templateType: TemplateType; + public readonly runtime: ProjectRuntime; - public async tryGetTemplatesFromCache(context: IActionContext, runtime: ProjectRuntime): Promise { - try { - return await this.getTemplatesFromCache(runtime); - } catch (error) { - const errorMessage: string = parseError(error).message; - ext.outputChannel.appendLog(errorMessage); - context.telemetry.properties.cacheError = errorMessage; - return undefined; - } - } - - public async tryGetTemplatesFromCliFeed(context: IActionContext, cliFeedJson: cliFeedJsonResponse, templateVersion: string, runtime: ProjectRuntime): Promise { - try { - context.telemetry.properties.templateVersion = templateVersion; - ext.outputChannel.appendLog(localize('updatingTemplates', 'Updating {0} templates for runtime "{1}" to version "{2}"...', this.templateType, runtime, templateVersion)); - const templates: IFunctionTemplate[] = await this.getTemplatesFromCliFeed(cliFeedJson, templateVersion, runtime, context); - ext.context.globalState.update(this.getCacheKey(TemplateRetriever.templateVersionKey, runtime), templateVersion); - await this.cacheTemplates(runtime); - ext.outputChannel.appendLog(localize('updatedTemplates', 'Successfully updated templates.')); - return templates; - } catch (error) { - const errorMessage: string = parseError(error).message; - ext.outputChannel.appendLog(errorMessage); - context.telemetry.properties.cliFeedError = errorMessage; - return undefined; - } - } - - public async tryGetTemplatesFromBackup(context: IActionContext, runtime: ProjectRuntime): Promise { - try { - const backupTemplateVersion: string = this.getBackupVersion(runtime); - const templates: IFunctionTemplate[] = await this.getTemplatesFromBackup(runtime); - ext.context.globalState.update(this.getCacheKey(TemplateRetriever.templateVersionKey, runtime), backupTemplateVersion); - await this.cacheTemplates(runtime); - ext.outputChannel.appendLog(localize('usingBackupTemplates', 'Falling back to version "{0}" for {1} templates for runtime "{2}".', backupTemplateVersion, this.templateType, runtime)); - return templates; - } catch (error) { - const errorMessage: string = parseError(error).message; - ext.outputChannel.appendLog(errorMessage); - context.telemetry.properties.backupError = errorMessage; - return undefined; - } + public constructor(runtime: ProjectRuntime) { + this.runtime = runtime; } /** * Adds runtime, templateType, and language information to a key to ensure there are no collisions in the cache * For backwards compatability, the original runtime, templateType, and language will not have this information */ - public getCacheKey(key: string, runtime: ProjectRuntime): string { - if (runtime !== ProjectRuntime.v1) { - key = `${key}.${runtime}`; + public getCacheKey(key: string): string { + if (this.runtime !== ProjectRuntime.v1) { + key = `${key}.${this.runtime}`; } if (this.templateType !== TemplateType.Script) { @@ -87,19 +48,19 @@ export abstract class TemplateRetriever { return key; } - protected abstract getTemplatesFromCache(runtime: ProjectRuntime): Promise; - protected abstract getTemplatesFromCliFeed(cliFeedJson: cliFeedJsonResponse, templateVersion: string, runtime: ProjectRuntime, context: IActionContext): Promise; - protected abstract getTemplatesFromBackup(runtime: ProjectRuntime): Promise; - protected abstract cacheTemplates(runtime: ProjectRuntime): Promise; + public abstract getCachedTemplates(): Promise; + public abstract getLatestTemplates(cliFeedJson: cliFeedJsonResponse, templateVersion: string, context: IActionContext): Promise; + public abstract getBackupTemplates(): Promise; + public abstract cacheTemplates(): Promise; - protected getBackupVersion(runtime: ProjectRuntime): string { - switch (runtime) { + public getBackupVersion(): string { + switch (this.runtime) { case ProjectRuntime.v1: return v1BackupTemplatesVersion; case ProjectRuntime.v2: return v2BackupTemplatesVersion; default: - throw new RangeError(localize('invalidRuntime', 'Invalid runtime "{0}".', runtime)); + throw new RangeError(localize('invalidRuntime', 'Invalid runtime "{0}".', this.runtime)); } } } diff --git a/src/templates/dotnet/DotnetTemplateProvider.ts b/src/templates/dotnet/DotnetTemplateProvider.ts index d33df568..7d6acfb2 100644 --- a/src/templates/dotnet/DotnetTemplateProvider.ts +++ b/src/templates/dotnet/DotnetTemplateProvider.ts @@ -6,56 +6,59 @@ import * as fse from 'fs-extra'; import * as path from 'path'; import { IActionContext } from 'vscode-azureextensionui'; -import { ProjectRuntime } from '../constants'; -import { ext } from '../extensionVariables'; -import { downloadFile } from '../utils/fs'; -import { cliFeedJsonResponse } from '../utils/getCliFeedJson'; +import { ext } from '../../extensionVariables'; +import { dotnetUtils } from '../../utils/dotnetUtils'; +import { downloadFile } from '../../utils/fs'; +import { cliFeedJsonResponse } from '../../utils/getCliFeedJson'; +import { IFunctionTemplate } from '../IFunctionTemplate'; +import { ITemplates } from '../ITemplates'; +import { TemplateProviderBase, TemplateType } from '../TemplateProviderBase'; import { executeDotnetTemplateCommand, getDotnetItemTemplatePath, getDotnetProjectTemplatePath, getDotnetTemplatesPath } from './executeDotnetTemplateCommand'; -import { IFunctionTemplate } from './IFunctionTemplate'; import { parseDotnetTemplates } from './parseDotnetTemplates'; -import { TemplateRetriever, TemplateType } from './TemplateRetriever'; -export class DotnetTemplateRetriever extends TemplateRetriever { +export class DotnetTemplateProvider extends TemplateProviderBase { public templateType: TemplateType = TemplateType.Dotnet; private readonly _dotnetTemplatesKey: string = 'DotnetTemplates'; private _rawTemplates: object[]; - protected async getTemplatesFromCache(runtime: ProjectRuntime): Promise { - const projectFilePath: string = getDotnetProjectTemplatePath(runtime); - const itemFilePath: string = getDotnetItemTemplatePath(runtime); + public async getCachedTemplates(): Promise { + const projectFilePath: string = getDotnetProjectTemplatePath(this.runtime); + const itemFilePath: string = getDotnetItemTemplatePath(this.runtime); if (!await fse.pathExists(projectFilePath) || !await fse.pathExists(itemFilePath)) { return undefined; } - const cachedDotnetTemplates: object[] | undefined = ext.context.globalState.get(this.getCacheKey(this._dotnetTemplatesKey, runtime)); + const cachedDotnetTemplates: object[] | undefined = ext.context.globalState.get(this.getCacheKey(this._dotnetTemplatesKey)); if (cachedDotnetTemplates) { - return parseDotnetTemplates(cachedDotnetTemplates, runtime); + return { functionTemplates: await parseDotnetTemplates(cachedDotnetTemplates, this.runtime) }; } else { return undefined; } } - protected async getTemplatesFromCliFeed(cliFeedJson: cliFeedJsonResponse, templateVersion: string, runtime: ProjectRuntime, _context: IActionContext): Promise { - const projectFilePath: string = getDotnetProjectTemplatePath(runtime); + public async getLatestTemplates(cliFeedJson: cliFeedJsonResponse, templateVersion: string, context: IActionContext): Promise { + await dotnetUtils.validateDotnetInstalled(context); + + const projectFilePath: string = getDotnetProjectTemplatePath(this.runtime); await downloadFile(cliFeedJson.releases[templateVersion].projectTemplates, projectFilePath); - const itemFilePath: string = getDotnetItemTemplatePath(runtime); + const itemFilePath: string = getDotnetItemTemplatePath(this.runtime); await downloadFile(cliFeedJson.releases[templateVersion].itemTemplates, itemFilePath); - return await this.parseTemplates(runtime); + return { functionTemplates: await this.parseTemplates() }; } - protected async getTemplatesFromBackup(runtime: ProjectRuntime): Promise { + public async getBackupTemplates(): Promise { await fse.copy(ext.context.asAbsolutePath(path.join('resources', 'backupDotnetTemplates')), getDotnetTemplatesPath(), { overwrite: true, recursive: false }); - return await this.parseTemplates(runtime); + return { functionTemplates: await this.parseTemplates() }; } - protected async cacheTemplates(runtime: ProjectRuntime): Promise { - ext.context.globalState.update(this.getCacheKey(this._dotnetTemplatesKey, runtime), this._rawTemplates); + public async cacheTemplates(): Promise { + ext.context.globalState.update(this.getCacheKey(this._dotnetTemplatesKey), this._rawTemplates); } - private async parseTemplates(runtime: ProjectRuntime): Promise { - this._rawTemplates = JSON.parse(await executeDotnetTemplateCommand(runtime, undefined, 'list')); - return parseDotnetTemplates(this._rawTemplates, runtime); + private async parseTemplates(): Promise { + this._rawTemplates = JSON.parse(await executeDotnetTemplateCommand(this.runtime, undefined, 'list')); + return parseDotnetTemplates(this._rawTemplates, this.runtime); } } diff --git a/src/templates/dotnet/executeDotnetTemplateCommand.ts b/src/templates/dotnet/executeDotnetTemplateCommand.ts index 576f1600..4ddc4504 100644 --- a/src/templates/dotnet/executeDotnetTemplateCommand.ts +++ b/src/templates/dotnet/executeDotnetTemplateCommand.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import { ProjectRuntime } from '../constants'; -import { ext } from "../extensionVariables"; -import { cpUtils } from "../utils/cpUtils"; +import { ProjectRuntime } from '../../constants'; +import { ext } from "../../extensionVariables"; +import { cpUtils } from "../../utils/cpUtils"; export async function executeDotnetTemplateCommand(runtime: ProjectRuntime, workingDirectory: string | undefined, operation: 'list' | 'create', ...args: string[]): Promise { const jsonDllPath: string = ext.context.asAbsolutePath(path.join('resources', 'dotnetJsonCli', 'Microsoft.TemplateEngine.JsonCli.dll')); @@ -26,7 +26,7 @@ export async function executeDotnetTemplateCommand(runtime: ProjectRuntime, work export function getDotnetTemplatesPath(): string { // tslint:disable-next-line:strict-boolean-expressions - return path.join(ext.context.globalStoragePath, 'dotnetTemplates', ext.templateSource || ''); + return path.join(ext.context.globalStoragePath, 'dotnetTemplates', ext.templateProvider.templateSource || ''); } export function getDotnetItemTemplatePath(runtime: ProjectRuntime): string { diff --git a/src/templates/dotnet/parseDotnetTemplates.ts b/src/templates/dotnet/parseDotnetTemplates.ts index f6136e9f..c07f723c 100644 --- a/src/templates/dotnet/parseDotnetTemplates.ts +++ b/src/templates/dotnet/parseDotnetTemplates.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ProjectLanguage, ProjectRuntime } from '../constants'; -import { IBindingSetting, ValueType } from './IBindingTemplate'; -import { IFunctionTemplate, TemplateCategory } from './IFunctionTemplate'; +import { callWithTelemetryAndErrorHandling, IActionContext } from 'vscode-azureextensionui'; +import { ProjectLanguage, ProjectRuntime } from '../../constants'; +import { ext } from '../../extensionVariables'; +import { IBindingSetting, ValueType } from '../IBindingTemplate'; +import { IFunctionTemplate, TemplateCategory } from '../IFunctionTemplate'; /** * Describes a dotnet template before it has been parsed @@ -70,7 +72,7 @@ function parseDotnetTemplate(rawTemplate: IRawTemplate): IFunctionTemplate { * Parses templates used by the .NET CLI * This basically converts the 'raw' templates in the externally defined JSON format to a common and understood format (IFunctionTemplate) used by this extension */ -export function parseDotnetTemplates(rawTemplates: object[], runtime: ProjectRuntime): IFunctionTemplate[] { +export async function parseDotnetTemplates(rawTemplates: object[], runtime: ProjectRuntime): Promise { const templates: IFunctionTemplate[] = []; for (const rawTemplate of rawTemplates) { try { @@ -83,5 +85,46 @@ export function parseDotnetTemplates(rawTemplates: object[], runtime: ProjectRun // Ignore errors so that a single poorly formed template does not affect other templates } } + + await copyCSharpSettingsFromJS(templates, runtime); + return templates; } + +/** + * The dotnet templates do not provide the validation and resourceType information that we desire + * As a workaround, we can check for the exact same JavaScript template/setting and leverage that information + */ +async function copyCSharpSettingsFromJS(csharpTemplates: IFunctionTemplate[], runtime: ProjectRuntime): Promise { + // Use separate telemetry event since we don't want to overwrite C# telemetry with JS telemetry + await callWithTelemetryAndErrorHandling('copyCSharpSettingsFromJS', async (jsContext: IActionContext) => { + jsContext.errorHandling.suppressDisplay = true; + jsContext.telemetry.properties.isActivationEvent = 'true'; + + const jsTemplates: IFunctionTemplate[] = await ext.templateProvider.getFunctionTemplates(jsContext, ProjectLanguage.JavaScript, runtime); + for (const csharpTemplate of csharpTemplates) { + const jsTemplate: IFunctionTemplate | undefined = jsTemplates.find((t: IFunctionTemplate) => normalizeId(t.id) === normalizeId(csharpTemplate.id)); + if (jsTemplate) { + for (const cSharpSetting of csharpTemplate.userPromptedSettings) { + const jsSetting: IBindingSetting | undefined = jsTemplate.userPromptedSettings.find((t: IBindingSetting) => normalizeName(t.name) === normalizeName(cSharpSetting.name)); + if (jsSetting) { + cSharpSetting.resourceType = jsSetting.resourceType; + cSharpSetting.validateSetting = jsSetting.validateSetting; + } + } + } + } + }); +} + +/** + * Converts ids like "Azure.Function.CSharp.QueueTrigger.2.x" or "QueueTrigger-JavaScript" to "queuetrigger" + */ +function normalizeId(id: string): string { + const match: RegExpMatchArray | null = id.match(/[a-z]+Trigger/i); + return normalizeName(match ? match[0] : id); +} + +function normalizeName(name: string): string { + return name.toLowerCase().replace(/\s/g, ''); +} diff --git a/src/templates/java/JavaTemplateProvider.ts b/src/templates/java/JavaTemplateProvider.ts index bafd704e..a69ac52b 100644 --- a/src/templates/java/JavaTemplateProvider.ts +++ b/src/templates/java/JavaTemplateProvider.ts @@ -3,72 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { parseError, TelemetryProperties } from 'vscode-azureextensionui'; -import { ProjectLanguage } from '../constants'; -import { mavenUtils } from "../utils/mavenUtils"; -import { IBindingTemplate } from './IBindingTemplate'; -import { IFunctionTemplate } from './IFunctionTemplate'; -import { IConfig, IRawTemplate, IResources, parseScriptBindings, parseScriptTemplate as parseJavaTemplate } from './parseScriptTemplates'; -import { removeLanguageFromId } from "./TemplateProvider"; +import { IActionContext } from 'vscode-azureextensionui'; +import { localize } from '../../localize'; +import { cliFeedJsonResponse } from '../../utils/getCliFeedJson'; +import { mavenUtils } from '../../utils/mavenUtils'; +import { ITemplates } from '../ITemplates'; +import { parseScriptTemplates } from '../script/parseScriptTemplates'; +import { ScriptTemplateProvider } from '../script/ScriptTemplateProvider'; +import { TemplateType } from '../TemplateProviderBase'; /** * Describes templates output before it has been parsed */ interface IRawJavaTemplates { - templates: IRawTemplate[]; + templates: object[]; } -const backupJavaTemplateNames: string[] = [ - 'HttpTrigger', - 'BlobTrigger', - 'QueueTrigger', - 'TimerTrigger' -]; - /** - * Parses templates contained in the output of 'mvn azure-functions:list'. - * This basically converts the 'raw' templates in the externally defined JSON format to a common and understood format (IFunctionTemplate) used by this extension + * Java templates largely follow the same formatting as script templates, but they come from maven */ -export async function parseJavaTemplates(allTemplates: IFunctionTemplate[], functionAppPath: string, telemetryProperties?: TelemetryProperties): Promise { - let embeddedTemplates: IRawJavaTemplates = { templates: [] }; - let embeddedConfig: IConfig | undefined; - let embeddedResources: object = {}; - try { - // Try to get the templates information by calling 'mvn azure-functions:list'. - const commandResult: string = await mavenUtils.executeMvnCommand(telemetryProperties, undefined, functionAppPath, 'azure-functions:list'); +export class JavaTemplateProvider extends ScriptTemplateProvider { + public templateType: TemplateType = TemplateType.Java; + protected readonly _templatesKey: string = 'JavaFunctionTemplates'; + protected readonly _configKey: string = 'JavaFunctionTemplateConfig'; + protected readonly _resourcesKey: string = 'JavaFunctionTemplateResources'; + + public async getLatestTemplates(_cliFeedJson: cliFeedJsonResponse, _templateVersion: string, context: IActionContext): Promise { + await mavenUtils.validateMavenInstalled(context); + + const commandResult: string = await mavenUtils.executeMvnCommand(context.telemetry.properties, undefined, undefined, 'azure-functions:list'); const regExp: RegExp = />> templates begin <<([\S\s]+)^.+INFO.+ >> templates end <<$[\S\s]+>> bindings begin <<([\S\s]+)^.+INFO.+ >> bindings end <<$[\S\s]+>> resources begin <<([\S\s]+)^.+INFO.+ >> resources end <<$/gm; const regExpResult: RegExpExecArray | null = regExp.exec(commandResult); if (regExpResult && regExpResult.length > 3) { - embeddedTemplates = JSON.parse(regExpResult[1]); - embeddedConfig = JSON.parse(regExpResult[2]); - embeddedResources = JSON.parse(regExpResult[3]); + this._rawTemplates = (JSON.parse(regExpResult[1])).templates; + this._rawConfig = JSON.parse(regExpResult[2]); + this._rawResources = JSON.parse(regExpResult[3]); + return parseScriptTemplates(this._rawResources, this._rawTemplates, this._rawConfig); + } else { + throw new Error(localize('oldFunctionPlugin', 'You must update the Azure Functions maven plugin for this functionality.')); } - } catch (error) { - // Swallow the exception if the plugin do not support list templates information. - if (telemetryProperties) { - telemetryProperties.parseJavaTemplateErrors = parseError(error).message; - } - } - - const templates: IFunctionTemplate[] = []; - if (embeddedConfig) { - const bindings: IBindingTemplate[] = parseScriptBindings(embeddedConfig, embeddedResources); - for (const template of embeddedTemplates.templates) { - try { - templates.push(parseJavaTemplate(template, embeddedResources, bindings)); - } catch (error) { - // Ignore errors so that a single poorly formed template does not affect other templates - } - } - } - - if (templates.length > 0) { - return templates; - } else { - // If the templates.length is 0, this means that the user is using an older version of Maven function plugin, - // which do not have the functionality to provide the template information. - // For this kind of scenario, we will fallback to leverage the JavaScript templates. - const javaScriptTemplates: IFunctionTemplate[] = allTemplates.filter((t: IFunctionTemplate) => t.language === ProjectLanguage.JavaScript); - return javaScriptTemplates.filter((t: IFunctionTemplate) => backupJavaTemplateNames.find((vt: string) => vt === removeLanguageFromId(t.id))); } } diff --git a/src/templates/script/ScriptTemplateProvider.ts b/src/templates/script/ScriptTemplateProvider.ts index 729bf2ba..924aaa2d 100644 --- a/src/templates/script/ScriptTemplateProvider.ts +++ b/src/templates/script/ScriptTemplateProvider.ts @@ -8,27 +8,28 @@ import * as fse from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; import { IActionContext } from 'vscode-azureextensionui'; -import { ProjectRuntime } from '../constants'; -import { ext } from '../extensionVariables'; -import { downloadFile } from '../utils/fs'; -import { cliFeedJsonResponse } from '../utils/getCliFeedJson'; -import { IFunctionTemplate } from './IFunctionTemplate'; +import { ext } from '../../extensionVariables'; +import { downloadFile } from '../../utils/fs'; +import { cliFeedJsonResponse } from '../../utils/getCliFeedJson'; +import { ITemplates } from '../ITemplates'; +import { TemplateProviderBase, TemplateType } from '../TemplateProviderBase'; +import { getScriptResourcesPath } from './getScriptResourcesPath'; import { parseScriptTemplates } from './parseScriptTemplates'; -import { TemplateRetriever, TemplateType } from './TemplateRetriever'; -export class ScriptTemplateRetriever extends TemplateRetriever { +export class ScriptTemplateProvider extends TemplateProviderBase { public templateType: TemplateType = TemplateType.Script; - private readonly _templatesKey: string = 'FunctionTemplates'; - private readonly _configKey: string = 'FunctionTemplateConfig'; - private readonly _resourcesKey: string = 'FunctionTemplateResources'; - private _rawResources: object; - private _rawTemplates: object[]; - private _rawConfig: object; + protected readonly _templatesKey: string = 'FunctionTemplates'; + protected readonly _configKey: string = 'FunctionTemplateConfig'; + protected readonly _resourcesKey: string = 'FunctionTemplateResources'; - protected async getTemplatesFromCache(runtime: ProjectRuntime): Promise { - const cachedResources: object | undefined = ext.context.globalState.get(this.getCacheKey(this._resourcesKey, runtime)); - const cachedTemplates: object[] | undefined = ext.context.globalState.get(this.getCacheKey(this._templatesKey, runtime)); - const cachedConfig: object | undefined = ext.context.globalState.get(this.getCacheKey(this._configKey, runtime)); + protected _rawResources: object; + protected _rawTemplates: object[]; + protected _rawConfig: object; + + public async getCachedTemplates(): Promise { + const cachedResources: object | undefined = ext.context.globalState.get(this.getCacheKey(this._resourcesKey)); + const cachedTemplates: object[] | undefined = ext.context.globalState.get(this.getCacheKey(this._templatesKey)); + const cachedConfig: object | undefined = ext.context.globalState.get(this.getCacheKey(this._configKey)); if (cachedResources && cachedTemplates && cachedConfig) { return parseScriptTemplates(cachedResources, cachedTemplates, cachedConfig); } else { @@ -36,7 +37,7 @@ export class ScriptTemplateRetriever extends TemplateRetriever { } } - protected async getTemplatesFromCliFeed(cliFeedJson: cliFeedJsonResponse, templateVersion: string, _runtime: ProjectRuntime, _context: IActionContext): Promise { + public async getLatestTemplates(cliFeedJson: cliFeedJsonResponse, templateVersion: string, _context: IActionContext): Promise { const templatesPath: string = path.join(os.tmpdir(), 'vscode-azurefunctions-templates'); try { const filePath: string = path.join(templatesPath, `templates-${templateVersion}.zip`); @@ -61,19 +62,19 @@ export class ScriptTemplateRetriever extends TemplateRetriever { } } - protected async getTemplatesFromBackup(runtime: ProjectRuntime): Promise { - const backupTemplatesPath: string = ext.context.asAbsolutePath(path.join('resources', 'backupScriptTemplates', runtime)); + public async getBackupTemplates(): Promise { + const backupTemplatesPath: string = ext.context.asAbsolutePath(path.join('resources', 'backupScriptTemplates', this.runtime)); return await this.parseTemplates(backupTemplatesPath); } - protected async cacheTemplates(runtime: ProjectRuntime): Promise { - ext.context.globalState.update(this.getCacheKey(this._templatesKey, runtime), this._rawTemplates); - ext.context.globalState.update(this.getCacheKey(this._configKey, runtime), this._rawConfig); - ext.context.globalState.update(this.getCacheKey(this._resourcesKey, runtime), this._rawResources); + public async cacheTemplates(): Promise { + ext.context.globalState.update(this.getCacheKey(this._templatesKey), this._rawTemplates); + ext.context.globalState.update(this.getCacheKey(this._configKey), this._rawConfig); + ext.context.globalState.update(this.getCacheKey(this._resourcesKey), this._rawResources); } - private async parseTemplates(templatesPath: string): Promise { - this._rawResources = await fse.readJSON(await getResourcesPath(templatesPath)); + protected async parseTemplates(templatesPath: string): Promise { + this._rawResources = await fse.readJSON(await getScriptResourcesPath(templatesPath)); this._rawTemplates = await fse.readJSON(path.join(templatesPath, 'templates', 'templates.json')); this._rawConfig = await fse.readJSON(path.join(templatesPath, 'bindings', 'bindings.json')); diff --git a/src/templates/script/parseScriptTemplates.ts b/src/templates/script/parseScriptTemplates.ts index 76df60b3..af487a10 100644 --- a/src/templates/script/parseScriptTemplates.ts +++ b/src/templates/script/parseScriptTemplates.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { isString } from 'util'; -import { ProjectLanguage } from '../constants'; -import { ext } from '../extensionVariables'; -import { IFunctionBinding, ParsedFunctionJson } from '../funcConfig/function'; -import { IBindingSetting, IBindingTemplate, IEnumValue, ResourceType, ValueType } from './IBindingTemplate'; -import { IFunctionTemplate, TemplateCategory } from './IFunctionTemplate'; +import { ProjectLanguage } from '../../constants'; +import { IFunctionBinding, ParsedFunctionJson } from '../../funcConfig/function'; +import { IBindingSetting, IBindingTemplate, IEnumValue, ResourceType, ValueType } from '../IBindingTemplate'; +import { IFunctionTemplate, TemplateCategory } from '../IFunctionTemplate'; +import { ITemplates } from '../ITemplates'; /** * Describes a script template before it has been parsed @@ -217,16 +217,17 @@ export interface IScriptFunctionTemplate extends IFunctionTemplate { * Parses templates contained in the templateApiZip of the functions cli feed. This contains all 'script' templates, including JavaScript, C#Script, Python, etc. * This basically converts the 'raw' templates in the externally defined JSON format to a common and understood format (IFunctionTemplate) used by this extension */ -export function parseScriptTemplates(rawResources: object, rawTemplates: object[], rawConfig: object): IFunctionTemplate[] { - ext.scriptBindings = parseScriptBindings(rawConfig, rawResources); +export function parseScriptTemplates(rawResources: object, rawTemplates: object[], rawConfig: object): ITemplates { + const bindingTemplates: IBindingTemplate[] = parseScriptBindings(rawConfig, rawResources); - const templates: IFunctionTemplate[] = []; + const functionTemplates: IFunctionTemplate[] = []; for (const rawTemplate of rawTemplates) { try { - templates.push(parseScriptTemplate(rawTemplate, rawResources, ext.scriptBindings)); + functionTemplates.push(parseScriptTemplate(rawTemplate, rawResources, bindingTemplates)); } catch (error) { // Ignore errors so that a single poorly formed template does not affect other templates } } - return templates; + + return { functionTemplates, bindingTemplates }; } diff --git a/src/utils/getCliFeedJson.ts b/src/utils/getCliFeedJson.ts index 1d1b4356..b7546ee0 100644 --- a/src/utils/getCliFeedJson.ts +++ b/src/utils/getCliFeedJson.ts @@ -37,12 +37,16 @@ export async function tryGetCliFeedJson(): PromiseJSON.parse(response); + return getCliFeedJson(); }); } +export async function getCliFeedJson(): Promise { + const request: requestUtils.Request = await requestUtils.getDefaultRequest(funcCliFeedUrl); + const response: string = await requestUtils.sendRequest(request); + return JSON.parse(response); +} + export function getFeedRuntime(runtime: ProjectRuntime): string { let result: string; switch (runtime) { @@ -56,7 +60,7 @@ export function getFeedRuntime(runtime: ProjectRuntime): string { throw new RangeError(localize('invalidRuntime', 'Invalid runtime "{0}".', runtime)); } - return ext.templateSource === TemplateSource.StagingCliFeed ? `${result}-prerelease` : result; + return ext.templateProvider.templateSource === TemplateSource.Staging ? `${result}-prerelease` : result; } /** diff --git a/src/utils/mavenUtils.ts b/src/utils/mavenUtils.ts index 10532a3b..8f5dd6a2 100644 --- a/src/utils/mavenUtils.ts +++ b/src/utils/mavenUtils.ts @@ -19,14 +19,18 @@ export namespace mavenUtils { await cpUtils.executeCommand(undefined, undefined, mvnCommand, '--version'); } catch (error) { const message: string = localize('azFunc.mvnNotFound', 'Failed to find "maven", please ensure that the maven bin directory is in your system path.'); - // don't wait - vscode.window.showErrorMessage(message, DialogResponses.learnMore).then(async result => { - if (result === DialogResponses.learnMore) { - await openUrl('https://aka.ms/azurefunction_maven'); - } - }); - context.errorHandling.suppressDisplay = true; // Swallow errors in case show two error message - throw new Error(localize('azFunc.mvnNotFound', 'Failed to find "maven" on path.')); + + if (!context.errorHandling.suppressDisplay) { + // don't wait + vscode.window.showErrorMessage(message, DialogResponses.learnMore).then(async result => { + if (result === DialogResponses.learnMore) { + await openUrl('https://aka.ms/azurefunction_maven'); + } + }); + context.errorHandling.suppressDisplay = true; + } + + throw new Error(message); } } diff --git a/src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts b/src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts index bf9fe4b6..ebe085c2 100644 --- a/src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts +++ b/src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts @@ -5,13 +5,13 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import { DialogResponses, IActionContext } from 'vscode-azureextensionui'; +import { callWithTelemetryAndErrorHandling, DialogResponses, IActionContext } from 'vscode-azureextensionui'; import { tryGetFunctionProjectRoot } from '../commands/createNewProject/verifyIsProject'; import { initProjectForVSCode } from '../commands/initProjectForVSCode/initProjectForVSCode'; -import { ProjectLanguage, projectLanguageSetting, projectRuntimeSetting } from '../constants'; +import { ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting } from '../constants'; import { ext } from '../extensionVariables'; import { localize } from '../localize'; -import { getWorkspaceSetting, updateGlobalSetting } from './settings'; +import { convertStringToRuntime, getWorkspaceSetting, updateGlobalSetting } from './settings'; import { verifyJavaDeployConfigIsValid } from './verifyJavaDeployConfigIsValid'; import { verifyJSDebugConfigIsValid } from './verifyJSDebugConfigIsValid'; import { verifyPythonVenv } from './verifyPythonVenv'; @@ -29,7 +29,17 @@ export async function verifyVSCodeConfigOnActivate(context: IActionContext, fold if (projectPath) { context.telemetry.suppressIfSuccessful = false; - if (isInitializedProject(projectPath)) { + const language: ProjectLanguage | undefined = getWorkspaceSetting(projectLanguageSetting, projectPath); + const runtime: ProjectRuntime | undefined = convertStringToRuntime(getWorkspaceSetting(projectRuntimeSetting, projectPath)); + if (language !== undefined && runtime !== undefined) { + // Don't wait + // tslint:disable-next-line: no-floating-promises + callWithTelemetryAndErrorHandling('initializeTemplates', async (templatesContext: IActionContext) => { + templatesContext.telemetry.properties.isActivationEvent = 'true'; + templatesContext.errorHandling.suppressDisplay = true; + await ext.templateProvider.getFunctionTemplates(templatesContext, language, runtime); + }); + const projectLanguage: string | undefined = getWorkspaceSetting(projectLanguageSetting, workspacePath); context.telemetry.properties.projectLanguage = projectLanguage; switch (projectLanguage) { @@ -75,9 +85,3 @@ async function promptToInitializeProject(workspacePath: string, context: IAction context.telemetry.properties.verifyConfigResult = 'suppressed'; } } - -function isInitializedProject(projectPath: string): boolean { - const language: string | undefined = getWorkspaceSetting(projectLanguageSetting, projectPath); - const runtime: string | undefined = getWorkspaceSetting(projectRuntimeSetting, projectPath); - return !!language && !!runtime; -} diff --git a/test/createFunction/FunctionTesterBase.ts b/test/createFunction/FunctionTesterBase.ts index 1514503d..98e8560b 100644 --- a/test/createFunction/FunctionTesterBase.ts +++ b/test/createFunction/FunctionTesterBase.ts @@ -7,8 +7,8 @@ import * as assert from 'assert'; import * as fse from 'fs-extra'; import * as path from 'path'; import { Disposable } from 'vscode'; -import { createFunction, ext, IFunctionTemplate, ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting, TemplateFilter, templateFilterSetting, TemplateProvider } from '../../extension.bundle'; -import { runForAllTemplateSources, testFolderPath, testUserInput } from '../global.test'; +import { createFunction, ext, IFunctionTemplate, ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting, TemplateFilter, templateFilterSetting } from '../../extension.bundle'; +import { runForAllTemplateSources, testActionContext, testFolderPath, testUserInput } from '../global.test'; import { runWithFuncSetting } from '../runWithSetting'; export abstract class FunctionTesterBase implements Disposable { @@ -32,8 +32,7 @@ export abstract class FunctionTesterBase implements Disposable { } public async dispose(): Promise { - const templateProvider: TemplateProvider = await ext.templateProviderTask; - const templates: IFunctionTemplate[] = await templateProvider.getTemplates(this.language, this.runtime, this.baseTestFolder, TemplateFilter.Verified); + const templates: IFunctionTemplate[] = await ext.templateProvider.getFunctionTemplates(testActionContext, this.language, this.runtime, TemplateFilter.Verified); assert.deepEqual(this.testedFunctions.sort(), templates.map(t => t.name).sort(), 'Not all "Verified" templates were tested'); } diff --git a/test/getResourcesPath.test.ts b/test/getResourcesPath.test.ts index 8cf91dec..6584aa02 100644 --- a/test/getResourcesPath.test.ts +++ b/test/getResourcesPath.test.ts @@ -6,16 +6,16 @@ import * as assert from 'assert'; import * as fse from 'fs-extra'; import * as path from 'path'; -import { ext, getResourcesPath } from '../extension.bundle'; +import { ext, getScriptResourcesPath } from '../extension.bundle'; import { testFolderPath } from './global.test'; async function verifyLanguage(vscodeLanguage: string, fileName: string, templatesPath?: string): Promise { templatesPath = templatesPath || ext.context.asAbsolutePath(path.join('resources', 'backupScriptTemplates', '~2')); - const actual: string = await getResourcesPath(templatesPath, vscodeLanguage); + const actual: string = await getScriptResourcesPath(templatesPath, vscodeLanguage); assert.equal(actual, path.join(templatesPath, 'resources', fileName)); } -suite('getResourcesPath', async () => { +suite('getScriptResourcesPath', async () => { let extraTemplatesPath: string; suiteSetup(async () => { extraTemplatesPath = path.join(testFolderPath, 'extraTemplates'); diff --git a/test/global.test.ts b/test/global.test.ts index 083f293a..df4cf064 100644 --- a/test/global.test.ts +++ b/test/global.test.ts @@ -10,7 +10,7 @@ import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import { TestOutputChannel, TestUserInput } from 'vscode-azureextensiondev'; -import { ext, getRandomHexString, getTemplateProvider, parseError, TemplateProvider, TemplateSource } from '../extension.bundle'; +import { CentralTemplateProvider, ext, getRandomHexString, IActionContext, parseError, ProjectLanguage, ProjectRuntime, TemplateSource } from '../extension.bundle'; /** * Folder for most tests that do not need a workspace open @@ -25,7 +25,9 @@ export let testWorkspacePath: string; export let longRunningTestsEnabled: boolean; export let testUserInput: TestUserInput = new TestUserInput(vscode); -let templatesMap: Map; +export const testActionContext: IActionContext = { telemetry: { properties: {}, measurements: {} }, errorHandling: {} }; + +let templateProviderMap: Map; // Runs before all tests suiteSetup(async function (this: IHookCallbackContext): Promise { @@ -35,24 +37,21 @@ suiteSetup(async function (this: IHookCallbackContext): Promise { testWorkspacePath = await initTestWorkspacePath(); await vscode.commands.executeCommand('azureFunctions.refresh'); // activate the extension before tests begin - await ext.templateProviderTask; // make sure default templates are loaded before setting up templates from other sources ext.outputChannel = new TestOutputChannel(); ext.ui = testUserInput; // Use prerelease func cli installed from gulp task (unless otherwise specified in env) ext.funcCliPath = process.env.FUNC_PATH || path.join(os.homedir(), 'tools', 'func', 'func'); - templatesMap = new Map(); - try { - for (const key of Object.keys(TemplateSource)) { - const source: TemplateSource = TemplateSource[key]; - ext.templateSource = source; - templatesMap.set(source, await getTemplateProvider()); - } - } finally { - ext.templateSource = undefined; + await preLoadTemplates(ext.templateProvider); + templateProviderMap = new Map(); + for (const key of Object.keys(TemplateSource)) { + const source: TemplateSource = TemplateSource[key]; + templateProviderMap.set(source, new CentralTemplateProvider(source)); } + await runForAllTemplateSources(async (_source, provider) => await preLoadTemplates(provider)); + // tslint:disable-next-line:strict-boolean-expressions longRunningTestsEnabled = !/^(false|0)?$/i.test(process.env.ENABLE_LONG_RUNNING_TESTS || ''); @@ -71,31 +70,44 @@ suiteTeardown(async function (this: IHookCallbackContext): Promise { } }); -export async function runForAllTemplateSources(callback: (source: TemplateSource, templates: TemplateProvider) => Promise): Promise { - for (const source of templatesMap.keys()) { - await runForTemplateSource(source, (templates: TemplateProvider) => callback(source, templates)); +/** + * Pre-load templates so that the first related unit test doesn't time out + */ +async function preLoadTemplates(provider: CentralTemplateProvider): Promise { + console.log(`Loading templates for source "${provider.templateSource}"`); + const runtimes: ProjectRuntime[] = [ProjectRuntime.v1, ProjectRuntime.v2]; + const languages: ProjectLanguage[] = [ProjectLanguage.JavaScript, ProjectLanguage.CSharp]; + + for (const runtime of runtimes) { + for (const language of languages) { + await provider.getFunctionTemplates(testActionContext, language, runtime); + } } } -export async function runForTemplateSource(source: TemplateSource | undefined, callback: (templates: TemplateProvider) => Promise): Promise { - const oldProvider: Promise = ext.templateProviderTask; +export async function runForAllTemplateSources(callback: (source: TemplateSource, templateProvider: CentralTemplateProvider) => Promise): Promise { + for (const source of templateProviderMap.keys()) { + await runForTemplateSource(source, (templateProvider: CentralTemplateProvider) => callback(source, templateProvider)); + } +} + +export async function runForTemplateSource(source: TemplateSource | undefined, callback: (templateProvider: CentralTemplateProvider) => Promise): Promise { + const oldProvider: CentralTemplateProvider = ext.templateProvider; try { - let templates: TemplateProvider | undefined; + let templateProvider: CentralTemplateProvider | undefined; if (source === undefined) { - templates = await ext.templateProviderTask; + templateProvider = ext.templateProvider; } else { - templates = templatesMap.get(source); - if (!templates) { + templateProvider = templateProviderMap.get(source); + if (!templateProvider) { throw new Error(`Unrecognized source ${source}`); } - ext.templateSource = source; - ext.templateProviderTask = Promise.resolve(templates); + ext.templateProvider = templateProvider; } - await callback(templates); + await callback(templateProvider); } finally { - ext.templateSource = undefined; - ext.templateProviderTask = oldProvider; + ext.templateProvider = oldProvider; } } diff --git a/test/templateCount.test.ts b/test/templateCount.test.ts index 8b4ffad1..d8a1e304 100644 --- a/test/templateCount.test.ts +++ b/test/templateCount.test.ts @@ -5,26 +5,26 @@ import * as assert from 'assert'; import { IHookCallbackContext } from 'mocha'; -import { IFunctionTemplate, ProjectLanguage, ProjectRuntime, TemplateFilter, TemplateProvider, TemplateSource } from '../extension.bundle'; -import { longRunningTestsEnabled, runForTemplateSource, testFolderPath } from './global.test'; +import { CentralTemplateProvider, IFunctionTemplate, ProjectLanguage, ProjectRuntime, TemplateFilter, TemplateSource } from '../extension.bundle'; +import { longRunningTestsEnabled, runForTemplateSource, testActionContext } from './global.test'; addSuite(undefined); -addSuite(TemplateSource.CliFeed); -addSuite(TemplateSource.StagingCliFeed); +addSuite(TemplateSource.Latest); +addSuite(TemplateSource.Staging); addSuite(TemplateSource.Backup); function addSuite(source: TemplateSource | undefined): void { suite(`Template Count - ${source === undefined ? 'defaultOnExtensionActivation' : source}`, async () => { test('JavaScript v1', async () => { - await runForTemplateSource(source, async (templates: TemplateProvider) => { - const jsTemplatesv1: IFunctionTemplate[] = await templates.getTemplates(ProjectLanguage.JavaScript, ProjectRuntime.v1, testFolderPath, TemplateFilter.Verified); + await runForTemplateSource(source, async (provider: CentralTemplateProvider) => { + const jsTemplatesv1: IFunctionTemplate[] = await provider.getFunctionTemplates(testActionContext, ProjectLanguage.JavaScript, ProjectRuntime.v1, TemplateFilter.Verified); assert.equal(jsTemplatesv1.length, 8); }); }); test('JavaScript v2', async () => { - await runForTemplateSource(source, async (templates: TemplateProvider) => { - const jsTemplatesv2: IFunctionTemplate[] = await templates.getTemplates(ProjectLanguage.JavaScript, ProjectRuntime.v2, testFolderPath, TemplateFilter.Verified); + await runForTemplateSource(source, async (provider: CentralTemplateProvider) => { + const jsTemplatesv2: IFunctionTemplate[] = await provider.getFunctionTemplates(testActionContext, ProjectLanguage.JavaScript, ProjectRuntime.v2, TemplateFilter.Verified); assert.equal(jsTemplatesv2.length, 12); }); }); @@ -35,43 +35,43 @@ function addSuite(source: TemplateSource | undefined): void { } this.timeout(60 * 1000); - await runForTemplateSource(source, async (templates: TemplateProvider) => { - const javaTemplates: IFunctionTemplate[] = await templates.getTemplates(ProjectLanguage.Java, ProjectRuntime.v2, testFolderPath, TemplateFilter.Verified); + await runForTemplateSource(source, async (provider: CentralTemplateProvider) => { + const javaTemplates: IFunctionTemplate[] = await provider.getFunctionTemplates(testActionContext, ProjectLanguage.Java, ProjectRuntime.v2, TemplateFilter.Verified); assert.equal(javaTemplates.length, 4); }); }); test('C# v1', async () => { - await runForTemplateSource(source, async (templates: TemplateProvider) => { - const cSharpTemplates: IFunctionTemplate[] = await templates.getTemplates(ProjectLanguage.CSharp, ProjectRuntime.v1, testFolderPath, TemplateFilter.Verified); + await runForTemplateSource(source, async (provider: CentralTemplateProvider) => { + const cSharpTemplates: IFunctionTemplate[] = await provider.getFunctionTemplates(testActionContext, ProjectLanguage.CSharp, ProjectRuntime.v1, TemplateFilter.Verified); assert.equal(cSharpTemplates.length, 12); }); }); test('C# v2', async () => { - await runForTemplateSource(source, async (templates: TemplateProvider) => { - const cSharpTemplatesv2: IFunctionTemplate[] = await templates.getTemplates(ProjectLanguage.CSharp, ProjectRuntime.v2, testFolderPath, TemplateFilter.Verified); + await runForTemplateSource(source, async (provider: CentralTemplateProvider) => { + const cSharpTemplatesv2: IFunctionTemplate[] = await provider.getFunctionTemplates(testActionContext, ProjectLanguage.CSharp, ProjectRuntime.v2, TemplateFilter.Verified); assert.equal(cSharpTemplatesv2.length, 9); }); }); test('Python v2', async () => { - await runForTemplateSource(source, async (templates: TemplateProvider) => { - const pythonTemplates: IFunctionTemplate[] = await templates.getTemplates(ProjectLanguage.Python, ProjectRuntime.v2, testFolderPath, TemplateFilter.Verified); + await runForTemplateSource(source, async (provider: CentralTemplateProvider) => { + const pythonTemplates: IFunctionTemplate[] = await provider.getFunctionTemplates(testActionContext, ProjectLanguage.Python, ProjectRuntime.v2, TemplateFilter.Verified); assert.equal(pythonTemplates.length, 9); }); }); test('TypeScript v2', async () => { - await runForTemplateSource(source, async (templates: TemplateProvider) => { - const tsTemplates: IFunctionTemplate[] = await templates.getTemplates(ProjectLanguage.TypeScript, ProjectRuntime.v2, testFolderPath, TemplateFilter.Verified); + await runForTemplateSource(source, async (provider: CentralTemplateProvider) => { + const tsTemplates: IFunctionTemplate[] = await provider.getFunctionTemplates(testActionContext, ProjectLanguage.TypeScript, ProjectRuntime.v2, TemplateFilter.Verified); assert.equal(tsTemplates.length, 12); }); }); test('PowerShell v2', async () => { - await runForTemplateSource(source, async (templates: TemplateProvider) => { - const powershellTemplates: IFunctionTemplate[] = await templates.getTemplates(ProjectLanguage.PowerShell, ProjectRuntime.v2, testFolderPath, TemplateFilter.Verified); + await runForTemplateSource(source, async (provider: CentralTemplateProvider) => { + const powershellTemplates: IFunctionTemplate[] = await provider.getFunctionTemplates(testActionContext, ProjectLanguage.PowerShell, ProjectRuntime.v2, TemplateFilter.Verified); assert.equal(powershellTemplates.length, 9); }); });