Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/easy-dots-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ux/adp-tooling': patch
---

fix: remove hard coded usage of webapp folder name
33 changes: 23 additions & 10 deletions packages/adp-tooling/src/base/change-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'node:path';
import type { Editor } from 'mem-fs-editor';
import { existsSync, readFileSync, readdirSync } from 'node:fs';

import { DirName } from '@sap-ux/project-access';
import { DirName, getWebappPath } from '@sap-ux/project-access';
import {
TemplateFileName,
type AnnotationsData,
Expand Down Expand Up @@ -32,18 +32,19 @@ interface InboundChange extends ManifestChangeProperties {
* @param {ManifestChangeProperties} change - The annotation data change that will be written.
* @param {Editor} fs - The `mem-fs-editor` instance used for file operations.
* @param {string} templatesPath - The path to the templates used for generating changes.
* @returns {void}
* @returns {Promise<void>}
*/
export function writeAnnotationChange(
export async function writeAnnotationChange(
projectPath: string,
timestamp: number,
annotation: AnnotationsData['annotation'],
change: ManifestChangeProperties | undefined,
fs: Editor,
templatesPath?: string
): void {
): Promise<void> {
try {
const changesFolderPath = path.join(projectPath, DirName.Webapp, DirName.Changes);
const webappPath = await getWebappPath(projectPath, fs);
const changesFolderPath = path.join(webappPath, DirName.Changes);
const annotationsFolderPath = path.join(changesFolderPath, DirName.Annotations);

if (change) {
Expand Down Expand Up @@ -83,11 +84,17 @@ export function writeAnnotationChange(
* @param {ManifestChangeProperties} change - The change data to be written to the file.
* @param {Editor} fs - The `mem-fs-editor` instance used for file operations.
* @param {string} [dir] - An optional subdirectory within the 'changes' directory where the file will be written.
* @returns {void}
* @returns {Promise<void>}
*/
export function writeChangeToFolder(projectPath: string, change: ManifestChangeProperties, fs: Editor, dir = ''): void {
export async function writeChangeToFolder(
projectPath: string,
change: ManifestChangeProperties,
fs: Editor,
dir = ''
): Promise<void> {
try {
let targetFolderPath = path.join(projectPath, DirName.Webapp, DirName.Changes);
const webappPath = await getWebappPath(projectPath, fs);
let targetFolderPath = path.join(webappPath, DirName.Changes);

if (dir) {
targetFolderPath = path.join(targetFolderPath, dir);
Expand Down Expand Up @@ -207,14 +214,20 @@ export function getChangesByType(
*
* @param {string} projectPath - The root path of the project.
* @param {string} inboundId - The inbound ID to search for within change files.
* @param {Editor} fs - The `mem-fs-editor` instance used for file operations.
* @returns {InboundChangeData} An object containing the file path and the change object with the matching inbound ID.
* @throws {Error} Throws an error if the change file cannot be read or if there's an issue accessing the directory.
*/
export function findChangeWithInboundId(projectPath: string, inboundId: string): InboundChangeData {
export async function findChangeWithInboundId(
projectPath: string,
inboundId: string,
fs: Editor
): Promise<InboundChangeData> {
let changeObj: InboundChange | undefined;
let filePath = '';

const pathToInboundChangeFiles = path.join(projectPath, DirName.Webapp, DirName.Changes);
const webappPath = await getWebappPath(projectPath, fs);
const pathToInboundChangeFiles = path.join(webappPath, DirName.Changes);

if (!existsSync(pathToInboundChangeFiles)) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,6 @@ export class AnnotationsWriter implements IWriter<AnnotationsData> {
if (data.isCommand) {
change = getChange(variant, timestamp, content, ChangeType.ADD_ANNOTATIONS_TO_ODATA);
}
writeAnnotationChange(this.projectPath, timestamp, data.annotation, change, this.fs, this.templatesPath);
await writeAnnotationChange(this.projectPath, timestamp, data.annotation, change, this.fs, this.templatesPath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class ComponentUsagesWriter implements IWriter<ComponentUsagesData> {
ChangeType.ADD_COMPONENT_USAGES
);

writeChangeToFolder(this.projectPath, compUsagesChange, this.fs);
await writeChangeToFolder(this.projectPath, compUsagesChange, this.fs);

if (!('library' in data)) {
return;
Expand All @@ -78,6 +78,6 @@ export class ComponentUsagesWriter implements IWriter<ComponentUsagesData> {
const libTimestamp = timestamp + 1;
const refLibChange = getChange(data.variant, libTimestamp, libRefContent, ChangeType.ADD_LIBRARY_REFERENCE);

writeChangeToFolder(this.projectPath, refLibChange, this.fs);
await writeChangeToFolder(this.projectPath, refLibChange, this.fs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ export class DataSourceWriter implements IWriter<DataSourceData> {
const content = this.constructContent(id, uri, maxAge);
const change = getChange(variant, timestamp, content, ChangeType.CHANGE_DATA_SOURCE);

writeChangeToFolder(this.projectPath, change, this.fs);
await writeChangeToFolder(this.projectPath, change, this.fs);

if (annotationId && annotationUri) {
const annotationContent = this.constructContent(annotationId, annotationUri);
const annotationTs = timestamp + 1;
const annotationChange = getChange(variant, annotationTs, annotationContent, ChangeType.CHANGE_DATA_SOURCE);

writeChangeToFolder(this.projectPath, annotationChange, this.fs);
await writeChangeToFolder(this.projectPath, annotationChange, this.fs);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,18 @@ export class InboundWriter implements IWriter<InboundData> {
* @returns {Promise<void>} A promise that resolves when the change writing process is completed.
*/
async write(data: InboundData): Promise<void> {
const { changeWithInboundId, filePath } = findChangeWithInboundId(this.projectPath, data.inboundId);
const { changeWithInboundId, filePath } = await findChangeWithInboundId(
this.projectPath,
data.inboundId,
this.fs
);
const timestamp = Date.now();

if (!changeWithInboundId) {
const content = this.constructContent(data);
const change = getChange(data.variant, timestamp, content, ChangeType.CHANGE_INBOUND);

writeChangeToFolder(this.projectPath, change, this.fs);
await writeChangeToFolder(this.projectPath, change, this.fs);
} else {
if (changeWithInboundId.content) {
this.getEnhancedContent(data, changeWithInboundId.content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ export class NewModelWriter implements IWriter<NewModelData> {
const content = this.constructContent(data);
const change = getChange(data.variant, timestamp, content, ChangeType.ADD_NEW_MODEL);

writeChangeToFolder(this.projectPath, change, this.fs);
await writeChangeToFolder(this.projectPath, change, this.fs);
}
}
68 changes: 35 additions & 33 deletions packages/adp-tooling/test/unit/base/change-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path, { resolve } from 'node:path';
import type { Editor } from 'mem-fs-editor';
import { create, type Editor } from 'mem-fs-editor';
import type { UI5FlexLayer } from '@sap-ux/project-access';
import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { renderFile } from 'ejs';
Expand All @@ -26,6 +26,7 @@ import {
writeAnnotationChange,
writeChangeToFolder
} from '../../../src/base/change-utils';
import { create as createStorage } from 'mem-fs';

jest.mock('fs', () => ({
...jest.requireActual('fs'),
Expand All @@ -51,8 +52,8 @@ describe('Change Utils', () => {
const writeJsonSpy = jest.fn();
const mockFs = { writeJSON: writeJsonSpy };

it('should write change to the specified folder without subdirectory', () => {
writeChangeToFolder(
it('should write change to the specified folder without subdirectory', async () => {
await writeChangeToFolder(
projectPath,
change as unknown as ManifestChangeProperties,
mockFs as unknown as Editor
Expand All @@ -61,9 +62,9 @@ describe('Change Utils', () => {
expect(writeJsonSpy).toHaveBeenCalledWith(expect.stringContaining(change.fileName), change);
});

it('should write change to the specified folder with subdirectory', () => {
it('should write change to the specified folder with subdirectory', async () => {
const dir = 'subdir';
writeChangeToFolder(
await writeChangeToFolder(
projectPath,
change as unknown as ManifestChangeProperties,
mockFs as unknown as Editor,
Expand All @@ -73,21 +74,21 @@ describe('Change Utils', () => {
expect(writeJsonSpy).toHaveBeenCalledWith(expect.stringContaining(path.join(dir, change.fileName)), change);
});

it('should throw error when writing json fails', () => {
it('should throw error when writing json fails', async () => {
const errMsg = 'Corrupted json.';
mockFs.writeJSON.mockImplementation(() => {
throw new Error(errMsg);
});

const expectedPath = path.join('project', 'webapp', 'changes', `${change.fileName}.change`);

expect(() => {
await expect(() =>
writeChangeToFolder(
projectPath,
change as unknown as ManifestChangeProperties,
mockFs as unknown as Editor
);
}).toThrow(
)
).rejects.toThrow(
`Could not write change to folder. Reason: Could not write change to file: ${expectedPath}. Reason: Corrupted json.`
);
});
Expand Down Expand Up @@ -265,6 +266,7 @@ describe('Change Utils', () => {
describe('findChangeWithInboundId', () => {
const mockProjectPath = '/mock/project/path';
const mockInboundId = 'mockInboundId';
const memFs = create(createStorage());

beforeEach(() => {
jest.resetAllMocks();
Expand All @@ -274,47 +276,47 @@ describe('Change Utils', () => {
const readdirSyncMock = readdirSync as jest.Mock;
const readFileSyncMock = readFileSync as jest.Mock;

it('should return empty results if the directory does not exist', () => {
it('should return empty results if the directory does not exist', async () => {
existsSyncMock.mockReturnValue(false);

const result = findChangeWithInboundId(mockProjectPath, mockInboundId);
const result = await findChangeWithInboundId(mockProjectPath, mockInboundId, memFs);

expect(result).toEqual({ filePath: '', changeWithInboundId: undefined });
});

it('should return empty results if no matching file is found', () => {
it('should return empty results if no matching file is found', async () => {
existsSyncMock.mockReturnValue(true);
readdirSyncMock.mockReturnValue([]);

const result = findChangeWithInboundId(mockProjectPath, mockInboundId);
const result = await findChangeWithInboundId(mockProjectPath, mockInboundId, memFs);

expect(result).toEqual({ filePath: '', changeWithInboundId: undefined });
});

it('should return the change object and file path if a matching file is found', () => {
it('should return the change object and file path if a matching file is found', async () => {
existsSyncMock.mockReturnValue(true);
readdirSyncMock.mockReturnValue([
{ name: 'id_addAnnotationsToOData.change', isFile: () => true },
{ name: 'id_changeInbound.change', isFile: () => true }
]);
readFileSyncMock.mockReturnValue(JSON.stringify({ content: { inboundId: mockInboundId } }));

const result = findChangeWithInboundId(mockProjectPath, mockInboundId);
const result = await findChangeWithInboundId(mockProjectPath, mockInboundId, memFs);

expect(result).toEqual({
filePath: expect.stringContaining('id_changeInbound.change'),
changeWithInboundId: { content: { inboundId: mockInboundId } }
});
});

it('should throw an error if reading the file fails', () => {
it('should throw an error if reading the file fails', async () => {
existsSyncMock.mockReturnValue(true);
readdirSyncMock.mockReturnValue([{ name: 'id_changeInbound.change', isFile: () => true }]);
readFileSyncMock.mockImplementation(() => {
throw new Error('Read file error');
});

expect(() => findChangeWithInboundId(mockProjectPath, mockInboundId)).toThrow(
await expect(() => findChangeWithInboundId(mockProjectPath, mockInboundId, memFs)).rejects.toThrow(
'Could not find change with inbound id'
);
});
Expand Down Expand Up @@ -349,11 +351,11 @@ describe('Change Utils', () => {
writeJSON: writeJsonSpy
};

it('should write the change file and an annotation file from a template', () => {
it('should write the change file and an annotation file from a template', async () => {
renderFileMock.mockImplementation((templatePath, data, options, callback) => {
callback(undefined, 'test');
});
writeAnnotationChange(
await writeAnnotationChange(
mockProjectPath,
123456789,
{
Expand Down Expand Up @@ -394,11 +396,11 @@ describe('Change Utils', () => {
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('mockAnnotation.xml'), 'test');
});

it('should write the change file and an annotation file from a template using the provided templates path', () => {
it('should write the change file and an annotation file from a template using the provided templates path', async () => {
renderFileMock.mockImplementation((templatePath, data, options, callback) => {
callback(undefined, 'test');
});
writeAnnotationChange(
await writeAnnotationChange(
mockProjectPath,
123456789,
{
Expand Down Expand Up @@ -440,10 +442,10 @@ describe('Change Utils', () => {
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('mockAnnotation.xml'), 'test');
});

it('should copy the annotation file to the correct directory if not creating a new empty file', () => {
it('should copy the annotation file to the correct directory if not creating a new empty file', async () => {
mockData.annotation.filePath = `mock/path/to/annotation/file.xml`;

writeAnnotationChange(
await writeAnnotationChange(
mockProjectPath,
123456789,
mockData.annotation as AnnotationsData['annotation'],
Expand All @@ -457,7 +459,7 @@ describe('Change Utils', () => {
);
});

it('should not copy the annotation file if the selected directory is the same as the target', () => {
it('should not copy the annotation file if the selected directory is the same as the target', async () => {
mockData.annotation.filePath = path.join(
'mock',
'project',
Expand All @@ -468,7 +470,7 @@ describe('Change Utils', () => {
'mockAnnotation.xml'
);

writeAnnotationChange(
await writeAnnotationChange(
mockProjectPath,
123456789,
mockData.annotation as AnnotationsData['annotation'],
Expand All @@ -479,22 +481,22 @@ describe('Change Utils', () => {
expect(copySpy).not.toHaveBeenCalled();
});

it('should throw error when write operation fails', () => {
it('should throw error when write operation fails', async () => {
mockData.annotation.filePath = '';

mockFs.writeJSON.mockImplementationOnce(() => {
throw new Error('Failed to write JSON');
});

expect(() => {
await expect(() =>
writeAnnotationChange(
mockProjectPath,
123456789,
mockData.annotation as AnnotationsData['annotation'],
mockChange as unknown as ManifestChangeProperties,
mockFs as unknown as Editor
);
}).toThrow(
)
).rejects.toThrow(
`Could not write annotation changes. Reason: Could not write change to file: ${path.join(
mockProjectPath,
'webapp',
Expand All @@ -504,20 +506,20 @@ describe('Change Utils', () => {
);
});

it('should throw an error if rendering the annotation file fails', () => {
it('should throw an error if rendering the annotation file fails', async () => {
renderFileMock.mockImplementation((templatePath, data, options, callback) => {
callback(new Error('Failed to render annotation file'), '');
});

expect(() => {
await expect(() =>
writeAnnotationChange(
mockProjectPath,
123456789,
mockData.annotation as AnnotationsData['annotation'],
mockChange as unknown as ManifestChangeProperties,
mockFs as unknown as Editor
);
}).toThrow('Failed to render annotation file');
)
).rejects.toThrow('Failed to render annotation file');
});
});
});
Loading
Loading