Skip to content

Commit

Permalink
Node.js Programming Model (Preview) (#3456)
Browse files Browse the repository at this point in the history
* Basic implementation of NPM

* Feature flag and changes to model structure

* Refactor some code, general cleaning

* Minor fixes for bundle versions and tasks.json

* Minor fixes

* PR feedback

* leanr to spell plz

* Util function for file extension

* Revert small change

* PR feedback

* Fix the build
  • Loading branch information
nturinski authored Dec 16, 2022
1 parent ea05316 commit a1d79e1
Show file tree
Hide file tree
Showing 46 changed files with 5,149 additions and 69 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

### Fixed

- Log Analytic workspace resource provider not being registered blocked creation
- Log Analytic workspace resource provider not being registered blocked creation [#3352](https://github.com/microsoft/vscode-azurefunctions/issues/3352)

## 1.8.0 - 2022-09-20

Expand Down
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,17 @@
"type": "boolean",
"description": "%azureFunctions.endOfLifeWarning%",
"default": true
},
"azureFunctions.functionSubpath": {
"scope": "resource",
"type": "string",
"default": "src/functions",
"description": "%azureFunctions.functionSubpath%"
},
"azureFunctions.showNodeProgrammingModel": {
"type": "boolean",
"description": "%azureFunctions.showNodeProgrammingModel%",
"default": false
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,7 @@
"azureFunctions.validateFuncCoreTools": "Validate the Azure Functions Core Tools is installed before debugging.",
"azureFunctions.viewCommitInGitHub": "View Commit in GitHub",
"azureFunctions.viewDeploymentLogs": "View Deployment Logs",
"azureFunctions.viewProperties": "View Properties"
"azureFunctions.viewProperties": "View Properties",
"azureFunctions.functionSubpath": "The default subpath to create new functions. Currently, this only applies to Node.js programming model v4+.",
"azureFunctions.showNodeProgrammingModel": "Enable preview Node.js programming model"
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4,657 changes: 4,657 additions & 0 deletions resources/backupTemplates/nodejs-4.x/templates/templates.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/FuncVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { IActionContext, IAzureQuickPickItem, IAzureQuickPickOptions } from '@microsoft/vscode-azext-utils';
import { learnMoreQp } from './constants';
import { localize } from './localize';
import { openUrl } from './utils/openUrl';

Expand All @@ -28,7 +29,7 @@ export async function promptForFuncVersion(context: IActionContext, message?: st

picks = picks.filter(p => osSupportsVersion(p.data));

picks.push({ label: localize('learnMore', '$(link-external) Learn more...'), description: '', data: undefined });
picks.push(learnMoreQp);

const options: IAzureQuickPickOptions = { placeHolder: message || localize('selectVersion', 'Select a version'), stepName: 'funcVersion', suppressPersistence: true };
// eslint-disable-next-line no-constant-condition
Expand Down
9 changes: 6 additions & 3 deletions src/commands/createFunction/FunctionListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { FuncVersion } from '../../FuncVersion';
import { localize } from '../../localize';
import { IFunctionTemplate } from '../../templates/IFunctionTemplate';
import { nonNullProp } from '../../utils/nonNull';
import { isPythonV2Plus } from '../../utils/pythonUtils';
import { isNodeV4Plus, isPythonV2Plus } from '../../utils/programmingModelUtils';
import { getWorkspaceSetting, updateWorkspaceSetting } from '../../vsCodeConfig/settings';
import { FunctionSubWizard } from './FunctionSubWizard';
import { IFunctionWizardContext } from './IFunctionWizardContext';
Expand Down Expand Up @@ -61,7 +61,7 @@ export class FunctionListStep extends AzureWizardPromptStep<IFunctionWizardConte
if (isV2PythonModel) {
return {
// TODO: Title?
promptSteps: [ new PythonLocationStep(this._functionSettings) ]
promptSteps: [new PythonLocationStep(this._functionSettings)]
};
} else {
return await FunctionSubWizard.createSubWizard(context, this._functionSettings);
Expand Down Expand Up @@ -136,7 +136,10 @@ export class FunctionListStep extends AzureWizardPromptStep<IFunctionWizardConte
data: <IFunctionTemplate | TemplatePromptResult><unknown>undefined,
onPicked: () => { /* do nothing */ }
})
} else if (language === ProjectLanguage.CSharp || language === ProjectLanguage.Java || (language === ProjectLanguage.Python && !isPythonV2Plus(language, languageModel)) || language === ProjectLanguage.TypeScript) {
} else if (language === ProjectLanguage.CSharp ||
language === ProjectLanguage.Java ||
(language === ProjectLanguage.Python && !isPythonV2Plus(language, languageModel)) ||
(language === ProjectLanguage.TypeScript && !isNodeV4Plus(context))) {
// NOTE: Only show this if we actually found other templates
picks.push({
label: localize('openAPI', 'HTTP trigger(s) from OpenAPI V2/V3 Specification (Preview)'),
Expand Down
50 changes: 29 additions & 21 deletions src/commands/createFunction/FunctionSubWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { canValidateAzureWebJobStorageOnDebug } from '../../debug/validatePreDeb
import { getAzureWebJobsStorage } from '../../funcConfig/local.settings';
import { localize } from '../../localize';
import { IFunctionTemplate } from '../../templates/IFunctionTemplate';
import { isPythonV2Plus } from '../../utils/pythonUtils';
import { isNodeV4Plus, isPythonV2Plus } from '../../utils/programmingModelUtils';
import { addBindingSettingSteps } from '../addBinding/settingSteps/addBindingSettingSteps';
import { AzureWebJobsStorageExecuteStep } from '../appSettings/AzureWebJobsStorageExecuteStep';
import { AzureWebJobsStoragePromptStep } from '../appSettings/AzureWebJobsStoragePromptStep';
Expand All @@ -22,6 +22,8 @@ import { JavaFunctionCreateStep } from './javaSteps/JavaFunctionCreateStep';
import { JavaFunctionNameStep } from './javaSteps/JavaFunctionNameStep';
import { OpenAPICreateStep } from './openAPISteps/OpenAPICreateStep';
import { OpenAPIGetSpecificationFileStep } from './openAPISteps/OpenAPIGetSpecificationFileStep';
import { NodeV4FunctionCreateStep } from './scriptSteps/NodeV4FunctionCreateStep';
import { NodeV4FunctionNameStep } from './scriptSteps/NodeV4FunctionNameStep';
import { PythonFunctionCreateStep } from './scriptSteps/PythonFunctionCreateStep';
import { PythonScriptStep } from './scriptSteps/PythonScriptStep';
import { ScriptFunctionCreateStep } from './scriptSteps/ScriptFunctionCreateStep';
Expand Down Expand Up @@ -51,8 +53,10 @@ export class FunctionSubWizard {
promptSteps.push(new DotnetFunctionNameStep(), new DotnetNamespaceStep());
break;
default:
// NOTE: The V2 Python model has attributed bindings and we don't (yet) update them from the template.
if (!isV2PythonModel) {
if (isNodeV4Plus(context)) {
promptSteps.push(new NodeV4FunctionNameStep())
} else if (!isV2PythonModel) {
// NOTE: The V2 Python model has attributed bindings and we don't (yet) update them from the template.
promptSteps.push(new ScriptFunctionNameStep());
}
break;
Expand All @@ -66,24 +70,28 @@ export class FunctionSubWizard {
addBindingSettingSteps(template.userPromptedSettings, promptSteps);

const executeSteps: AzureWizardExecuteStep<IFunctionWizardContext>[] = [];
switch (context.language) {
case ProjectLanguage.Java:
executeSteps.push(new JavaFunctionCreateStep());
break;
case ProjectLanguage.CSharp:
case ProjectLanguage.FSharp:
executeSteps.push(await DotnetFunctionCreateStep.createStep(context));
break;
case ProjectLanguage.TypeScript:
executeSteps.push(new TypeScriptFunctionCreateStep());
break;
default:
if (isV2PythonModel) {
executeSteps.push(new PythonFunctionCreateStep());
} else {
executeSteps.push(new ScriptFunctionCreateStep());
}
break;
if (isNodeV4Plus(context)) {
executeSteps.push(new NodeV4FunctionCreateStep());
} else {
switch (context.language) {
case ProjectLanguage.Java:
executeSteps.push(new JavaFunctionCreateStep());
break;
case ProjectLanguage.CSharp:
case ProjectLanguage.FSharp:
executeSteps.push(await DotnetFunctionCreateStep.createStep(context));
break;
case ProjectLanguage.TypeScript:
executeSteps.push(new TypeScriptFunctionCreateStep());
break;
default:
if (isV2PythonModel) {
executeSteps.push(new PythonFunctionCreateStep());
} else {
executeSteps.push(new ScriptFunctionCreateStep());
}
break;
}
}

if ((!template.isHttpTrigger && !template.isSqlBindingTemplate) && !canValidateAzureWebJobStorageOnDebug(context.language) && !await getAzureWebJobsStorage(context, context.projectPath)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzExtFsExtra, nonNullProp } from '@microsoft/vscode-azext-utils';
import * as path from 'path';
import { functionSubpathSetting } from '../../../constants';
import { IScriptFunctionTemplate } from '../../../templates/script/parseScriptTemplates';
import { getWorkspaceSetting } from '../../../vsCodeConfig/settings';
import { FunctionCreateStepBase } from '../FunctionCreateStepBase';
import { IScriptFunctionWizardContext } from './IScriptFunctionWizardContext';
import { getFileExtensionFromLanguage } from './ScriptFunctionCreateStep';

export class NodeV4FunctionCreateStep extends FunctionCreateStepBase<IScriptFunctionWizardContext> {
public async executeCore(context: IScriptFunctionWizardContext): Promise<string> {
const functionSubpath: string = getWorkspaceSetting(functionSubpathSetting, context.projectPath) as string;
const functionPath = path.join(context.projectPath, functionSubpath);
await AzExtFsExtra.ensureDir(functionPath);

const functionName = nonNullProp(context, 'functionName');
const fileExt = getFileExtensionFromLanguage(context.language);
const fileName = `${functionName}${fileExt}`;

const template: IScriptFunctionTemplate = nonNullProp(context, 'functionTemplate');
await Promise.all(Object.keys(template.templateFiles).map(async f => {
let contents = template.templateFiles[f];
contents = contents.replace(/%functionName%/g, functionName);

for (const setting of template.userPromptedSettings) {
// the setting name keys are lowercased
contents = contents.replace(new RegExp(`%${setting.name}%`, 'g'), context[setting.name.toLowerCase()]);
}

await AzExtFsExtra.writeFile(path.join(functionPath, fileName), contents);
}));

return path.join(functionPath, fileName);
}
}
37 changes: 37 additions & 0 deletions src/commands/createFunction/scriptSteps/NodeV4FunctionNameStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzExtFsExtra } from '@microsoft/vscode-azext-utils';
import * as path from 'path';
import { functionSubpathSetting } from '../../../constants';
import { localize } from "../../../localize";
import { IScriptFunctionTemplate } from '../../../templates/script/parseScriptTemplates';
import { nonNullProp } from '../../../utils/nonNull';
import { getWorkspaceSetting } from '../../../vsCodeConfig/settings';
import { FunctionNameStepBase } from '../FunctionNameStepBase';
import { IScriptFunctionWizardContext } from './IScriptFunctionWizardContext';
import { getFileExtensionFromLanguage } from './ScriptFunctionCreateStep';

export class NodeV4FunctionNameStep extends FunctionNameStepBase<IScriptFunctionWizardContext> {
protected async getUniqueFunctionName(context: IScriptFunctionWizardContext): Promise<string | undefined> {
const template: IScriptFunctionTemplate = nonNullProp(context, 'functionTemplate');
const functionSubpath: string = getWorkspaceSetting(functionSubpathSetting, context.projectPath) as string;
return await this.getUniqueFsPath(
path.join(context.projectPath, functionSubpath),
template.defaultFunctionName,
getFileExtensionFromLanguage(context.language));
}

protected async validateFunctionNameCore(context: IScriptFunctionWizardContext, name: string): Promise<string | undefined> {
const functionSubpath: string = getWorkspaceSetting(functionSubpathSetting, context.projectPath) as string;
name = `${name}${getFileExtensionFromLanguage(context.language)}`;

if (await AzExtFsExtra.pathExists(path.join(context.projectPath, functionSubpath, name))) {
return localize('existingFileError', 'A file with the name "{0}" already exists.', name);
} else {
return undefined;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ export function getScriptFileNameFromLanguage(language: string): string | undefi
}
}

export function getFileExtensionFromLanguage(language?: string): string | undefined {
switch (language) {
case ProjectLanguage.CSharpScript:
return '.csx';
case ProjectLanguage.FSharpScript:
return '.fsx';
case ProjectLanguage.JavaScript:
return '.js';
case ProjectLanguage.PowerShell:
return '.ps1';
case ProjectLanguage.Python:
return '.py';
case ProjectLanguage.TypeScript:
return '.ts';
default:
return undefined;
}
}

export class ScriptFunctionCreateStep extends FunctionCreateStepBase<IScriptFunctionWizardContext> {
public async executeCore(context: IScriptFunctionWizardContext): Promise<string> {
const functionPath: string = path.join(context.projectPath, nonNullProp(context, 'functionName'));
Expand Down
13 changes: 11 additions & 2 deletions src/commands/createNewProject/NewProjectLanguageStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@

import { AzureWizardExecuteStep, AzureWizardPromptStep, IAzureQuickPickItem, IWizardOptions, UserCancelledError } from '@microsoft/vscode-azext-utils';
import { QuickPickOptions } from 'vscode';
import { previewPythonModel, ProjectLanguage, pythonNewModelPreview } from '../../constants';
import { nodeLearnMoreLink, nodeModels, previewPythonModel, ProjectLanguage, pythonNewModelPreview } from '../../constants';
import { localize } from '../../localize';
import { nonNullProp } from '../../utils/nonNull';
import { openUrl } from '../../utils/openUrl';
import { isPythonV2Plus } from '../../utils/pythonUtils';
import { isPythonV2Plus } from '../../utils/programmingModelUtils';
import { FunctionListStep } from '../createFunction/FunctionListStep';
import { addInitVSCodeSteps } from '../initProjectForVSCode/InitVSCodeLanguageStep';
import { DotnetRuntimeStep } from './dotnetSteps/DotnetRuntimeStep';
import { IProjectWizardContext } from './IProjectWizardContext';
import { addJavaCreateProjectSteps } from './javaSteps/addJavaCreateProjectSteps';
import { ProgrammingModelStep } from './ProgrammingModelStep';
import { CustomProjectCreateStep } from './ProjectCreateStep/CustomProjectCreateStep';
import { DotnetProjectCreateStep } from './ProjectCreateStep/DotnetProjectCreateStep';
import { JavaScriptProjectCreateStep } from './ProjectCreateStep/JavaScriptProjectCreateStep';
Expand Down Expand Up @@ -79,9 +80,17 @@ export class NewProjectLanguageStep extends AzureWizardPromptStep<IProjectWizard
const promptSteps: AzureWizardPromptStep<IProjectWizardContext>[] = [];
switch (language) {
case ProjectLanguage.JavaScript:
promptSteps.push(new ProgrammingModelStep({
models: nodeModels,
learnMoreLink: nodeLearnMoreLink
}));
executeSteps.push(new JavaScriptProjectCreateStep());
break;
case ProjectLanguage.TypeScript:
promptSteps.push(new ProgrammingModelStep({
models: nodeModels,
learnMoreLink: nodeLearnMoreLink
}));
executeSteps.push(new TypeScriptProjectCreateStep());
break;
case ProjectLanguage.CSharp:
Expand Down
65 changes: 65 additions & 0 deletions src/commands/createNewProject/ProgrammingModelStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, IAzureQuickPickItem, IAzureQuickPickOptions, nonNullValue, openUrl } from '@microsoft/vscode-azext-utils';
import { learnMoreQp, nodeProgrammingModelSetting } from '../../constants';
import { localize } from '../../localize';
import { getWorkspaceSetting } from '../../vsCodeConfig/settings';
import { IProjectWizardContext } from './IProjectWizardContext';

type ProgrammingModel = { modelVersion: number | undefined, label: string };
export class ProgrammingModelStep extends AzureWizardPromptStep<IProjectWizardContext> {
public hideStepCount: boolean = true;
private _models: ProgrammingModel[] = [];
private _learnMoreLink: string | undefined;

public constructor(options: { models: ProgrammingModel | ProgrammingModel[], learnMoreLink?: string }) {
super();
this._models = Array.isArray(options.models) ? options.models : [options.models];
this._learnMoreLink = options.learnMoreLink;
}

public async prompt(context: IProjectWizardContext): Promise<void> {
const modelsPick: IAzureQuickPickItem<number | undefined>[] = this._models.map(model => {
return {
label: model.label,
data: model.modelVersion
}
});

if (this._learnMoreLink) {
modelsPick.push(learnMoreQp);
}

const options: IAzureQuickPickOptions = {
placeHolder: localize('selectLanguage', 'Select a {0} programming model', context.language),
suppressPersistence: true,
learnMoreLink: this._learnMoreLink
};

let result: IAzureQuickPickItem<number | undefined>;
do {
result = (await context.ui.showQuickPick(modelsPick, options));
if (result === learnMoreQp) {
await openUrl(nonNullValue(this._learnMoreLink));
}
}
while (result === learnMoreQp);

context.languageModel = result.data;
}

public shouldPrompt(context: IProjectWizardContext): boolean {
// auto-select the default model if there is only one
if (this._models.length === 1) {
context.languageModel = this._models[0].modelVersion;
}

// this only impacts node for now so only check the feature flag for node
return context.languageModel === undefined &&
(!!getWorkspaceSetting(nodeProgrammingModelSetting) &&
(context.language === 'JavaScript' || context.language === 'TypeScript'));
}
}
Loading

0 comments on commit a1d79e1

Please sign in to comment.