From 463f482d279b819e2ce24e92fa18210bc48ef841 Mon Sep 17 00:00:00 2001 From: kitzkan Date: Thu, 19 Sep 2024 17:47:16 +0100 Subject: [PATCH] Revise launch config (#2347) * remove fs node module * removing write application info logic * return file editor * fix launch config tests * pnpm recursive install * add changeset * remove exports of ftns from workspace manager to test * refactor createLaunchConfig * Linting auto fix commit * reuse createLaunchConfig * pnpm install updates * launch config review * Update project data source types in types.ts * fixing lint issues --------- Co-authored-by: github-actions[bot] Co-authored-by: Klaus Keller <66327622+Klaus-Keller@users.noreply.github.com> --- .changeset/tidy-papayas-film.md | 6 + packages/launch-config/package.json | 2 - .../launch-config/src/debug-config/config.ts | 30 +- .../src/debug-config/workspaceManager.ts | 25 +- packages/launch-config/src/index.ts | 2 +- .../src/launch-config-crud/create.ts | 249 ++++++++------- packages/launch-config/src/types/types.ts | 18 +- .../test/debug-config/config.test.ts | 31 +- .../configureLaunchConfig.test.ts | 244 -------------- .../test/debug-config/helpers.test.ts | 6 +- .../debug-config/workspaceManager.test.ts | 149 ++++----- .../test/launch-config-crud/create.test.ts | 298 +++++++++++++++++- .../test/launch-config-crud/update.test.ts | 3 +- packages/launch-config/tsconfig.json | 6 - pnpm-lock.yaml | 6 - 15 files changed, 568 insertions(+), 507 deletions(-) create mode 100644 .changeset/tidy-papayas-film.md delete mode 100644 packages/launch-config/test/debug-config/configureLaunchConfig.test.ts diff --git a/.changeset/tidy-papayas-film.md b/.changeset/tidy-papayas-film.md new file mode 100644 index 0000000000..093413332b --- /dev/null +++ b/.changeset/tidy-papayas-film.md @@ -0,0 +1,6 @@ +--- +'@sap-ux/launch-config': minor +--- + +Reverted the use of Node.js `fs` modules and replaced them with `mem-fs` for writing launch config files & Removed `writeApplicationInfoSettings()` from `@sap-ux/launch-config` +Refactoring create launch config functionalities. \ No newline at end of file diff --git a/packages/launch-config/package.json b/packages/launch-config/package.json index b6fd0eaf62..ce20331e55 100644 --- a/packages/launch-config/package.json +++ b/packages/launch-config/package.json @@ -34,8 +34,6 @@ "@sap-ux/project-access": "workspace:*", "@sap-ux/ui5-config": "workspace:*", "@sap-ux/ui5-info": "workspace:*", - "@sap-ux/odata-service-inquirer": "workspace:*", - "@sap-ux/store": "workspace:*", "i18next": "23.5.1", "jsonc-parser": "3.2.0", "mem-fs": "2.1.0", diff --git a/packages/launch-config/src/debug-config/config.ts b/packages/launch-config/src/debug-config/config.ts index f130269fb6..7e6c6bbf28 100644 --- a/packages/launch-config/src/debug-config/config.ts +++ b/packages/launch-config/src/debug-config/config.ts @@ -1,8 +1,7 @@ -import { DatasourceType, OdataVersion } from '@sap-ux/odata-service-inquirer'; import { basename } from 'path'; import { getLaunchConfig } from '../launch-config-crud/utils'; import type { LaunchConfig, LaunchJSON, DebugOptions, LaunchConfigEnv } from '../types'; -import { FIORI_TOOLS_LAUNCH_CONFIG_HANDLER_ID } from '../types'; +import { FIORI_TOOLS_LAUNCH_CONFIG_HANDLER_ID, ProjectDataSourceType } from '../types'; // debug constants const testFlpSandboxHtml = 'test/flpSandbox.html'; @@ -27,7 +26,7 @@ function getEnvUrlParams(sapClientParam: string): string { } /** - * Creates a launch configuration. + * Gets launch configuration. * * @param {string} name - The name of the configuration. * @param {string} cwd - The current working directory. @@ -37,7 +36,7 @@ function getEnvUrlParams(sapClientParam: string): string { * @param {string} [runConfig] - The optional run configuration for AppStudio. * @returns {LaunchConfig} The launch configuration object. */ -function createLaunchConfig( +function configureLaunchConfig( name: string, cwd: string, runtimeArgs: string[], @@ -56,13 +55,13 @@ function createLaunchConfig( /** * Configures the launch.json file based on provided options. * + * @param rootFolder - The root folder path where the app will be generated. * @param {string} cwd - The current working directory. * @param {DebugOptions} configOpts - Configuration options for the launch.json file. * @returns {LaunchJSON} The configured launch.json object. */ -export function configureLaunchJsonFile(cwd: string, configOpts: DebugOptions): LaunchJSON { +export function configureLaunchJsonFile(rootFolder: string, cwd: string, configOpts: DebugOptions): LaunchJSON { const { - projectPath, isAppStudio, datasourceType, flpAppId, @@ -73,14 +72,13 @@ export function configureLaunchJsonFile(cwd: string, configOpts: DebugOptions): isFioriElement, migratorMockIntent } = configOpts; - - const projectName = basename(projectPath); + const projectName = basename(rootFolder); const flpAppIdWithHash = flpAppId && !flpAppId.startsWith('#') ? `#${flpAppId}` : flpAppId; const startHtmlFile = flpSandboxAvailable ? testFlpSandboxHtml : indexHtml; const runConfig = isAppStudio ? JSON.stringify({ handlerId: FIORI_TOOLS_LAUNCH_CONFIG_HANDLER_ID, - runnableId: projectPath + runnableId: rootFolder }) : undefined; const envUrlParam = getEnvUrlParams(sapClientParam); @@ -88,9 +86,9 @@ export function configureLaunchJsonFile(cwd: string, configOpts: DebugOptions): const launchFile: LaunchJSON = { version: '0.2.0', configurations: [] }; // Add live configuration if the datasource is not from a metadata file - if (datasourceType !== DatasourceType.metadataFile) { + if (datasourceType !== ProjectDataSourceType.metadataFile) { const startCommand = `${startHtmlFile}${flpAppIdWithHash}`; - const liveConfig = createLaunchConfig( + const liveConfig = configureLaunchConfig( `Start ${projectName}`, cwd, ['fiori', 'run'], @@ -102,13 +100,13 @@ export function configureLaunchJsonFile(cwd: string, configOpts: DebugOptions): } // Add mock configuration for OData V2 or V4 - if (odataVersion && [OdataVersion.v2, OdataVersion.v4].includes(odataVersion)) { + if (odataVersion && ['2.0', '4.0'].includes(odataVersion)) { const params = `${flpAppIdWithHash ?? ''}`; const mockCmdArgs = - isMigrator && odataVersion === OdataVersion.v2 + isMigrator && odataVersion === '2.0' ? ['--open', `${testFlpSandboxMockServerHtml}${params}`] : ['--config', './ui5-mock.yaml', '--open', `${testFlpSandboxHtml}${params}`]; - const mockConfig = createLaunchConfig( + const mockConfig = configureLaunchConfig( `Start ${projectName} Mock`, cwd, ['fiori', 'run'], @@ -120,12 +118,12 @@ export function configureLaunchJsonFile(cwd: string, configOpts: DebugOptions): } // Add local configuration - const shouldUseMockServer = isFioriElement && odataVersion === OdataVersion.v2 && isMigrator; + const shouldUseMockServer = isFioriElement && odataVersion === '2.0' && isMigrator; const localHtmlFile = shouldUseMockServer ? testFlpSandboxMockServerHtml : startHtmlFile; const startLocalCommand = `${localHtmlFile}${ migratorMockIntent ? `#${migratorMockIntent.replace('#', '')}` : flpAppIdWithHash }`; - const localConfig = createLaunchConfig( + const localConfig = configureLaunchConfig( `Start ${projectName} Local`, cwd, ['fiori', 'run'], diff --git a/packages/launch-config/src/debug-config/workspaceManager.ts b/packages/launch-config/src/debug-config/workspaceManager.ts index 6dd8d0b17a..6222d65010 100644 --- a/packages/launch-config/src/debug-config/workspaceManager.ts +++ b/packages/launch-config/src/debug-config/workspaceManager.ts @@ -10,7 +10,7 @@ import { formatCwd, getLaunchJsonPath, isFolderInWorkspace, handleAppsNotInWorks * @param {any} vscode - The VS Code API object. * @returns {WorkspaceHandlerInfo} An object containing the path to the `launch.json` configuration file and the cwd for the launch configuration. */ -export function handleUnsavedWorkspace(projectPath: string, vscode: any): WorkspaceHandlerInfo { +function handleUnsavedWorkspace(projectPath: string, vscode: any): WorkspaceHandlerInfo { const workspace = vscode.workspace; const wsFolder = workspace.getWorkspaceFolder(vscode.Uri.file(projectPath))?.uri?.fsPath; const nestedFolder = relative(wsFolder ?? projectPath, projectPath); @@ -31,7 +31,7 @@ export function handleUnsavedWorkspace(projectPath: string, vscode: any): Worksp * @param {any} vscode - The VS Code API object. * @returns {WorkspaceHandlerInfo} An object containing the path to the `launch.json` configuration file and the cwd for the launch configuration. */ -export function handleSavedWorkspace( +function handleSavedWorkspace( projectPath: string, projectName: string, targetFolder: string, @@ -58,7 +58,7 @@ export function handleSavedWorkspace( * @param {any} vscode - The VS Code API object. * @returns {WorkspaceHandlerInfo} An object containing the path to the `launch.json` configuration file and the cwd for the launch configuration. */ -export function handleOpenFolderButNoWorkspaceFile( +function handleOpenFolderButNoWorkspaceFile( projectPath: string, targetFolder: string, isAppStudio: boolean, @@ -83,6 +83,7 @@ export function handleOpenFolderButNoWorkspaceFile( * This function handles different scenarios depending on whether a workspace is open, * whether the project is inside or outside of a workspace, and other factors. * + * @param rootFolder - The root folder path where the app will be generated. * @param {DebugOptions} options - The options used to determine how to manage the workspace configuration. * @param {string} options.projectPath -The project's path including project name. * @param {boolean} [options.isAppStudio] - A boolean indicating whether the current environment is BAS. @@ -90,30 +91,30 @@ export function handleOpenFolderButNoWorkspaceFile( * @param {any} options.vscode - The VS Code API object. * @returns {WorkspaceHandlerInfo} An object containing the path to the `launch.json` configuration file, the cwd command, workspaceFolderUri if provided will enable reload. */ -export function handleWorkspaceConfig(options: DebugOptions): WorkspaceHandlerInfo { - const { projectPath, isAppStudio = false, writeToAppOnly = false, vscode } = options; +export function handleWorkspaceConfig(rootFolder: string, options: DebugOptions): WorkspaceHandlerInfo { + const { isAppStudio = false, writeToAppOnly = false, vscode } = options; - const projectName = basename(projectPath); - const targetFolder = dirname(projectPath); + const projectName = basename(rootFolder); + const targetFolder = dirname(rootFolder); // Directly handle the case where we ignore workspace settings if (writeToAppOnly) { - return handleAppsNotInWorkspace(projectPath, isAppStudio, vscode); + return handleAppsNotInWorkspace(rootFolder, isAppStudio, vscode); } const workspace = vscode.workspace; const workspaceFile = workspace?.workspaceFile; // Handles the scenario where no workspace or folder is open in VS Code. if (!workspace) { - return handleAppsNotInWorkspace(projectPath, isAppStudio, vscode); + return handleAppsNotInWorkspace(rootFolder, isAppStudio, vscode); } // Handle case where a folder is open, but not a workspace file if (!workspaceFile) { - return handleOpenFolderButNoWorkspaceFile(projectPath, targetFolder, isAppStudio, vscode); + return handleOpenFolderButNoWorkspaceFile(rootFolder, targetFolder, isAppStudio, vscode); } // Handles the case where a previously saved workspace is open if (workspaceFile.scheme === 'file') { - return handleSavedWorkspace(projectPath, projectName, targetFolder, isAppStudio, vscode); + return handleSavedWorkspace(rootFolder, projectName, targetFolder, isAppStudio, vscode); } // Handles the case where an unsaved workspace is open - return handleUnsavedWorkspace(projectPath, vscode); + return handleUnsavedWorkspace(rootFolder, vscode); } diff --git a/packages/launch-config/src/index.ts b/packages/launch-config/src/index.ts index d38fecfed7..fae4f11272 100644 --- a/packages/launch-config/src/index.ts +++ b/packages/launch-config/src/index.ts @@ -1,5 +1,5 @@ export * from './types'; -export { createLaunchConfig, configureLaunchConfig } from './launch-config-crud/create'; +export { createLaunchConfig } from './launch-config-crud/create'; export { deleteLaunchConfig } from './launch-config-crud/delete'; export { convertOldLaunchConfigToFioriRun } from './launch-config-crud/modify'; export { getLaunchConfigs, getLaunchConfigByName } from './launch-config-crud/read'; diff --git a/packages/launch-config/src/launch-config-crud/create.ts b/packages/launch-config/src/launch-config-crud/create.ts index 626da87ef3..d21a715bd4 100644 --- a/packages/launch-config/src/launch-config-crud/create.ts +++ b/packages/launch-config/src/launch-config-crud/create.ts @@ -2,73 +2,102 @@ import { create as createStorage } from 'mem-fs'; import { create } from 'mem-fs-editor'; import { join, basename } from 'path'; import { DirName } from '@sap-ux/project-access'; -import { LAUNCH_JSON_FILE } from '../types'; -import type { FioriOptions, LaunchJSON, UpdateWorkspaceFolderOptions, DebugOptions } from '../types'; +import { LAUNCH_JSON_FILE, ProjectDataSourceType } from '../types'; +import type { FioriOptions, LaunchJSON, UpdateWorkspaceFolderOptions, DebugOptions, LaunchConfig } from '../types'; import type { Editor } from 'mem-fs-editor'; import { generateNewFioriLaunchConfig } from './utils'; import { updateLaunchJSON } from './writer'; import { parse } from 'jsonc-parser'; import { handleWorkspaceConfig } from '../debug-config/workspaceManager'; import { configureLaunchJsonFile } from '../debug-config/config'; -import { getFioriToolsDirectory } from '@sap-ux/store'; import type { Logger } from '@sap-ux/logger'; -import { DatasourceType } from '@sap-ux/odata-service-inquirer'; import { t } from '../i18n'; -import fs from 'fs'; /** - * Enhance or create the launch.json file with new launch config. + * Writes the `launch.json` file with the specified configurations. If the file already exists, it will be overwritten. * - * @param rootFolder - workspace root folder. - * @param fioriOptions - options for the new launch config. - * @param fs - optional, the memfs editor instance. - * @returns memfs editor instance. + * @param {Editor} fs - The file system editor used to write the `launch.json` file. + * @param {string} launchJSONPath - The full path to the `launch.json` file. + * @param {LaunchConfig[]} configurations - An array of launch configurations to be included in the `launch.json` file. + * @returns {void} */ -export async function createLaunchConfig(rootFolder: string, fioriOptions: FioriOptions, fs?: Editor): Promise { - if (!fs) { - fs = create(createStorage()); - } - const launchJSONPath = join(rootFolder, DirName.VSCode, LAUNCH_JSON_FILE); - if (fs.exists(launchJSONPath)) { +function writeLaunchJsonFile(fs: Editor, launchJSONPath: string, configurations: LaunchConfig[]): void { + const newLaunchJSONContent = { version: '0.2.0', configurations }; + fs.write(launchJSONPath, JSON.stringify(newLaunchJSONContent, null, 4)); +} + +/** + * Handles the case where there are no debug options provided. It either enhances an existing `launch.json` + * file with a new launch configuration or creates a new `launch.json` file with the initial configuration. + * + * @param {string} rootFolder - The root directory where the `launch.json` file is located or will be created. + * @param {FioriOptions} fioriOptions - The options used to generate the new launch configuration for the `launch.json` file. + * @param {Editor} fs - The file system editor used to read and write the `launch.json` file. + * @returns {Promise} - A promise that resolves with the file system editor after the `launch.json` file has been + * updated or created. + */ +async function handleNoDebugOptions(rootFolder: string, fioriOptions: FioriOptions, fs: Editor): Promise { + const launchJsonWritePath = join(rootFolder, DirName.VSCode, LAUNCH_JSON_FILE); + if (fs.exists(launchJsonWritePath)) { // launch.json exists, enhance existing file with new config const launchConfig = generateNewFioriLaunchConfig(rootFolder, fioriOptions); - const launchJsonString = fs.read(launchJSONPath); + const launchJsonString = fs.read(launchJsonWritePath); const launchJson = parse(launchJsonString) as LaunchJSON; await updateLaunchJSON( launchConfig, - launchJSONPath, + launchJsonWritePath, ['configurations', launchJson.configurations.length + 1], { isArrayInsertion: true }, fs ); - } else { - // launch.json is missing, new file with new config - const configurations = generateNewFioriLaunchConfig(rootFolder, fioriOptions); - const newLaunchJSONContent = { version: '0.2.0', configurations: [configurations] }; - fs.write(launchJSONPath, JSON.stringify(newLaunchJSONContent, null, 4)); + return fs; } + // launch.json is missing, new file with new config + const configurations = [generateNewFioriLaunchConfig(rootFolder, fioriOptions)]; + writeLaunchJsonFile(fs, launchJsonWritePath, configurations); return fs; } /** - * Writes the application info settings to the appInfo.json file. - * Adds the specified path to the latestGeneratedFiles array. + * Updates or replaces the `launch.json` file depending on whether the file should be replaced + * or enhanced with additional configurations. If `replaceWithNew` is true, the entire file + * content is replaced with the new configurations. Otherwise, the configurations are added + * to the existing `launch.json`. * - * @param {string} path - The project file path to add. - * @param log - The logger instance. + * @param {Editor} fs - The file system editor to read and write the `launch.json` file. + * @param {string} launchJSONPath - The path to the existing `launch.json` file. + * @param {LaunchConfig[]} configurations - An array of new launch configurations to be added or replaced. + * @param {boolean} replaceWithNew - A flag indicating whether to replace the existing `launch.json` + * with new configurations (`true`) or append to the existing ones (`false`). + * @returns {Promise} - A promise that resolves once the `launch.json` file has been updated or replaced. */ -export function writeApplicationInfoSettings(path: string, log?: Logger): void { - const appInfoFilePath: string = getFioriToolsDirectory(); - const appInfoContents = fs.existsSync(appInfoFilePath) - ? JSON.parse(fs.readFileSync(appInfoFilePath, 'utf-8')) - : { latestGeneratedFiles: [] }; - appInfoContents.latestGeneratedFiles.push(path); - try { - fs.writeFileSync(appInfoFilePath, JSON.stringify(appInfoContents, null, 2)); - } catch (error) { - log?.error(t('errorAppInfoFile', { error: error })); +async function handleExistingLaunchJson( + fs: Editor, + launchJSONPath: string, + configurations: LaunchConfig[], + replaceWithNew: boolean = false +): Promise { + const launchJsonString = fs.read(launchJSONPath); + const launchJson = parse(launchJsonString) as LaunchJSON; + if (replaceWithNew) { + // replaceWithNew is needed in cases where launch config exists in + // `.vscode` but isn't added to the workspace. If `replaceWithNew` is `true`, it indicates that the app is not + // in the workspace, so the entire `launch.json` and replaced since launch config is then generated in app folder. + writeLaunchJsonFile(fs, launchJSONPath, configurations); + } else { + for (const config of configurations) { + await updateLaunchJSON( + config, + launchJSONPath, + ['configurations', launchJson.configurations.length + 1], + { + isArrayInsertion: true + }, + fs + ); + } } } @@ -76,18 +105,10 @@ export function writeApplicationInfoSettings(path: string, log?: Logger): void { * Updates the workspace folders in VSCode if the update options are provided. * * @param {UpdateWorkspaceFolderOptions} updateWorkspaceFolders - The options for updating workspace folders. - * @param {string} rootFolderPath - The root folder path of the project. - * @param log - The logger instance. */ -export function updateWorkspaceFoldersIfNeeded( - updateWorkspaceFolders: UpdateWorkspaceFolderOptions | undefined, - rootFolderPath: string, - log?: Logger -): void { +function updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders?: UpdateWorkspaceFolderOptions): void { if (updateWorkspaceFolders) { const { uri, vscode, projectName } = updateWorkspaceFolders; - writeApplicationInfoSettings(rootFolderPath, log); - if (uri && vscode) { const currentWorkspaceFolders = vscode.workspace.workspaceFolders || []; vscode.workspace.updateWorkspaceFolders(currentWorkspaceFolders.length, undefined, { @@ -99,79 +120,85 @@ export function updateWorkspaceFoldersIfNeeded( } /** - * Creates or updates the launch.json file with the provided configurations. + * Handles the creation and configuration of the `launch.json` file based on debug options. + * This function processes workspace configuration, updates the `launch.json` file if it exists, + * and creates it if it does not. Additionally, it updates workspace folders if applicable. * - * @param {string} rootFolderPath - The root folder path of the project. - * @param {LaunchJSON} launchJsonFile - The launch.json configuration to write. - * @param {UpdateWorkspaceFolderOptions} [updateWorkspaceFolders] - Optional workspace folder update options. - * @param {boolean} appNotInWorkspace - Indicates if the app is not in the workspace. - * @param log - The logger instance. + * @param rootFolder - root folder. + * @param {Editor} fs - The file system editor to read and write the `launch.json` file. + * @param {DebugOptions} debugOptions - Debug configuration options that dictate how the `launch.json` + * should be generated and what commands should be logged. + * @param {Logger} logger - Logger instance for logging information or warnings. + * @returns {Promise} - Returns the file system editor after potentially modifying the workspace + * and updating or creating the `launch.json` file. */ -export function createOrUpdateLaunchConfigJSON( - rootFolderPath: string, - launchJsonFile?: LaunchJSON, - updateWorkspaceFolders?: UpdateWorkspaceFolderOptions, - appNotInWorkspace: boolean = false, - log?: Logger -): void { - try { - const launchJSONPath = join(rootFolderPath, DirName.VSCode, LAUNCH_JSON_FILE); - if (fs.existsSync(launchJSONPath) && !appNotInWorkspace) { - const existingLaunchConfig = parse(fs.readFileSync(launchJSONPath, 'utf-8')) as LaunchJSON; - const updatedConfigurations = existingLaunchConfig.configurations.concat( - launchJsonFile?.configurations ?? [] - ); - fs.writeFileSync( - launchJSONPath, - JSON.stringify({ ...existingLaunchConfig, configurations: updatedConfigurations }, null, 4) - ); - } else { - const dotVscodePath = join(rootFolderPath, DirName.VSCode); - fs.mkdirSync(dotVscodePath, { recursive: true }); - const path = join(dotVscodePath, 'launch.json'); - fs.writeFileSync(path, JSON.stringify(launchJsonFile ?? {}, null, 4), 'utf8'); - } - } catch (error) { - log?.error(t('errorLaunchFile', { error: error })); +async function handleDebugOptions( + rootFolder: string, + fs: Editor, + debugOptions: DebugOptions, + logger?: Logger +): Promise { + const { launchJsonPath, workspaceFolderUri, cwd, appNotInWorkspace } = handleWorkspaceConfig( + rootFolder, + debugOptions + ); + const configurations = configureLaunchJsonFile(rootFolder, cwd, debugOptions).configurations; + + const npmCommand = debugOptions.datasourceType === ProjectDataSourceType.metadataFile ? 'run start-mock' : 'start'; + logger?.info( + t('startServerMessage', { + folder: basename(rootFolder), + npmCommand + }) + ); + const launchJsonWritePath = join(launchJsonPath, DirName.VSCode, LAUNCH_JSON_FILE); + if (fs.exists(launchJsonWritePath)) { + await handleExistingLaunchJson(fs, launchJsonWritePath, configurations, appNotInWorkspace); + } else { + writeLaunchJsonFile(fs, launchJsonWritePath, configurations); } - updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders, rootFolderPath, log); + + // The `workspaceFolderUri` is a URI obtained from VS Code that specifies the path to the workspace folder. + // This URI is populated when a reload of the workspace is required. It allows us to identify and update + // the workspace folder correctly within VS Code. + const updateWorkspaceFolders = workspaceFolderUri + ? ({ + uri: workspaceFolderUri, + projectName: basename(rootFolder), + vscode: debugOptions.vscode + } as UpdateWorkspaceFolderOptions) + : undefined; + + updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders); + return fs; } /** - * Generates and creates launch configuration for the project based on debug options. + * Enhance or create the launch.json file with new launch config. * - * @param {DebugOptions} options - The options for configuring the debug setup. - * @param log - The logger instance. + * @param rootFolder - workspace root folder. + * @param fioriOptions - options for the new launch config. + * @param fs - optional, the memfs editor instance. + * @param logger - optional, the logger instance. + * @returns memfs editor instance. */ -export function configureLaunchConfig(options: DebugOptions, log?: Logger): void { - const { datasourceType, projectPath, vscode } = options; - if (datasourceType === DatasourceType.capProject) { - log?.info(t('startApp', { npmStart: '`npm start`', cdsRun: '`cds run --in-memory`' })); - return; +export async function createLaunchConfig( + rootFolder: string, + fioriOptions: FioriOptions, + fs?: Editor, + logger?: Logger +): Promise { + fs = fs ?? create(createStorage()); + const debugOptions = fioriOptions.debugOptions; + if (!debugOptions) { + return await handleNoDebugOptions(rootFolder, fioriOptions, fs); } - if (!vscode) { - return; + if (!debugOptions.vscode) { + return fs; } - const { launchJsonPath, workspaceFolderUri, cwd, appNotInWorkspace } = handleWorkspaceConfig(options); - // construct launch.json file - const launchJsonFile = configureLaunchJsonFile(cwd, options); - // update workspace folders if workspaceFolderUri is available - const updateWorkspaceFolders = workspaceFolderUri - ? { - uri: workspaceFolderUri, - projectName: basename(options.projectPath), - vscode - } - : undefined; - - createOrUpdateLaunchConfigJSON(launchJsonPath, launchJsonFile, updateWorkspaceFolders, appNotInWorkspace, log); - - const npmCommand = datasourceType === DatasourceType.metadataFile ? 'run start-mock' : 'start'; - const projectName = basename(projectPath); - log?.info( - t('startServerMessage', { - folder: projectName, - npmCommand - }) - ); + if (debugOptions.datasourceType === ProjectDataSourceType.capProject) { + logger?.info(t('startApp', { npmStart: '`npm start`', cdsRun: '`cds run --in-memory`' })); + return fs; + } + return await handleDebugOptions(rootFolder, fs, debugOptions, logger); } diff --git a/packages/launch-config/src/types/types.ts b/packages/launch-config/src/types/types.ts index 8cf24216be..5e6e44fd4d 100644 --- a/packages/launch-config/src/types/types.ts +++ b/packages/launch-config/src/types/types.ts @@ -1,6 +1,5 @@ import type { ODataVersion } from '@sap-ux/project-access'; import type { FioriToolsProxyConfigBackend } from '@sap-ux/ui5-config'; -import type { OdataVersion, DatasourceType } from '@sap-ux/odata-service-inquirer'; export enum Arguments { FrameworkVersion = '--framework-version', @@ -21,6 +20,7 @@ export interface FioriOptions { backendConfigs?: FioriToolsProxyConfigBackend[]; urlParameters?: string; visible?: boolean; + debugOptions?: DebugOptions; } export interface LaunchJSON { @@ -60,14 +60,22 @@ export interface LaunchConfigInfo { filePath: string; } +/** + * Enum representing the types of data sources or origins for a project. + * These types indicate how a project is generated. + */ +export enum ProjectDataSourceType { + capProject = 'capProject', + odataServiceUrl = 'odataServiceUrl', + metadataFile = 'metadataFile' +} + /** * Configuration options for debugging launch configurations. */ export interface DebugOptions { - /** Path to the project directory. */ - projectPath: string; /** Type of the data source used in the project. */ - datasourceType: DatasourceType; + datasourceType: ProjectDataSourceType; /** SAP client parameter for the connection. */ sapClientParam: string; /** FLP application ID. */ @@ -75,7 +83,7 @@ export interface DebugOptions { /** Indicates if the FLP sandbox environment is available. */ flpSandboxAvailable: boolean; /** Version of the OData service. */ - odataVersion?: OdataVersion; + odataVersion?: ODataVersion; /** Indicates if the project is a Fiori Element. */ isFioriElement?: boolean; /** Intent parameter for the migrator mock. */ diff --git a/packages/launch-config/test/debug-config/config.test.ts b/packages/launch-config/test/debug-config/config.test.ts index 7a0b31841b..d7142587d4 100644 --- a/packages/launch-config/test/debug-config/config.test.ts +++ b/packages/launch-config/test/debug-config/config.test.ts @@ -1,11 +1,11 @@ import { configureLaunchJsonFile } from '../../src/debug-config/config'; import type { DebugOptions, LaunchConfig, LaunchJSON } from '../../src/types'; import path from 'path'; -import { DatasourceType, OdataVersion } from '@sap-ux/odata-service-inquirer'; -import { FIORI_TOOLS_LAUNCH_CONFIG_HANDLER_ID } from '../../src/types'; +import { FIORI_TOOLS_LAUNCH_CONFIG_HANDLER_ID, ProjectDataSourceType } from '../../src/types'; const projectName = 'project1'; const cwd = `\${workspaceFolder}`; +const projectPath = path.join(__dirname, projectName); // Base configuration template const baseConfigurationObj: Partial = { @@ -56,13 +56,12 @@ describe('debug config tests', () => { beforeEach(() => { configOptions = { vscode: vscodeMock, - projectPath: path.join(__dirname, projectName), - odataVersion: OdataVersion.v2, + odataVersion: '2.0', sapClientParam: '', flpAppId: 'project1-tile', isFioriElement: true, flpSandboxAvailable: true, - datasourceType: DatasourceType.odataServiceUrl + datasourceType: ProjectDataSourceType.odataServiceUrl }; }); @@ -72,7 +71,7 @@ describe('debug config tests', () => { }); it('Should return the correct configuration for OData v2', () => { - const launchFile = configureLaunchJsonFile(cwd, configOptions); + const launchFile = configureLaunchJsonFile(projectPath, cwd, configOptions); expect(launchFile.configurations.length).toBe(3); expect(findConfiguration(launchFile, `Start ${projectName}`)).toEqual(liveConfigurationObj); @@ -81,8 +80,8 @@ describe('debug config tests', () => { }); it('Should return the correct configuration for OData v4', () => { - configOptions.odataVersion = OdataVersion.v4; - const launchFile = configureLaunchJsonFile(cwd, configOptions); + configOptions.odataVersion = '4.0'; + const launchFile = configureLaunchJsonFile(projectPath, cwd, configOptions); expect(launchFile.configurations.length).toBe(3); expect(findConfiguration(launchFile, `Start ${projectName}`)).toEqual(liveConfigurationObj); @@ -91,8 +90,8 @@ describe('debug config tests', () => { }); it('Should return correct configuration for local metadata', () => { - configOptions.datasourceType = DatasourceType.metadataFile; - const launchFile = configureLaunchJsonFile(cwd, configOptions); + configOptions.datasourceType = ProjectDataSourceType.metadataFile; + const launchFile = configureLaunchJsonFile(projectPath, cwd, configOptions); expect(launchFile.configurations.length).toBe(2); expect(findConfiguration(launchFile, `Start ${projectName}`)).toBeUndefined(); @@ -102,7 +101,7 @@ describe('debug config tests', () => { it('Should return correct configuration when project is being migrated', () => { configOptions.isMigrator = true; - const launchFile = configureLaunchJsonFile(cwd, configOptions); + const launchFile = configureLaunchJsonFile(projectPath, cwd, configOptions); const mockConfigWithMigrator = { ...mockConfigurationObj, args: ['--open', 'test/flpSandboxMockServer.html#project1-tile'] @@ -114,7 +113,7 @@ describe('debug config tests', () => { configOptions.isFioriElement = false; configOptions.flpSandboxAvailable = false; configOptions.flpAppId = ''; - const launchFile = configureLaunchJsonFile(cwd, configOptions); + const launchFile = configureLaunchJsonFile(projectPath, cwd, configOptions); const localConfig = { ...localConfigurationObj, args: ['--config', './ui5-local.yaml', '--open', 'index.html'] @@ -124,7 +123,7 @@ describe('debug config tests', () => { it('Should return correct configuration when migrator mock intent is provided', () => { configOptions.migratorMockIntent = 'flpSandboxMockFlpIntent'; - const launchFile = configureLaunchJsonFile(cwd, configOptions); + const launchFile = configureLaunchJsonFile(projectPath, cwd, configOptions); const localConfig = { ...localConfigurationObj, args: ['--config', './ui5-local.yaml', '--open', 'test/flpSandbox.html#flpSandboxMockFlpIntent'] @@ -133,12 +132,12 @@ describe('debug config tests', () => { }); it('Should return correct configuration on BAS and sapClientParam is available', () => { - configOptions.odataVersion = OdataVersion.v2; - configOptions.datasourceType = DatasourceType.odataServiceUrl; + configOptions.odataVersion = '2.0'; + configOptions.datasourceType = ProjectDataSourceType.odataServiceUrl; configOptions.sapClientParam = 'sapClientParam'; configOptions.isAppStudio = true; - const launchFile = configureLaunchJsonFile(cwd, configOptions); + const launchFile = configureLaunchJsonFile(path.join(__dirname, projectName), cwd, configOptions); expect(launchFile.configurations.length).toBe(3); const projectPath = path.join(__dirname, 'project1'); diff --git a/packages/launch-config/test/debug-config/configureLaunchConfig.test.ts b/packages/launch-config/test/debug-config/configureLaunchConfig.test.ts deleted file mode 100644 index 0a4f12249a..0000000000 --- a/packages/launch-config/test/debug-config/configureLaunchConfig.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { join } from 'path'; -import { handleWorkspaceConfig } from '../../src/debug-config/workspaceManager'; -import type { DebugOptions, UpdateWorkspaceFolderOptions, LaunchJSON } from '../../src/types'; -import { LAUNCH_JSON_FILE } from '../../src/types'; -import { - writeApplicationInfoSettings, - updateWorkspaceFoldersIfNeeded, - createOrUpdateLaunchConfigJSON, - configureLaunchConfig -} from '../../src/launch-config-crud/create'; -import { t } from '../../src/i18n'; -import { DatasourceType } from '@sap-ux/odata-service-inquirer'; -import type { Editor } from 'mem-fs-editor'; -import { DirName } from '@sap-ux/project-access'; -import { getFioriToolsDirectory } from '@sap-ux/store'; -import type { Logger } from '@sap-ux/logger'; -import { existsSync, mkdir } from 'fs'; -import fs from 'fs'; - -// Mock dependencies -jest.mock('mem-fs'); -jest.mock('mem-fs-editor'); -jest.mock('jsonc-parser', () => ({ - parse: jest.fn().mockReturnValue({ - configurations: [{ name: 'Existing Config', type: 'node' }] - }) -})); -jest.mock('../../src/debug-config/workspaceManager', () => ({ - handleWorkspaceConfig: jest.fn() -})); -jest.mock('../../src/debug-config/config', () => ({ - configureLaunchJsonFile: jest.fn(), - writeApplicationInfoSettings: jest.requireActual('../../src/debug-config/config').writeApplicationInfoSettings -})); -const mockLog = { - error: jest.fn(), - info: jest.fn() -} as unknown as Logger; - -const mockEditor = { - exists: jest.fn().mockReturnValue(false), - read: jest.fn(), - write: jest.fn() -} as unknown as Editor; -const mockPath = '/mock/project/path'; -// Define a variable to control the behavior of writeFileSync -let writeFileSyncMockBehavior: 'success' | 'error'; - -jest.mock('fs', () => ({ - ...jest.requireActual('fs'), - //mkdirSync: jest.fn(), - existsSync: jest.fn().mockReturnValue(true), - readFileSync: jest.fn((path: string, encoding: string) => { - // Mock different behaviors based on the path - if (path) { - return JSON.stringify({ latestGeneratedFiles: [] }); // Mock file content - } - throw new Error('Simulated read error'); - }), - writeFileSync: jest.fn().mockImplementation(() => { - if (writeFileSyncMockBehavior === 'error') { - throw new Error('Simulated write error'); // Throw an error for `writeFileSync` when behavior is 'error' - } - // Otherwise, assume it succeeds - }) -})); - -// Function to set the behavior for writeFileSync -const setWriteFileSyncBehavior = (behavior: 'success' | 'error') => { - writeFileSyncMockBehavior = behavior; - // Reinitialize the mock to apply the new behavior - fs.writeFileSync = jest.fn().mockImplementation(() => { - if (writeFileSyncMockBehavior === 'error') { - throw new Error(); - } - }); -}; - -describe('Config Functions', () => { - const launchJson = { - configurations: [{ name: 'New Config', type: 'node' }] - } as LaunchJSON; - - const existingLaunchJson = { - configurations: [{ name: 'Existing Config', type: 'node' }] - } as LaunchJSON; - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('writeApplicationInfoSettings', () => { - it('should write application info settings to appInfo.json', () => { - writeApplicationInfoSettings(mockPath, mockLog); - expect(fs.writeFileSync).toHaveBeenCalledWith( - getFioriToolsDirectory(), - JSON.stringify({ latestGeneratedFiles: [mockPath] }, null, 2) - ); - }); - - it('should handle error while writing to appInfo.json', () => { - setWriteFileSyncBehavior('error'); - writeApplicationInfoSettings(mockPath, mockLog); - expect(mockLog.error).toHaveBeenCalledWith(t('errorAppInfoFile')); - }); - }); - - describe('updateWorkspaceFoldersIfNeeded', () => { - it('should update workspace folders if options are provided', () => { - const updateOptions = { - uri: '/mock/uri', - vscode: { - workspace: { - workspaceFolders: [], - updateWorkspaceFolders: jest.fn() - } - }, - projectName: 'Test Project' - } as UpdateWorkspaceFolderOptions; - updateWorkspaceFoldersIfNeeded(updateOptions, '/root/folder/path', mockLog); - expect(updateOptions.vscode.workspace.updateWorkspaceFolders).toHaveBeenCalledWith(0, undefined, { - name: 'Test Project', - uri: '/mock/uri' - }); - }); - - it('should not update workspace folders if no options are provided', () => { - const updateOptions: UpdateWorkspaceFolderOptions | undefined = undefined; - updateWorkspaceFoldersIfNeeded(updateOptions, '/root/folder/path', mockLog); - // No updateWorkspaceFolders call expected hence no app info json written - expect(fs.writeFileSync).not.toHaveBeenCalled(); - }); - }); - - describe('createOrUpdateLaunchConfigJSON', () => { - it('should create a new launch.json file if it does not exist', () => { - const rootFolderPath = '/root/folder'; - const appNotInWorkspace = false; - fs.mkdirSync = jest.fn().mockReturnValue(rootFolderPath); - fs.existsSync = jest.fn().mockReturnValue(false); - createOrUpdateLaunchConfigJSON(rootFolderPath, launchJson, undefined, appNotInWorkspace, mockLog); - expect(fs.writeFileSync).toHaveBeenCalledWith( - join(rootFolderPath, DirName.VSCode, LAUNCH_JSON_FILE), - JSON.stringify(launchJson, null, 4), - 'utf8' - ); - }); - - it('should update an existing launch.json file', () => { - const rootFolderPath = '/root/folder'; - const appNotInWorkspace = false; - fs.existsSync = jest.fn().mockReturnValue(true); - createOrUpdateLaunchConfigJSON(rootFolderPath, launchJson, undefined, appNotInWorkspace, mockLog); - - expect(fs.writeFileSync).toHaveBeenCalledWith( - join(rootFolderPath, DirName.VSCode, LAUNCH_JSON_FILE), - JSON.stringify( - { - configurations: [...existingLaunchJson.configurations, ...launchJson.configurations] - }, - null, - 4 - ) - ); - }); - - it('should not update an existing launch.json file when app not in workspace', () => { - const rootFolderPath = '/root/folder'; - const appNotInWorkspace = true; - fs.existsSync = jest.fn().mockReturnValue(true); - createOrUpdateLaunchConfigJSON(rootFolderPath, launchJson, undefined, appNotInWorkspace, mockLog); - expect(fs.writeFileSync).toHaveBeenCalledWith( - join(rootFolderPath, DirName.VSCode, LAUNCH_JSON_FILE), - JSON.stringify(launchJson, null, 4), - 'utf8' - ); - }); - - it('should handle errors while writing launch.json file', () => { - const rootFolderPath = '/root/folder'; - const appNotInWorkspace = false; - setWriteFileSyncBehavior('error'); - createOrUpdateLaunchConfigJSON(rootFolderPath, launchJson, undefined, appNotInWorkspace, mockLog); - expect(mockLog.error).toHaveBeenCalledWith(t('errorLaunchFile')); - }); - }); - - describe('configureLaunchConfig', () => { - it('should configure launch config and update workspace folders', () => { - const mockOptions = { - projectPath: '/mock/project/path', - writeToAppOnly: true, - vscode: { - workspace: { - workspaceFolders: [], - updateWorkspaceFolders: jest.fn() - } - } as any - } as DebugOptions; - - const mockLog = { - info: jest.fn(), - error: jest.fn() - } as unknown as Logger; - - // Mock handleWorkspaceConfig to return a specific launchJsonPath and cwd - (handleWorkspaceConfig as jest.Mock).mockReturnValue({ - launchJsonPath: '/mock/launch.json', - cwd: '${workspaceFolder}/path', - workspaceFolderUri: '/mock/launch.json' - }); - - // Call the function under test - configureLaunchConfig(mockOptions, mockLog); - - // Expectations to ensure that workspace folders are updated correctly - expect(mockOptions.vscode.workspace.updateWorkspaceFolders).toHaveBeenCalledWith(0, undefined, { - uri: '/mock/launch.json', - name: 'path' - }); - }); - - it('should log startApp message when datasourceType is capProject', () => { - const options = { - datasourceType: DatasourceType.capProject, - projectPath: 'some/path' - } as DebugOptions; - configureLaunchConfig(options, mockLog); - expect(mockLog.info).toHaveBeenCalledWith( - t('startApp', { npmStart: '`npm start`', cdsRun: '`cds run --in-memory`' }) - ); - }); - - it('Should not run in Yeoman CLI or if vscode not found', () => { - const options = { - datasourceType: DatasourceType.metadataFile, - projectPath: 'some/path', - vscode: false - } as DebugOptions; - configureLaunchConfig(options, mockLog); - expect(mockLog.info).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/launch-config/test/debug-config/helpers.test.ts b/packages/launch-config/test/debug-config/helpers.test.ts index 2445185ace..61be487724 100644 --- a/packages/launch-config/test/debug-config/helpers.test.ts +++ b/packages/launch-config/test/debug-config/helpers.test.ts @@ -95,7 +95,7 @@ describe('launchConfig Unit Tests', () => { // Test for create launch config outside workspace describe('handleAppsNotInWorkspace', () => { it('should create a launch config for non-workspace apps', () => { - const mockProjectPath = '/mock/project/path'; + const mockProjectPath = path.join('/mock/project/path'); const result = handleAppsNotInWorkspace(mockProjectPath, isAppStudio, mockVscode); expect(result.cwd).toBe('${workspaceFolder}'); expect(result.launchJsonPath).toBe( @@ -107,7 +107,7 @@ describe('launchConfig Unit Tests', () => { }); it('should handle cases where vscode.Uri is not available', () => { - const mockProjectPath = '/mock/project/path'; + const mockProjectPath = path.join('/mock/project/path'); const result = handleAppsNotInWorkspace(mockProjectPath, isAppStudio, {}); expect(result.cwd).toBe('${workspaceFolder}'); expect(result.launchJsonPath).toBe( @@ -117,7 +117,7 @@ describe('launchConfig Unit Tests', () => { }); it('should handle cases where isAppStudio is true', () => { - const mockProjectPath = '/mock/project/path', + const mockProjectPath = path.join('/mock/project/path'), isAppStudio = true; const result = handleAppsNotInWorkspace(mockProjectPath, isAppStudio, mockVscode); expect(result.cwd).toBe('${workspaceFolder}'); diff --git a/packages/launch-config/test/debug-config/workspaceManager.test.ts b/packages/launch-config/test/debug-config/workspaceManager.test.ts index fecef5dc4b..d91b03163b 100644 --- a/packages/launch-config/test/debug-config/workspaceManager.test.ts +++ b/packages/launch-config/test/debug-config/workspaceManager.test.ts @@ -1,9 +1,4 @@ -import { - handleWorkspaceConfig, - handleUnsavedWorkspace, - handleSavedWorkspace, - handleOpenFolderButNoWorkspaceFile -} from '../../src/debug-config/workspaceManager'; +import { handleWorkspaceConfig } from '../../src/debug-config/workspaceManager'; import { formatCwd, getLaunchJsonPath, @@ -44,18 +39,45 @@ describe('launchConfig Unit Tests', () => { jest.clearAllMocks(); }); - describe('handleUnsavedWorkspace', () => { - it('should update paths for nested folders inside a workspace', () => { + describe('handleOpenFolderButNoWorkspaceFile', () => { + it('should create a launch config for non-workspace apps if folder is not in workspace', () => { + const mockProjectPath = path.join('/mock/project/path'); + (isFolderInWorkspace as jest.Mock).mockReturnValue(false); + (handleAppsNotInWorkspace as jest.Mock).mockReturnValue({ + launchJsonPath: mockProjectPath, + cwd: '${workspaceFolder}' + }); + + const options = { + isAppStudio, + vscode: mockVscode + } as DebugOptions; + + const result = handleWorkspaceConfig(mockProjectPath, options); + expect(result).toEqual({ + launchJsonPath: mockProjectPath, + cwd: '${workspaceFolder}' + }); + }); + + it('should update paths for nested folders inside an open folder', () => { const mockProjectPath = path.join('/mock/project/nestedFolder'); - const mockWsFolder = path.join('/mock/workspace/folder'); + const mockTargetFolder = path.join('/target/folder'); const mockNestedFolder = 'nestedFolder'; - mockVscode.workspace.getWorkspaceFolder.mockReturnValue({ uri: { fsPath: mockWsFolder } }); + + (isFolderInWorkspace as jest.Mock).mockReturnValue(true); (path.relative as jest.Mock).mockReturnValue(mockNestedFolder); (formatCwd as jest.Mock).mockReturnValue('${workspaceFolder}/nestedFolder'); + (getLaunchJsonPath as jest.Mock).mockReturnValue(mockTargetFolder); + + const options = { + isAppStudio, + vscode: mockVscode + } as DebugOptions; - const result = handleUnsavedWorkspace(mockProjectPath, mockVscode); + const result = handleWorkspaceConfig(mockProjectPath, options); expect(result).toEqual({ - launchJsonPath: mockWsFolder, + launchJsonPath: mockTargetFolder, cwd: '${workspaceFolder}/nestedFolder' }); }); @@ -63,20 +85,17 @@ describe('launchConfig Unit Tests', () => { describe('handleSavedWorkspace', () => { it('should handle projects inside the workspace', () => { - const mockProjectPath = '/mock/project/path'; - const mockProjectName = 'project'; - const mockTargetFolder = '/target/folder'; + const mockProjectPath = path.join('/mock/project/path'); + const mockTargetFolder = path.join('/target/folder'); (isFolderInWorkspace as jest.Mock).mockReturnValue(true); (formatCwd as jest.Mock).mockReturnValue('${workspaceFolder}/project'); (getLaunchJsonPath as jest.Mock).mockReturnValue(mockTargetFolder); - const result = handleSavedWorkspace( - mockProjectPath, - mockProjectName, - mockTargetFolder, + const options = { isAppStudio, - mockVscode - ); + vscode: mockVscode + } as DebugOptions; + const result = handleWorkspaceConfig(mockProjectPath, options); expect(result).toEqual({ launchJsonPath: mockTargetFolder, cwd: '${workspaceFolder}/project' @@ -84,22 +103,18 @@ describe('launchConfig Unit Tests', () => { }); it('should create a launch config for non-workspace apps', () => { - const mockProjectPath = '/mock/project/path'; - const mockProjectName = 'project'; - const mockTargetFolder = '/target/folder'; + const mockProjectPath = path.join('/mock/project/path'); (isFolderInWorkspace as jest.Mock).mockReturnValue(false); (handleAppsNotInWorkspace as jest.Mock).mockReturnValue({ launchJsonPath: mockProjectPath, cwd: '${workspaceFolder}' }); - const result = handleSavedWorkspace( - mockProjectPath, - mockProjectName, - mockTargetFolder, + const options = { isAppStudio, - mockVscode - ); + vscode: mockVscode + } as DebugOptions; + const result = handleWorkspaceConfig(mockProjectPath, options); expect(result).toEqual({ launchJsonPath: mockProjectPath, cwd: '${workspaceFolder}' @@ -107,48 +122,21 @@ describe('launchConfig Unit Tests', () => { }); }); - describe('handleOpenFolderButNoWorkspaceFile', () => { - it('should create a launch config for non-workspace apps if folder is not in workspace', () => { - const mockProjectPath = '/mock/project/path'; - const mockTargetFolder = '/target/folder'; - (isFolderInWorkspace as jest.Mock).mockReturnValue(false); - (handleAppsNotInWorkspace as jest.Mock).mockReturnValue({ - launchJsonPath: mockProjectPath, - cwd: '${workspaceFolder}' - }); - - const result = handleOpenFolderButNoWorkspaceFile( - mockProjectPath, - mockTargetFolder, - isAppStudio, - mockVscode - ); - expect(result).toEqual({ - launchJsonPath: mockProjectPath, - cwd: '${workspaceFolder}' - }); - }); - - it('should update paths for nested folders inside an open folder', () => { - const mockProjectPath = '/mock/project/nestedFolder'; - const mockTargetFolder = '/target/folder'; - const mockWsFolder = '/mock/workspace/folder'; + describe('handleUnsavedWorkspace', () => { + it('should update paths for nested folders inside a workspace', () => { + const mockProjectPath = path.join('mock/project/configureLaunchConfig'); + const mockWsFolder = path.join('mock/workspace/folder'); const mockNestedFolder = 'nestedFolder'; - - (isFolderInWorkspace as jest.Mock).mockReturnValue(true); mockVscode.workspace.getWorkspaceFolder.mockReturnValue({ uri: { fsPath: mockWsFolder } }); + mockVscode.workspace.workspaceFile.scheme = 'folder'; (path.relative as jest.Mock).mockReturnValue(mockNestedFolder); (formatCwd as jest.Mock).mockReturnValue('${workspaceFolder}/nestedFolder'); - (getLaunchJsonPath as jest.Mock).mockReturnValue(mockTargetFolder); - - const result = handleOpenFolderButNoWorkspaceFile( - mockProjectPath, - mockTargetFolder, - isAppStudio, - mockVscode - ); + const options = { + vscode: mockVscode + } as DebugOptions; + const result = handleWorkspaceConfig(mockProjectPath, options); expect(result).toEqual({ - launchJsonPath: mockTargetFolder, + launchJsonPath: mockWsFolder, cwd: '${workspaceFolder}/nestedFolder' }); }); @@ -156,9 +144,8 @@ describe('launchConfig Unit Tests', () => { describe('handleWorkspaceConfig', () => { it('should handle writeToAppOnly option', () => { - const mockProjectPath = '/mock/project/path'; + const mockProjectPath = path.join('/mock/project/path'); const options = { - projectPath: mockProjectPath, writeToAppOnly: true, vscode: mockVscode } as DebugOptions; @@ -168,7 +155,7 @@ describe('launchConfig Unit Tests', () => { cwd: '${workspaceFolder}' }); - const result = handleWorkspaceConfig(options); + const result = handleWorkspaceConfig(mockProjectPath, options); expect(result).toEqual({ launchJsonPath: mockProjectPath, cwd: '${workspaceFolder}' @@ -177,10 +164,9 @@ describe('launchConfig Unit Tests', () => { }); it('should handle open folder but no workspace file case', () => { - const mockProjectPath = '/mock/project/path'; - const mockTargetFolder = '/target/folder'; + const mockProjectPath = path.join('/mock/project/path'); + const mockTargetFolder = path.join('/target/folder'); const options = { - projectPath: mockProjectPath, vscode: { ...mockVscode, workspace: { ...mockVscode.workspace, workspaceFile: undefined } @@ -193,7 +179,7 @@ describe('launchConfig Unit Tests', () => { (getLaunchJsonPath as jest.Mock).mockReturnValue(mockTargetFolder); // Call the function under test - const result = handleWorkspaceConfig(options); + const result = handleWorkspaceConfig(mockProjectPath, options); // Assertions expect(result).toEqual({ @@ -210,9 +196,8 @@ describe('launchConfig Unit Tests', () => { }); it('should handle no workspace case', () => { - const mockProjectPath = '/mock/project/path'; + const mockProjectPath = path.join('/mock/project/path'); const options = { - projectPath: mockProjectPath, vscode: { ...mockVscode, workspace: undefined } } as DebugOptions; (handleAppsNotInWorkspace as jest.Mock).mockReturnValue({ @@ -220,7 +205,7 @@ describe('launchConfig Unit Tests', () => { cwd: '${workspaceFolder}' }); - const result = handleWorkspaceConfig(options); + const result = handleWorkspaceConfig(mockProjectPath, options); expect(result).toEqual({ launchJsonPath: mockProjectPath, cwd: '${workspaceFolder}' @@ -229,8 +214,8 @@ describe('launchConfig Unit Tests', () => { }); it('should handle saved workspace case', () => { - const mockProjectPath = '/mock/project/path'; - const mockTargetFolder = '/target/folder'; + const mockProjectPath = path.join('/mock/project/path'); + const mockTargetFolder = path.join('/target/folder'); const mockVscode = { workspace: { getWorkspaceFolder: jest.fn().mockReturnValue({ uri: { fsPath: mockTargetFolder } }), @@ -239,11 +224,10 @@ describe('launchConfig Unit Tests', () => { }; // Prepare options for the test const options = { - projectPath: mockProjectPath, vscode: mockVscode } as DebugOptions; // Call the function under test - const result = handleWorkspaceConfig(options); + const result = handleWorkspaceConfig(mockProjectPath, options); // Assertions expect(result).toEqual({ launchJsonPath: mockTargetFolder, @@ -270,11 +254,10 @@ describe('launchConfig Unit Tests', () => { // Prepare options for the test const options = { - projectPath: mockProjectPath, vscode: mockVscode } as DebugOptions; // Call the function under test - const result = handleWorkspaceConfig(options); + const result = handleWorkspaceConfig(mockProjectPath, options); // Assertions expect(result).toEqual({ launchJsonPath: mockProjectPath, diff --git a/packages/launch-config/test/launch-config-crud/create.test.ts b/packages/launch-config/test/launch-config-crud/create.test.ts index e6c9b19e78..718b8e792f 100644 --- a/packages/launch-config/test/launch-config-crud/create.test.ts +++ b/packages/launch-config/test/launch-config-crud/create.test.ts @@ -1,14 +1,35 @@ -import { join } from 'path'; +import { basename, join } from 'path'; import { create as createStorage } from 'mem-fs'; import { create } from 'mem-fs-editor'; import { createLaunchConfig } from '../../src/launch-config-crud/create'; import { DirName, FileName } from '@sap-ux/project-access'; import { TestPaths } from '../test-data/utils'; +import type { DebugOptions } from '../../src/types'; +import { LAUNCH_JSON_FILE, ProjectDataSourceType } from '../../src/types'; +import type { Logger } from '@sap-ux/logger'; +import { t } from '../../src/i18n'; +import { isFolderInWorkspace } from '../../src/debug-config/helpers'; + +// Mock the helpers +jest.mock('../../src/debug-config/helpers', () => ({ + ...jest.requireActual('../../src/debug-config/helpers'), + isFolderInWorkspace: jest.fn() +})); describe('create', () => { const memFs = create(createStorage()); const memFilePath = join(TestPaths.tmpDir, 'fe-projects', FileName.Package); const memFileContent = '{}\n'; + const mockLog = { + error: jest.fn(), + info: jest.fn() + } as unknown as Logger; + + const clearMemFsPaths = (path: string) => { + if (memFs.exists(path)) { + memFs.delete(path); + } + }; beforeEach(() => { memFs.writeJSON(memFilePath, memFileContent); @@ -153,4 +174,279 @@ describe('create', () => { ] }); }); + + test('launch.json file is missing, create new file with new config when debug options is provided', async () => { + const projectPath = join(TestPaths.tmpDir, 'test-projects'); + const launchConfigPath = join(projectPath, '.vscode', 'launch.json'); + if (memFs.exists(launchConfigPath)) { + memFs.delete(launchConfigPath); + } + const fs = await createLaunchConfig( + projectPath, + { + name: 'test-projects', + projectRoot: projectPath, + debugOptions: { + datasourceType: ProjectDataSourceType.odataServiceUrl, + vscode: true + } as DebugOptions + }, + memFs, + mockLog + ); + + expect(fs.exists(launchConfigPath)).toBe(true); + expect(mockLog.info).toHaveBeenCalledWith( + t('startServerMessage', { + folder: basename(projectPath), + npmCommand: 'start' + }) + ); + expect(fs.readJSON(launchConfigPath)).toStrictEqual({ + version: '0.2.0', + configurations: [ + { + name: 'Start test-projects', + type: 'node', + request: 'launch', + cwd: '${workspaceFolder}', + runtimeExecutable: 'npx', + windows: { + runtimeExecutable: 'npx.cmd' + }, + runtimeArgs: ['fiori', 'run'], + args: ['--open', 'index.htmlundefined'], + console: 'internalConsole', + internalConsoleOptions: 'openOnSessionStart', + outputCapture: 'std', + env: { + 'DEBUG': '--inspect', + 'FIORI_TOOLS_URL_PARAMS': 'sap-ui-xx-viewCache=false' + } + }, + { + name: 'Start test-projects Local', + type: 'node', + request: 'launch', + cwd: '${workspaceFolder}', + runtimeExecutable: 'npx', + windows: { + runtimeExecutable: 'npx.cmd' + }, + runtimeArgs: ['fiori', 'run'], + args: ['--config', './ui5-local.yaml', '--open', 'index.htmlundefined'], + console: 'internalConsole', + internalConsoleOptions: 'openOnSessionStart', + outputCapture: 'std', + env: { + 'FIORI_TOOLS_URL_PARAMS': 'sap-ui-xx-viewCache=false' + } + } + ] + }); + }); + + test('launch.json file is missing, will not create config when debug options provided and app source is cap project', async () => { + const projectPath = join(TestPaths.tmpDir, 'test-projects'); + const launchConfigPath = join(projectPath, DirName.VSCode, LAUNCH_JSON_FILE); + clearMemFsPaths(launchConfigPath); + + const fs = await createLaunchConfig( + TestPaths.tmpDir, + { + name: 'test-projects', + projectRoot: projectPath, + debugOptions: { + datasourceType: ProjectDataSourceType.capProject, + vscode: true + } as DebugOptions + }, + memFs, + mockLog + ); + expect(fs.exists(launchConfigPath)).toBe(false); + expect(mockLog.info).toHaveBeenCalledWith( + t('startApp', { npmStart: '`npm start`', cdsRun: '`cds run --in-memory`' }) + ); + }); + + test('Should create launch.json or run in Yeoman CLI or if vscode not found', async () => { + const projectPath = join(TestPaths.tmpDir, 'test-projects'); + const launchConfigPath = join(projectPath, DirName.VSCode, LAUNCH_JSON_FILE); + clearMemFsPaths(launchConfigPath); + const fs = await createLaunchConfig( + TestPaths.tmpDir, + { + name: 'test-projects', + projectRoot: projectPath, + debugOptions: { + datasourceType: ProjectDataSourceType.capProject, + vscode: false + } as DebugOptions + }, + memFs, + mockLog + ); + expect(fs.exists(launchConfigPath)).toBe(false); + }); + + test('launch.json file already exists, update file with debig config when debug options is provided and app is created out of', async () => { + const projectPath = join(TestPaths.tmpDir, 'test', 'test-projects'); + const launchJSONPath = join(TestPaths.tmpDir, 'test', '.vscode', 'launch.json'); + clearMemFsPaths(launchJSONPath); + memFs.writeJSON(launchJSONPath, { + version: '0.2.0', + configurations: [ + { + name: 'LaunchConfig_One' + } + ] + }); + (isFolderInWorkspace as jest.Mock).mockReturnValue(true); + const result: any = await createLaunchConfig( + projectPath, + { + name: 'test-projects', + projectRoot: projectPath, + debugOptions: { + datasourceType: ProjectDataSourceType.odataServiceUrl, + vscode: { + workspace: { + workspaceFile: { scheme: 'file' } + } + } as any, + isAppStudio: false + } as DebugOptions + }, + memFs + ); + expect(result.exists(launchJSONPath)).toBe(true); + const expectedLaunchConfigPath = join(TestPaths.tmpDir, 'test', '.vscode', 'launch.json'); + const updatedJson = result.readJSON(expectedLaunchConfigPath); + expect(updatedJson).toStrictEqual({ + version: '0.2.0', + configurations: [ + { + name: 'LaunchConfig_One' + }, + { + name: 'Start test-projects', + type: 'node', + request: 'launch', + cwd: '${workspaceFolder}/test-projects', + runtimeExecutable: 'npx', + windows: { + runtimeExecutable: 'npx.cmd' + }, + runtimeArgs: ['fiori', 'run'], + args: ['--open', 'index.htmlundefined'], + console: 'internalConsole', + internalConsoleOptions: 'openOnSessionStart', + outputCapture: 'std', + env: { + DEBUG: '--inspect', + FIORI_TOOLS_URL_PARAMS: 'sap-ui-xx-viewCache=false' + } + }, + { + name: 'Start test-projects Local', + type: 'node', + request: 'launch', + cwd: '${workspaceFolder}/test-projects', + runtimeExecutable: 'npx', + windows: { + runtimeExecutable: 'npx.cmd' + }, + runtimeArgs: ['fiori', 'run'], + args: ['--config', './ui5-local.yaml', '--open', 'index.htmlundefined'], + console: 'internalConsole', + internalConsoleOptions: 'openOnSessionStart', + outputCapture: 'std', + env: { + FIORI_TOOLS_URL_PARAMS: 'sap-ui-xx-viewCache=false' + } + } + ] + }); + }); + + test('launch.json file already exists, update file with debig config when debug options is provided', async () => { + const projectPath = join(TestPaths.tmpDir, 'test', 'test-projects'); + const launchJSONPath = join(TestPaths.tmpDir, 'test', '.vscode', 'launch.json'); + clearMemFsPaths(launchJSONPath); + memFs.writeJSON(launchJSONPath, { + version: '0.2.0', + configurations: [ + { + name: 'LaunchConfig_One' + } + ] + }); + (isFolderInWorkspace as jest.Mock).mockReturnValue(true); + const result: any = await createLaunchConfig( + projectPath, + { + name: 'test-projects', + projectRoot: projectPath, + debugOptions: { + datasourceType: ProjectDataSourceType.odataServiceUrl, + vscode: { + workspace: { + workspaceFile: { scheme: 'file' } + } + } as any, + isAppStudio: false + } as DebugOptions + }, + memFs + ); + expect(result.exists(launchJSONPath)).toBe(true); + const expectedLaunchConfigPath = join(TestPaths.tmpDir, 'test', '.vscode', 'launch.json'); + const updatedJson = result.readJSON(expectedLaunchConfigPath); + expect(updatedJson).toStrictEqual({ + version: '0.2.0', + configurations: [ + { + name: 'LaunchConfig_One' + }, + { + name: 'Start test-projects', + type: 'node', + request: 'launch', + cwd: '${workspaceFolder}/test-projects', + runtimeExecutable: 'npx', + windows: { + runtimeExecutable: 'npx.cmd' + }, + runtimeArgs: ['fiori', 'run'], + args: ['--open', 'index.htmlundefined'], + console: 'internalConsole', + internalConsoleOptions: 'openOnSessionStart', + outputCapture: 'std', + env: { + DEBUG: '--inspect', + FIORI_TOOLS_URL_PARAMS: 'sap-ui-xx-viewCache=false' + } + }, + { + name: 'Start test-projects Local', + type: 'node', + request: 'launch', + cwd: '${workspaceFolder}/test-projects', + runtimeExecutable: 'npx', + windows: { + runtimeExecutable: 'npx.cmd' + }, + runtimeArgs: ['fiori', 'run'], + args: ['--config', './ui5-local.yaml', '--open', 'index.htmlundefined'], + console: 'internalConsole', + internalConsoleOptions: 'openOnSessionStart', + outputCapture: 'std', + env: { + FIORI_TOOLS_URL_PARAMS: 'sap-ui-xx-viewCache=false' + } + } + ] + }); + }); }); diff --git a/packages/launch-config/test/launch-config-crud/update.test.ts b/packages/launch-config/test/launch-config-crud/update.test.ts index 53a0ec8cd3..a4a328542e 100644 --- a/packages/launch-config/test/launch-config-crud/update.test.ts +++ b/packages/launch-config/test/launch-config-crud/update.test.ts @@ -5,6 +5,7 @@ import { create } from 'mem-fs-editor'; import { createLaunchConfig, LAUNCH_JSON_FILE, updateLaunchConfig } from '../../src'; import { TestPaths } from '../test-data/utils'; import { parse } from 'jsonc-parser'; +import type { Editor } from 'mem-fs-editor'; function checkJSONComments(launchJsonString: string) { expect(launchJsonString).toMatch('// test json with comments - comment 1'); @@ -34,7 +35,7 @@ describe('update', () => { test('Create and then update existing launch config in launch.json', async (): Promise => { // create a new const launchJSONPath = join(TestPaths.feProjectsLaunchConfig); - let result = await createLaunchConfig( + let result: Editor = await createLaunchConfig( TestPaths.feProjects, { name: 'LaunchConfig_One', diff --git a/packages/launch-config/tsconfig.json b/packages/launch-config/tsconfig.json index d03f1afd91..c8d9cee638 100644 --- a/packages/launch-config/tsconfig.json +++ b/packages/launch-config/tsconfig.json @@ -13,15 +13,9 @@ { "path": "../logger" }, - { - "path": "../odata-service-inquirer" - }, { "path": "../project-access" }, - { - "path": "../store" - }, { "path": "../ui5-config" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 919978dd54..6b18891833 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1656,15 +1656,9 @@ importers: packages/launch-config: dependencies: - '@sap-ux/odata-service-inquirer': - specifier: workspace:* - version: link:../odata-service-inquirer '@sap-ux/project-access': specifier: workspace:* version: link:../project-access - '@sap-ux/store': - specifier: workspace:* - version: link:../store '@sap-ux/ui5-config': specifier: workspace:* version: link:../ui5-config