Skip to content

Commit

Permalink
feat(vscode): Custom Code Support in VSCode (#6251)
Browse files Browse the repository at this point in the history
* merge

* temp commit

* work in progress

* temp commit

* work being done

* remove datamapper code

* fixing code based on comments

* merge

* add unit tests

* edit error message
  • Loading branch information
Eric-B-Wu authored Jan 21, 2025
1 parent 32a579e commit 16fc422
Show file tree
Hide file tree
Showing 54 changed files with 489 additions and 212 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"error",
{
"argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
"destructuredArrayIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"@typescript-eslint/explicit-module-boundary-types": "off",
Expand Down
1 change: 1 addition & 0 deletions apps/Standalone/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
{
argsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const Artifact = {
ConnectionsFile: 'connections.json',
ParametersFile: 'parameters.json',
WorkflowFile: 'workflow.json',
HostFile: 'host.json',
} as const;

export interface ArtifactProperties {
Expand Down Expand Up @@ -105,7 +106,7 @@ export interface ConnectionsData {
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ParametersData extends Record<string, Parameter> {}
export type ParametersData = Record<string, Parameter>;

export interface Parameter {
name?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const fetchAppsByQuery = async (query: string): Promise<any[]> => {
return await requestPage(value, pageNum + 1, $skipToken);
}
return value;
} catch (error) {
} catch (_e) {
return value;
}
};
Expand Down
2 changes: 0 additions & 2 deletions apps/Standalone/src/designer/state/store.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import workflowSlice from './workflowLoadingSlice';
import { configureStore } from '@reduxjs/toolkit';

// eslint-disable-next-line @typescript-eslint/no-unused-vars

export const store = configureStore({
reducer: {
workflowLoader: workflowSlice,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { extractConnectionDetails, changeAuthTypeToRaw } from '../cloudToLocalHelper';
import { extractConnectionDetails } from '../cloudToLocalHelper';
import type { ConnectionReferenceModel } from '@microsoft/vscode-extension-logic-apps';
import { describe, it, expect, vi } from 'vitest';
import { beforeEach } from 'vitest';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
supportedSchemaFileExts,
supportedCustomXsltFileExts,
} from './extensionConfig';
import { type SchemaType, type MapMetadata, type IFileSysTreeItem, LogEntryLevel } from '@microsoft/logic-apps-shared';
import { LogEntryLevel } from '@microsoft/logic-apps-shared';
import type { SchemaType, MapMetadata, IFileSysTreeItem } from '@microsoft/logic-apps-shared';
import type { IActionContext } from '@microsoft/vscode-azext-utils';
import { callWithTelemetryAndErrorHandlingSync } from '@microsoft/vscode-azext-utils';
import type { MapDefinitionData, MessageToVsix, MessageToWebview } from '@microsoft/vscode-extension-logic-apps';
Expand Down
2 changes: 1 addition & 1 deletion apps/vs-code-designer/src/app/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ async function getLogicAppsPicks(
* @param context {@link IActionContext}
* @param fsPath publish path for logic app extension
*/
async function cleanupPublishBinPath(context: IActionContext, fsPath: string): Promise<void> {
async function cleanupPublishBinPath(_context: IActionContext, fsPath: string): Promise<void> {
const netFxWorkerBinPath = path.join(fsPath, 'bin', 'NetFxWorker');
const netFxWorkerAssetPath = path.join(fsPath, 'NetFxWorker');
if (await fse.pathExists(netFxWorkerBinPath)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import {
addConnectionData,
getConnectionsAndSettingsToUpdate,
getConnectionsFromFile,
getCustomCodeFromFiles,
getCustomCodeToUpdate,
getLogicAppProjectRoot,
getParametersFromFile,
saveConnectionReferences,
saveCustomCodeStandard,
} from '../../../utils/codeless/connection';
import { saveWorkflowParameter } from '../../../utils/codeless/parameter';
import { startDesignTimeApi } from '../../../utils/codeless/startDesignTimeApi';
Expand Down Expand Up @@ -251,14 +254,14 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase {

await window.withProgress(options, async () => {
try {
const { definition, connectionReferences, parameters } = workflowToSave;
const { definition, connectionReferences, parameters, customCodeData } = workflowToSave;
const definitionToSave: any = definition;
const parametersFromDefinition = parameters;
const projectPath = await getLogicAppProjectRoot(this.context, filePath);

workflow.definition = definitionToSave;

if (connectionReferences) {
const projectPath = await getLogicAppProjectRoot(this.context, filePath);
const connectionsAndSettingsToUpdate = await getConnectionsAndSettingsToUpdate(
this.context,
projectPath,
Expand All @@ -271,6 +274,11 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase {
await saveConnectionReferences(this.context, projectPath, connectionsAndSettingsToUpdate);
}

if (customCodeData) {
const customCodeToUpdate = await getCustomCodeToUpdate(this.context, filePath, customCodeData);
await saveCustomCodeStandard(filePath, customCodeToUpdate);
}

if (parametersFromDefinition) {
delete parametersFromDefinition.$connections;
for (const parameterKey of Object.keys(parametersFromDefinition)) {
Expand Down Expand Up @@ -417,6 +425,9 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase {
const connectionsData: string = await getConnectionsFromFile(this.context, this.workflowFilePath);
const projectPath: string | undefined = await getLogicAppProjectRoot(this.context, this.workflowFilePath);
const parametersData: Record<string, Parameter> = await getParametersFromFile(this.context, this.workflowFilePath);
const customCodeData: Record<string, string> = await getCustomCodeFromFiles(this.workflowFilePath);
const workflowDetails = await getManualWorkflowsInLocalProject(projectPath, this.workflowName);
const artifacts = await getArtifactsInLocalProject(projectPath);
let localSettings: Record<string, string>;
let azureDetails: AzureConnectorDetails;

Expand All @@ -432,14 +443,15 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase {
appSettingNames: Object.keys(localSettings),
standardApp: getStandardAppData(this.workflowName, workflowContent),
connectionsData,
customCodeData,
parametersData,
localSettings,
azureDetails,
accessToken: azureDetails.accessToken,
workflowContent,
workflowDetails: await getManualWorkflowsInLocalProject(projectPath, this.workflowName),
workflowDetails,
workflowName: this.workflowName,
artifacts: await getArtifactsInLocalProject(projectPath),
artifacts,
schemaArtifacts: this.schemaArtifacts,
mapArtifacts: this.mapArtifacts,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
getAzureConnectorDetailsForLocalProject,
getStandardAppData,
} from '../../../utils/codeless/common';
import { getConnectionsFromFile, getLogicAppProjectRoot, getParametersFromFile } from '../../../utils/codeless/connection';
import {
getConnectionsFromFile,
getCustomCodeFromFiles,
getLogicAppProjectRoot,
getParametersFromFile,
} from '../../../utils/codeless/connection';
import { sendRequest } from '../../../utils/requestUtils';
import { OpenMonitoringViewBase } from './openMonitoringViewBase';
import { getTriggerName, HTTP_METHODS } from '@microsoft/logic-apps-shared';
Expand Down Expand Up @@ -166,6 +171,7 @@ export default class OpenMonitoringViewForLocal extends OpenMonitoringViewBase {
const projectPath: string | undefined = await getLogicAppProjectRoot(this.context, this.workflowFilePath);
const workflowContent: any = JSON.parse(readFileSync(this.workflowFilePath, 'utf8'));
const parametersData: Record<string, Parameter> = await getParametersFromFile(this.context, this.workflowFilePath);
const customCodeData: Record<string, string> = await getCustomCodeFromFiles(this.workflowFilePath);
let localSettings: Record<string, string>;
let azureDetails: AzureConnectorDetails;

Expand All @@ -182,6 +188,7 @@ export default class OpenMonitoringViewForLocal extends OpenMonitoringViewBase {
connectionsData,
localSettings,
parametersData,
customCodeData,
azureDetails,
accessToken: azureDetails.accessToken,
workflowName: this.workflowName,
Expand Down
65 changes: 64 additions & 1 deletion apps/vs-code-designer/src/app/utils/codeless/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getContainingWorkspace } from '../workspace';
import { getWorkflowParameters } from './common';
import { getAuthorizationToken } from './getAuthorizationToken';
import { getParametersJson, saveWorkflowParameterRecords } from './parameter';
import { deleteCustomCode, getCustomCode, getCustomCodeAppFilesToUpdate, uploadCustomCode } from './customcode';
import { addNewFileInCSharpProject } from './updateBuildFile';
import { HTTP_METHODS, isString } from '@microsoft/logic-apps-shared';
import type { ParsedSite } from '@microsoft/vscode-azext-azureappservice';
Expand All @@ -28,6 +29,8 @@ import type {
ConnectionAcl,
ConnectionAndAppSetting,
Parameter,
CustomCodeFileNameMapping,
AllCustomCodeFiles,
} from '@microsoft/vscode-extension-logic-apps';
import { JwtTokenHelper, JwtTokenConstants, resolveConnectionsReferences } from '@microsoft/vscode-extension-logic-apps';
import axios from 'axios';
Expand All @@ -48,6 +51,20 @@ export async function getParametersFromFile(context: IActionContext, workflowFil
return getParametersJson(projectRoot);
}

async function getCustomCodeAppFiles(
context: IActionContext,
workflowFilePath: string,
customCode: CustomCodeFileNameMapping
): Promise<Record<string, string>> {
const projectRoot: string = await getLogicAppProjectRoot(context, workflowFilePath);
return getCustomCodeAppFilesToUpdate(projectRoot, customCode);
}

export async function getCustomCodeFromFiles(workflowFilePath: string): Promise<Record<string, string>> {
const workspaceFolder = path.dirname(workflowFilePath);
return getCustomCode(workspaceFolder);
}

export async function getConnectionsJson(projectRoot: string): Promise<string> {
const connectionFilePath: string = path.join(projectRoot, connectionsFileName);
if (await fse.pathExists(connectionFilePath)) {
Expand Down Expand Up @@ -303,6 +320,52 @@ export async function getConnectionsAndSettingsToUpdate(
};
}

export async function getCustomCodeToUpdate(
context: IActionContext,
filePath: string,
customCode: CustomCodeFileNameMapping
): Promise<AllCustomCodeFiles | undefined> {
const filteredCustomCodeMapping: CustomCodeFileNameMapping = {};
const originalCustomCodeData = Object.keys(await getCustomCodeFromFiles(filePath));
if (!customCode || Object.keys(customCode).length === 0) {
return;
}

const appFiles = await getCustomCodeAppFiles(context, filePath, customCode);
Object.entries(customCode).forEach(([fileName, customCodeData]) => {
const { isModified, isDeleted } = customCodeData;
if ((isDeleted && originalCustomCodeData.includes(fileName)) || (isModified && !isDeleted)) {
filteredCustomCodeMapping[fileName] = { ...customCodeData };
}
});
return { customCodeFiles: filteredCustomCodeMapping, appFiles };
}

export async function saveCustomCodeStandard(filePath: string, allCustomCodeFiles?: AllCustomCodeFiles): Promise<void> {
const { customCodeFiles: customCode, appFiles } = allCustomCodeFiles ?? {};
if (!customCode || Object.keys(customCode).length === 0) {
return;
}
try {
const projectPath = await getLogicAppProjectRoot(this.context, filePath);
const workspaceFolder = path.dirname(filePath);
// to prevent 404's we first check which custom code files are already present before deleting
Object.entries(customCode).forEach(([fileName, customCodeData]) => {
const { isModified, isDeleted, fileData } = customCodeData;
if (isDeleted) {
deleteCustomCode(workspaceFolder, fileName);
} else if (isModified && fileData) {
uploadCustomCode(workspaceFolder, fileName, fileData);
}
});
// upload the app files needed for powershell actions
Object.entries(appFiles ?? {}).forEach(([fileName, fileData]) => uploadCustomCode(projectPath, fileName, fileData));
} catch (error) {
const errorMessage = `Failed to save custom code: ${error}`;
throw new Error(errorMessage);
}
}

export async function saveConnectionReferences(
context: IActionContext,
projectPath: string,
Expand Down Expand Up @@ -385,7 +448,7 @@ export async function createAclInConnectionIfNeeded(
try {
const response = await sendAzureRequest(url, identityWizardContext, HTTP_METHODS.GET, site.subscription);
connectionAcls = response.parsedBody.value;
} catch (error) {
} catch (_error) {
connectionAcls = [];
}

Expand Down
93 changes: 93 additions & 0 deletions apps/vs-code-designer/src/app/utils/codeless/customcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as path from 'path';
import * as fse from 'fs-extra';
import { localize } from '../../../localize';
import { parseError } from '@microsoft/vscode-azext-utils';
import { hostFileName, powershellRequirementsFileName, workflowFileName } from '../../../constants';
import type { CustomCodeFileNameMapping } from '@microsoft/vscode-extension-logic-apps';
import { parseJson } from '../parseJson';
import { getAppFileForFileExtension } from '@microsoft/logic-apps-shared';
/**
* Retrieves the custom code files.
* @param {string} workflowFilePath The path to the workflow folder.
* @returns A promise that resolves to a Record<string, string> object representing the custom code files.
* @throws An error if the custom code files cannot be parsed.
*/
export async function getCustomCode(workflowFilePath: string): Promise<Record<string, string>> {
const customCodeFiles: Record<string, string> = {};
try {
const subPaths: string[] = await fse.readdir(workflowFilePath);
for (const subPath of subPaths) {
const fullPath: string = path.join(workflowFilePath, subPath);

if ((await fse.pathExists(fullPath)) && subPath !== workflowFileName) {
if ((await fse.stat(fullPath)).isFile()) {
customCodeFiles[subPath] = await fse.readFile(fullPath, 'utf8');
}
}
}
} catch (error) {
const message: string = localize('failedToParse', 'Failed to parse "{0}": {1}.', workflowFilePath, parseError(error).message);
throw new Error(message);
}

return customCodeFiles;
}

export async function getCustomCodeAppFilesToUpdate(
workflowFilePath: string,
customCodeFiles?: CustomCodeFileNameMapping
): Promise<Record<string, string>> {
// // only powershell files have custom app files
// // to reduce the number of requests, we only check if there are any modified powershell files
if (!customCodeFiles || !Object.values(customCodeFiles).some((file) => file.isModified && file.fileExtension === '.ps1')) {
return {};
}
const appFiles: Record<string, string> = {};
const hostFilePath: string = path.join(workflowFilePath, hostFileName);
if (await fse.pathExists(hostFilePath)) {
const data: string = (await fse.readFile(hostFilePath)).toString();
if (/[^\s]/.test(data)) {
try {
const hostFile: any = parseJson(data);
if (!hostFile.managedDependency?.enabled) {
hostFile.managedDependency = {
enabled: true,
};
appFiles['host.json'] = JSON.stringify(hostFile, null, 2);
}
} catch (error) {
const message: string = localize('failedToParse', 'Failed to parse "{0}": {1}.', hostFileName, parseError(error).message);
throw new Error(message);
}
}
}
const requirementsFilePath: string = path.join(workflowFilePath, powershellRequirementsFileName);
if (!(await fse.pathExists(requirementsFilePath))) {
appFiles['requirements.psd1'] = getAppFileForFileExtension('.ps1');
}
return appFiles;
}

export async function uploadCustomCode(workflowFilePath: string, fileName: string, fileData: string): Promise<void> {
const filePath: string = path.join(workflowFilePath, fileName);
try {
await fse.writeFile(filePath, fileData, 'utf8');
} catch (error) {
const message: string = localize('Failed to write file at "{0}": {1}', filePath, parseError(error).message);
throw new Error(message);
}
}

export async function deleteCustomCode(workflowFilePath: string, fileName: string): Promise<void> {
const filePath: string = path.join(workflowFilePath, fileName);
try {
if (await fse.pathExists(filePath)) {
await fse.unlink(filePath);
} else {
console.warn(`File at "${filePath}" does not exist.`);
}
} catch (error) {
const message = localize('Failed to delete file at "{0}": {1}', filePath, parseError(error).message);
throw new Error(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ function extractPinnedVersion(input: string): string | null {
export async function checkFuncProcessId(): Promise<boolean> {
let correctId = false;
if (os.platform() === Platform.windows) {
await pstree(ext.designChildProcess.pid, (err, children) => {
await pstree(ext.designChildProcess.pid, (_err, children) => {
children.forEach((p) => {
if (p.PID === ext.designChildFuncProcessId && (p.COMMAND || p.COMM) === 'func.exe') {
correctId = true;
Expand Down
1 change: 1 addition & 0 deletions apps/vs-code-designer/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const extensionsFileName = 'extensions.json';
export const vscodeFolderName = '.vscode';
export const workflowFileName = 'workflow.json';
export const funcIgnoreFileName = '.funcignore';
export const powershellRequirementsFileName = 'requirements.psd1';

// Directories names
export const deploymentsDirectory = 'deployments';
Expand Down
Loading

0 comments on commit 16fc422

Please sign in to comment.