Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revise launch config #2347

Merged
merged 21 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-papayas-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@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`
1 change: 0 additions & 1 deletion packages/launch-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"@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",
Expand Down
57 changes: 17 additions & 40 deletions packages/launch-config/src/launch-config-crud/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ 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.
Expand Down Expand Up @@ -52,42 +50,14 @@ export async function createLaunchConfig(rootFolder: string, fioriOptions: Fiori
return fs;
}

/**
* Writes the application info settings to the appInfo.json file.
* Adds the specified path to the latestGeneratedFiles array.
*
* @param {string} path - The project file path to add.
* @param log - The logger instance.
*/
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 }));
}
}

/**
* 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 {
export function updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders: UpdateWorkspaceFolderOptions | undefined): 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, {
Expand All @@ -103,47 +73,50 @@ export function updateWorkspaceFoldersIfNeeded(
*
* @param {string} rootFolderPath - The root folder path of the project.
* @param {LaunchJSON} launchJsonFile - The launch.json configuration to write.
* @param fs - The memfs editor instance.
* @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.
*/
export function createOrUpdateLaunchConfigJSON(
rootFolderPath: string,
launchJsonFile?: LaunchJSON,
launchJsonFile: LaunchJSON,
fs: Editor,
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;
if (fs.exists(launchJSONPath) && !appNotInWorkspace) {
const existingLaunchConfig = parse(fs.read(launchJSONPath)) as LaunchJSON;
const updatedConfigurations = existingLaunchConfig.configurations.concat(
launchJsonFile?.configurations ?? []
);
fs.writeFileSync(
fs.write(
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');
fs.write(path, JSON.stringify(launchJsonFile ?? {}, null, 4));
}
} catch (error) {
log?.error(t('errorLaunchFile', { error: error }));
}
updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders, rootFolderPath, log);
updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders);
}

/**
* Generates and creates launch configuration for the project based on debug options.
*
* @param {DebugOptions} options - The options for configuring the debug setup.
* @param fs - The memfs editor instance.
* @param log - The logger instance.
* @returns {Editor | undefined} memfs editor instance. Returns undefined if the datasource type is CAP project or no vscode is available.
*/
export function configureLaunchConfig(options: DebugOptions, log?: Logger): void {
export function configureLaunchConfig(options: DebugOptions, fs?: Editor, log?: Logger): Editor | undefined {
const { datasourceType, projectPath, vscode } = options;
if (datasourceType === DatasourceType.capProject) {
log?.info(t('startApp', { npmStart: '`npm start`', cdsRun: '`cds run --in-memory`' }));
Expand All @@ -164,7 +137,10 @@ export function configureLaunchConfig(options: DebugOptions, log?: Logger): void
}
: undefined;

createOrUpdateLaunchConfigJSON(launchJsonPath, launchJsonFile, updateWorkspaceFolders, appNotInWorkspace, log);
if (!fs) {
fs = create(createStorage());
}
createOrUpdateLaunchConfigJSON(launchJsonPath, launchJsonFile, fs, updateWorkspaceFolders, appNotInWorkspace, log);

const npmCommand = datasourceType === DatasourceType.metadataFile ? 'run start-mock' : 'start';
const projectName = basename(projectPath);
Expand All @@ -174,4 +150,5 @@ export function configureLaunchConfig(options: DebugOptions, log?: Logger): void
npmCommand
})
);
return fs;
}
131 changes: 54 additions & 77 deletions packages/launch-config/test/debug-config/configureLaunchConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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
Expand All @@ -12,14 +11,9 @@ 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' }]
Expand All @@ -32,6 +26,7 @@ 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()
Expand All @@ -42,39 +37,6 @@ const mockEditor = {
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 = {
Expand All @@ -89,22 +51,6 @@ describe('Config Functions', () => {
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 = {
Expand All @@ -117,7 +63,7 @@ describe('Config Functions', () => {
},
projectName: 'Test Project'
} as UpdateWorkspaceFolderOptions;
updateWorkspaceFoldersIfNeeded(updateOptions, '/root/folder/path', mockLog);
updateWorkspaceFoldersIfNeeded(updateOptions);
expect(updateOptions.vscode.workspace.updateWorkspaceFolders).toHaveBeenCalledWith(0, undefined, {
name: 'Test Project',
uri: '/mock/uri'
Expand All @@ -126,33 +72,45 @@ describe('Config Functions', () => {

it('should not update workspace folders if no options are provided', () => {
const updateOptions: UpdateWorkspaceFolderOptions | undefined = undefined;
updateWorkspaceFoldersIfNeeded(updateOptions, '/root/folder/path', mockLog);
updateWorkspaceFoldersIfNeeded(updateOptions);
// No updateWorkspaceFolders call expected hence no app info json written
expect(fs.writeFileSync).not.toHaveBeenCalled();
expect(mockEditor.write).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(
mockEditor.exists = jest.fn().mockReturnValue(false);
createOrUpdateLaunchConfigJSON(
rootFolderPath,
launchJson,
mockEditor,
undefined,
appNotInWorkspace,
mockLog
);
expect(mockEditor.write).toHaveBeenCalledWith(
join(rootFolderPath, DirName.VSCode, LAUNCH_JSON_FILE),
JSON.stringify(launchJson, null, 4),
'utf8'
JSON.stringify(launchJson, null, 4)
);
});

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);
mockEditor.exists = jest.fn().mockReturnValue(true);
createOrUpdateLaunchConfigJSON(
rootFolderPath,
launchJson,
mockEditor,
undefined,
appNotInWorkspace,
mockLog
);

expect(fs.writeFileSync).toHaveBeenCalledWith(
expect(mockEditor.write).toHaveBeenCalledWith(
join(rootFolderPath, DirName.VSCode, LAUNCH_JSON_FILE),
JSON.stringify(
{
Expand All @@ -167,20 +125,39 @@ describe('Config Functions', () => {
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(
mockEditor.exists = jest.fn().mockReturnValue(true);
createOrUpdateLaunchConfigJSON(
rootFolderPath,
launchJson,
mockEditor,
undefined,
appNotInWorkspace,
mockLog
);
expect(mockEditor.write).toHaveBeenCalledWith(
join(rootFolderPath, DirName.VSCode, LAUNCH_JSON_FILE),
JSON.stringify(launchJson, null, 4),
'utf8'
JSON.stringify(launchJson, null, 4)
);
});

it('should handle errors while writing launch.json file', () => {
const rootFolderPath = '/root/folder';
const appNotInWorkspace = false;
setWriteFileSyncBehavior('error');
createOrUpdateLaunchConfigJSON(rootFolderPath, launchJson, undefined, appNotInWorkspace, mockLog);
const mockEditorWithError = {
exists: jest.fn().mockReturnValue(false),
read: jest.fn(),
write: jest.fn().mockImplementation(() => {
throw new Error();
})
} as unknown as Editor;
createOrUpdateLaunchConfigJSON(
rootFolderPath,
launchJson,
mockEditorWithError,
undefined,
appNotInWorkspace,
mockLog
);
expect(mockLog.error).toHaveBeenCalledWith(t('errorLaunchFile'));
});
});
Expand Down Expand Up @@ -211,7 +188,7 @@ describe('Config Functions', () => {
});

// Call the function under test
configureLaunchConfig(mockOptions, mockLog);
configureLaunchConfig(mockOptions, mockEditor, mockLog);

// Expectations to ensure that workspace folders are updated correctly
expect(mockOptions.vscode.workspace.updateWorkspaceFolders).toHaveBeenCalledWith(0, undefined, {
Expand All @@ -225,7 +202,7 @@ describe('Config Functions', () => {
datasourceType: DatasourceType.capProject,
projectPath: 'some/path'
} as DebugOptions;
configureLaunchConfig(options, mockLog);
configureLaunchConfig(options, mockEditor, mockLog);
expect(mockLog.info).toHaveBeenCalledWith(
t('startApp', { npmStart: '`npm start`', cdsRun: '`cds run --in-memory`' })
);
Expand All @@ -237,7 +214,7 @@ describe('Config Functions', () => {
projectPath: 'some/path',
vscode: false
} as DebugOptions;
configureLaunchConfig(options, mockLog);
configureLaunchConfig(options, mockEditor, mockLog);
expect(mockLog.info).not.toHaveBeenCalled();
});
});
Expand Down
3 changes: 0 additions & 3 deletions packages/launch-config/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
{
"path": "../project-access"
},
{
"path": "../store"
},
{
"path": "../ui5-config"
},
Expand Down
Loading
Loading