diff --git a/.changeset/orange-donkeys-type.md b/.changeset/orange-donkeys-type.md new file mode 100644 index 0000000000..cd9fd79c61 --- /dev/null +++ b/.changeset/orange-donkeys-type.md @@ -0,0 +1,11 @@ +--- +'@sap-ux/adp-flp-config-sub-generator': patch +'@sap-ux/mockserver-config-writer': patch +'@sap-ux/odata-service-writer': patch +'@sap-ux/adp-tooling': patch +'@sap-ux/telemetry': patch +'@sap-ux/create': patch +'@sap-ux/i18n': patch +--- + +fix: usage of static webapp path diff --git a/packages/adp-flp-config-sub-generator/src/app/index.ts b/packages/adp-flp-config-sub-generator/src/app/index.ts index e6e155a73e..e0bb64d06e 100644 --- a/packages/adp-flp-config-sub-generator/src/app/index.ts +++ b/packages/adp-flp-config-sub-generator/src/app/index.ts @@ -197,7 +197,7 @@ export default class extends Generator { requestOptions['auth'] = { username: this.credentials.username, password: this.credentials.password }; } const provider = await createAbapServiceProvider(target, requestOptions, false, this.toolsLogger); - const variant = getVariant(this.projectRootPath); + const variant = await getVariant(this.projectRootPath); const manifestService = await ManifestService.initMergedManifest( provider, this.projectRootPath, diff --git a/packages/adp-tooling/src/base/abap/manifest-service.ts b/packages/adp-tooling/src/base/abap/manifest-service.ts index 145e8a47db..1cbe5d3d10 100644 --- a/packages/adp-tooling/src/base/abap/manifest-service.ts +++ b/packages/adp-tooling/src/base/abap/manifest-service.ts @@ -141,7 +141,7 @@ export class ManifestService { */ private async fetchMergedManifest(basePath: string, descriptorVariantId: string): Promise { const zip = new ZipFile(); - const files = getWebappFiles(basePath); + const files = await getWebappFiles(basePath); for (const file of files) { zip.addFile(file.relativePath, Buffer.from(file.content, 'utf-8')); } diff --git a/packages/adp-tooling/src/base/helper.ts b/packages/adp-tooling/src/base/helper.ts index 6ff3494097..04c5f7d60b 100644 --- a/packages/adp-tooling/src/base/helper.ts +++ b/packages/adp-tooling/src/base/helper.ts @@ -1,8 +1,8 @@ import type { Editor } from 'mem-fs-editor'; import { existsSync, readdirSync, readFileSync } from 'fs'; -import { join, isAbsolute, relative } from 'path'; - -import { UI5Config } from '@sap-ux/ui5-config'; +import { join, isAbsolute, relative, basename, dirname } from 'path'; +import { getWebappPath, FileName, readUi5Yaml } from '@sap-ux/project-access'; +import type { UI5Config } from '@sap-ux/ui5-config'; import type { DescriptorVariant, AdpPreviewConfig } from '../types'; @@ -11,13 +11,14 @@ import type { DescriptorVariant, AdpPreviewConfig } from '../types'; * * @param {string} basePath - The path to the adaptation project. * @param {Editor} fs - The mem-fs editor instance. - * @returns {DescriptorVariant} The app descriptor variant. + * @returns {Promise} The app descriptor variant. */ -export function getVariant(basePath: string, fs?: Editor): DescriptorVariant { +export async function getVariant(basePath: string, fs?: Editor): Promise { + const webappPath = await getWebappPath(basePath); if (fs) { - return fs.readJSON(join(basePath, 'webapp', 'manifest.appdescr_variant')) as unknown as DescriptorVariant; + return fs.readJSON(join(webappPath, FileName.ManifestAppDescrVar)) as unknown as DescriptorVariant; } - return JSON.parse(readFileSync(join(basePath, 'webapp', 'manifest.appdescr_variant'), 'utf-8')); + return JSON.parse(readFileSync(join(webappPath, FileName.ManifestAppDescrVar), 'utf-8')); } /** @@ -27,8 +28,8 @@ export function getVariant(basePath: string, fs?: Editor): DescriptorVariant { * @param {DescriptorVariant} variant - The descriptor variant object. * @param {Editor} fs - The mem-fs editor instance. */ -export function updateVariant(basePath: string, variant: DescriptorVariant, fs: Editor) { - fs.writeJSON(join(basePath, 'webapp', 'manifest.appdescr_variant'), variant); +export async function updateVariant(basePath: string, variant: DescriptorVariant, fs: Editor): Promise { + fs.writeJSON(join(await getWebappPath(basePath), FileName.ManifestAppDescrVar), variant); } /** @@ -38,12 +39,12 @@ export function updateVariant(basePath: string, variant: DescriptorVariant, fs: * or `appdescr_app_addNewInbound` present in the content of the descriptor variant. * * @param {string} basePath - The base path of the project where the manifest.appdescr_variant is located. - * @returns {boolean} Returns `true` if FLP configuration changes exist, otherwise `false`. + * @returns {Promise} Returns `true` if FLP configuration changes exist, otherwise `false`. * @throws {Error} Throws an error if the variant could not be retrieved. */ -export function flpConfigurationExists(basePath: string): boolean { +export async function flpConfigurationExists(basePath: string): Promise { try { - const variant = getVariant(basePath); + const variant = await getVariant(basePath); return variant.content?.some( ({ changeType }) => changeType === 'appdescr_app_changeInbound' || changeType === 'appdescr_app_addNewInbound' @@ -74,13 +75,19 @@ export function isTypescriptSupported(basePath: string, fs?: Editor): boolean { */ export async function getAdpConfig(basePath: string, yamlPath: string): Promise { const ui5ConfigPath = isAbsolute(yamlPath) ? yamlPath : join(basePath, yamlPath); - const ui5Conf = await UI5Config.newInstance(readFileSync(ui5ConfigPath, 'utf-8')); - const customMiddlerware = - ui5Conf.findCustomMiddleware<{ adp: AdpPreviewConfig }>('fiori-tools-preview') ?? - ui5Conf.findCustomMiddleware<{ adp: AdpPreviewConfig }>('preview-middleware'); - const adp = customMiddlerware?.configuration?.adp; + let ui5Conf: UI5Config; + let adp: AdpPreviewConfig | undefined; + try { + ui5Conf = await readUi5Yaml(dirname(ui5ConfigPath), basename(ui5ConfigPath)); + const customMiddleware = + ui5Conf.findCustomMiddleware<{ adp: AdpPreviewConfig }>('fiori-tools-preview') ?? + ui5Conf.findCustomMiddleware<{ adp: AdpPreviewConfig }>('preview-middleware'); + adp = customMiddleware?.configuration?.adp; + } catch (error) { + // do nothing here + } if (!adp) { - throw new Error('No system configuration found in ui5.yaml'); + throw new Error(`No system configuration found in ${basename(ui5ConfigPath)}`); } return adp; } @@ -89,10 +96,10 @@ export async function getAdpConfig(basePath: string, yamlPath: string): Promise< * Get all files in the webapp folder. * * @param {string} basePath - The path to the adaptation project. - * @returns {Array<{ relativePath: string; content: string }>} The files in the webapp folder. + * @returns {Promise<{ relativePath: string; content: string }[]>} The files in the webapp folder. */ -export function getWebappFiles(basePath: string): { relativePath: string; content: string }[] { - const dir = join(basePath, 'webapp'); +export async function getWebappFiles(basePath: string): Promise<{ relativePath: string; content: string }[]> { + const dir = await getWebappPath(basePath); const files: { relativePath: string; content: string }[] = []; const getFilesRecursivelySync = (directory: string): void => { diff --git a/packages/adp-tooling/src/preview/change-handler.ts b/packages/adp-tooling/src/preview/change-handler.ts index b86d940371..c494e0eced 100644 --- a/packages/adp-tooling/src/preview/change-handler.ts +++ b/packages/adp-tooling/src/preview/change-handler.ts @@ -270,7 +270,7 @@ export async function addAnnotationFile( serviceUrl: datasoruces[dataSourceId].uri, fileName: basename(dataSource[annotationDataSourceKey].uri) }, - variant: getVariant(projectRoot), + variant: await getVariant(projectRoot), isCommand: false }, fs @@ -289,7 +289,7 @@ export async function addAnnotationFile( * @returns Promise */ async function getManifestService(basePath: string, logger: Logger): Promise { - const variant = getVariant(basePath); + const variant = await getVariant(basePath); const { target, ignoreCertErrors = false } = await getAdpConfig(basePath, join(basePath, FileName.Ui5Yaml)); const provider = await createAbapServiceProvider(target, { ignoreCertErrors }, true, logger); return await ManifestService.initMergedManifest(provider, basePath, variant, logger as unknown as ToolsLogger); diff --git a/packages/adp-tooling/src/preview/routes-handler.ts b/packages/adp-tooling/src/preview/routes-handler.ts index 582b463e0c..dab5ca7ffc 100644 --- a/packages/adp-tooling/src/preview/routes-handler.ts +++ b/packages/adp-tooling/src/preview/routes-handler.ts @@ -74,7 +74,7 @@ export default class RoutesHandler { * @param data Data that is sent to the client * @param contentType Content type, defaults to json */ - private sendFilesResponse(res: Response, data: object | string, contentType: string = 'application/json') { + private sendFilesResponse(res: Response, data: object | string, contentType: string = 'application/json'): void { res.status(HttpStatusCodes.OK).contentType(contentType).send(data); } @@ -100,12 +100,12 @@ export default class RoutesHandler { * @param res Response * @param next Next Function */ - public handleReadAllFragments = async (_: Request, res: Response, next: NextFunction) => { + public handleReadAllFragments = async (_: Request, res: Response, next: NextFunction): Promise => { try { const files = await this.readAllFilesByGlob('/**/changes/fragments/*.fragment.xml'); - const fileNames = files.map((f) => ({ - fragmentName: f.getName() + const fileNames = files.map((file) => ({ + fragmentName: file.getName() })); this.sendFilesResponse(res, { @@ -125,12 +125,12 @@ export default class RoutesHandler { * @param res Response * @param next Next Function */ - public handleReadAllControllers = async (_: Request, res: Response, next: NextFunction) => { + public handleReadAllControllers = async (_: Request, res: Response, next: NextFunction): Promise => { try { const files = await this.readAllFilesByGlob('/**/changes/coding/*.js'); - const fileNames = files.map((f) => ({ - controllerName: f.getName() + const fileNames = files.map((file) => ({ + controllerName: file.getName() })); this.sendFilesResponse(res, { @@ -150,7 +150,11 @@ export default class RoutesHandler { * @param res Response * @param next Next Function */ - public handleGetControllerExtensionData = async (req: Request, res: Response, next: NextFunction) => { + public handleGetControllerExtensionData = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { try { const params = req.params as { controllerName: string }; const controllerName = sanitize(params.controllerName); @@ -219,7 +223,7 @@ export default class RoutesHandler { * @param res Response * @param next Next Function */ - public handleWriteControllerExt = async (req: Request, res: Response, next: NextFunction) => { + public handleWriteControllerExt = async (req: Request, res: Response, next: NextFunction): Promise => { try { const data = req.body as WriteControllerBody; @@ -249,7 +253,7 @@ export default class RoutesHandler { return; } - generateControllerFile(rootPath, filePath, name); + await generateControllerFile(rootPath, filePath, name); const message = 'Controller extension created!'; res.status(HttpStatusCodes.CREATED).send(message); @@ -269,7 +273,11 @@ export default class RoutesHandler { * @param res Response * @param next Next Function */ - public handleGetAllAnnotationFilesMappedByDataSource = async (_req: Request, res: Response, next: NextFunction) => { + public handleGetAllAnnotationFilesMappedByDataSource = async ( + _req: Request, + res: Response, + next: NextFunction + ): Promise => { try { const isRunningInBAS = isAppStudio(); @@ -344,7 +352,7 @@ export default class RoutesHandler { private async getManifestService(): Promise { const project = this.util.getProject(); const basePath = project.getRootPath(); - const variant = getVariant(basePath); + const variant = await getVariant(basePath); const { target, ignoreCertErrors = false } = await getAdpConfig( basePath, path.join(basePath, FileName.Ui5Yaml) @@ -365,8 +373,8 @@ export default class RoutesHandler { * @param {string} name - The name of the controller extension (used in TypeScript templates). * @throws {Error} Throws an error if rendering the template fails. */ -function generateControllerFile(rootPath: string, filePath: string, name: string): void { - const id = getVariant(rootPath)?.id; +async function generateControllerFile(rootPath: string, filePath: string, name: string): Promise { + const id = (await getVariant(rootPath))?.id; const isTsSupported = isTypescriptSupported(rootPath); const tmplFileName = isTsSupported ? TemplateFileName.TSController : TemplateFileName.Controller; const tmplPath = path.join(__dirname, '../../templates/rta', tmplFileName); diff --git a/packages/adp-tooling/src/prompts/add-annotations-to-odata/index.ts b/packages/adp-tooling/src/prompts/add-annotations-to-odata/index.ts index c335fff695..5ad6dd388e 100644 --- a/packages/adp-tooling/src/prompts/add-annotations-to-odata/index.ts +++ b/packages/adp-tooling/src/prompts/add-annotations-to-odata/index.ts @@ -2,10 +2,10 @@ import type { ListQuestion, FileBrowserQuestion, YUIQuestion } from '@sap-ux/inq import type { ManifestNamespace } from '@sap-ux/project-access'; import { AnnotationFileSelectType, type AddAnnotationsAnswers } from '../../types'; import { t } from '../../i18n'; -import { filterDataSourcesByType } from '@sap-ux/project-access'; +import { filterDataSourcesByType, getWebappPath, DirName } from '@sap-ux/project-access'; import { existsSync } from 'fs'; import { validateEmptyString } from '@sap-ux/project-input-validator'; -import { join, isAbsolute, sep } from 'path'; +import { join, isAbsolute, basename } from 'path'; /** * Gets the prompts for adding annotations to OData service. @@ -60,7 +60,7 @@ export function getPrompts( default: '', when: (answers: AddAnnotationsAnswers) => answers.id !== '' && answers.fileSelectOption === AnnotationFileSelectType.ExistingFile, - validate: (value) => { + validate: async (value: string) => { const validationResult = validateEmptyString(value); if (typeof validationResult === 'string') { return validationResult; @@ -71,8 +71,11 @@ export function getPrompts( return t('validators.fileDoesNotExist'); } - const fileName = filePath.split(sep).pop(); - if (existsSync(join(basePath, 'webapp', 'changes', 'annotations', fileName))) { + if ( + existsSync( + join(await getWebappPath(basePath), DirName.Changes, DirName.Annotations, basename(filePath)) + ) + ) { return t('validators.annotationFileAlreadyExists'); } diff --git a/packages/adp-tooling/src/writer/inbound-navigation.ts b/packages/adp-tooling/src/writer/inbound-navigation.ts index 620b295a0f..9d282df1c7 100644 --- a/packages/adp-tooling/src/writer/inbound-navigation.ts +++ b/packages/adp-tooling/src/writer/inbound-navigation.ts @@ -25,7 +25,7 @@ export async function generateInboundConfig( fs = create(createStorage()); } - const variant = getVariant(basePath, fs); + const variant = await getVariant(basePath, fs); if (!config?.inboundId) { config.addInboundId = true; @@ -34,7 +34,7 @@ export async function generateInboundConfig( enhanceInboundConfig(config, variant.id, variant.content as Content[]); - updateVariant(basePath, variant, fs); + await updateVariant(basePath, variant, fs); await updateI18n(basePath, variant.id, config, fs); return fs; diff --git a/packages/adp-tooling/test/unit/base/abap/manifest-service.test.ts b/packages/adp-tooling/test/unit/base/abap/manifest-service.test.ts index 595486d108..4745a9a013 100644 --- a/packages/adp-tooling/test/unit/base/abap/manifest-service.test.ts +++ b/packages/adp-tooling/test/unit/base/abap/manifest-service.test.ts @@ -112,9 +112,9 @@ describe('ManifestService', () => { describe('initMergedManifest', () => { it('should initialize and fetch the merged manifest', async () => { const variant = { id: 'descriptorVariantId', reference: 'referenceAppId' }; - (getWebappFiles as jest.MockedFunction).mockReturnValue([ - { relativePath: 'path', content: 'content' } - ]); + (getWebappFiles as jest.MockedFunction).mockReturnValue( + Promise.resolve([{ relativePath: 'path', content: 'content' }]) + ); manifestService = await ManifestService.initMergedManifest( provider, 'basePath', diff --git a/packages/adp-tooling/test/unit/base/helper.test.ts b/packages/adp-tooling/test/unit/base/helper.test.ts index a5144d5451..de4356c9a9 100644 --- a/packages/adp-tooling/test/unit/base/helper.test.ts +++ b/packages/adp-tooling/test/unit/base/helper.test.ts @@ -42,18 +42,18 @@ describe('helper', () => { jest.clearAllMocks(); }); - test('should return variant', () => { + test('should return variant', async () => { readFileSyncMock.mockImplementation(() => mockVariant); - expect(getVariant(basePath)).toStrictEqual(JSON.parse(mockVariant)); + expect(await getVariant(basePath)).toStrictEqual(JSON.parse(mockVariant)); }); - test('should return variant using fs editor', () => { + test('should return variant using fs editor', async () => { const fs = { readJSON: jest.fn().mockReturnValue(JSON.parse(mockVariant)) } as unknown as Editor; - const result = getVariant(basePath, fs); + const result = await getVariant(basePath, fs); expect(fs.readJSON).toHaveBeenCalledWith(join(basePath, 'webapp', 'manifest.appdescr_variant')); expect(result).toStrictEqual(JSON.parse(mockVariant)); @@ -70,8 +70,8 @@ describe('helper', () => { jest.clearAllMocks(); }); - it('should write the updated variant content to the manifest file', () => { - updateVariant(basePath, mockVariant, fs); + it('should write the updated variant content to the manifest file', async () => { + await updateVariant(basePath, mockVariant, fs); expect(fs.writeJSON).toHaveBeenCalledWith( join(basePath, 'webapp', 'manifest.appdescr_variant'), @@ -87,7 +87,7 @@ describe('helper', () => { jest.clearAllMocks(); }); - it('should return true if valid FLP configuration exists', () => { + it('should return true if valid FLP configuration exists', async () => { readFileSyncMock.mockReturnValue( JSON.stringify({ content: [ @@ -97,13 +97,13 @@ describe('helper', () => { }) ); - const result = flpConfigurationExists(basePath); + const result = await flpConfigurationExists(basePath); expect(result).toBe(true); expect(readFileSyncMock).toHaveBeenCalledWith(appDescrPath, 'utf-8'); }); - it('should return false if no valid FLP configuration exists', () => { + it('should return false if no valid FLP configuration exists', async () => { readFileSyncMock.mockReturnValue( JSON.stringify({ content: [ @@ -113,18 +113,18 @@ describe('helper', () => { }) ); - const result = flpConfigurationExists(basePath); + const result = await flpConfigurationExists(basePath); expect(result).toBe(false); expect(readFileSyncMock).toHaveBeenCalledWith(appDescrPath, 'utf-8'); }); - it('should throw an error if getVariant fails', () => { + it('should throw an error if getVariant fails', async () => { readFileSyncMock.mockImplementation(() => { throw new Error('Failed to retrieve variant'); }); - expect(() => flpConfigurationExists(basePath)).toThrow( + await expect(flpConfigurationExists(basePath)).rejects.toThrow( 'Failed to check if FLP configuration exists: Failed to retrieve variant' ); expect(readFileSyncMock).toHaveBeenCalledWith(appDescrPath, 'utf-8'); @@ -214,8 +214,20 @@ describe('helper', () => { jest.clearAllMocks(); }); - test('should return webapp files', () => { - expect(getWebappFiles(basePath)).toEqual([ + test('should return webapp files', async () => { + jest.spyOn(UI5Config, 'newInstance').mockResolvedValue({ + findCustomMiddleware: jest.fn().mockReturnValue({ + configuration: { + adp: mockAdp + } + } as Partial as CustomMiddleware), + getConfiguration: jest.fn().mockReturnValue({ + paths: { + webapp: 'webapp' + } + }) + } as Partial as UI5Config); + expect(await getWebappFiles(basePath)).toEqual([ { relativePath: join('i18n', 'i18n.properties'), content: expect.any(String) diff --git a/packages/adp-tooling/test/unit/preview/adp-preview.test.ts b/packages/adp-tooling/test/unit/preview/adp-preview.test.ts index bda9d02356..5a7b6cd9b4 100644 --- a/packages/adp-tooling/test/unit/preview/adp-preview.test.ts +++ b/packages/adp-tooling/test/unit/preview/adp-preview.test.ts @@ -14,7 +14,7 @@ import * as serviceWriter from '@sap-ux/odata-service-writer/dist/data/annotatio import * as helper from '../../../src/base/helper'; import * as editors from '../../../src/writer/editors'; -import { AdpPreview } from '../../../src/preview/adp-preview'; +import { AdpPreview } from '../../../src'; import * as manifestService from '../../../src/base/abap/manifest-service'; import type { AdpPreviewConfig, CommonChangeProperties } from '../../../src'; import { addXmlFragment, tryFixChange } from '../../../src/preview/change-handler'; @@ -485,7 +485,7 @@ describe('AdaptationProject', () => { middlewareUtil, logger ); - jest.spyOn(helper, 'getVariant').mockReturnValue({ + jest.spyOn(helper, 'getVariant').mockResolvedValue({ content: [], id: 'adp/project', layer: 'VENDOR', diff --git a/packages/adp-tooling/test/unit/preview/change-handler.test.ts b/packages/adp-tooling/test/unit/preview/change-handler.test.ts index 2932ccb0cf..17fc56ec4f 100644 --- a/packages/adp-tooling/test/unit/preview/change-handler.test.ts +++ b/packages/adp-tooling/test/unit/preview/change-handler.test.ts @@ -561,7 +561,7 @@ id=\\"btn-30303030\\"" } }) } as any); - jest.spyOn(helper, 'getVariant').mockReturnValue({ + jest.spyOn(helper, 'getVariant').mockResolvedValue({ content: [], id: 'adp/project', layer: 'VENDOR', @@ -589,7 +589,6 @@ id=\\"btn-30303030\\"" error: jest.fn() }; - const fragmentName = 'Share'; const change = { changeType: 'appdescr_app_addAnnotationsToOData', content: { diff --git a/packages/adp-tooling/test/unit/prompts/add-annotations-to-odata/index.test.ts b/packages/adp-tooling/test/unit/prompts/add-annotations-to-odata/index.test.ts index 2e140845d8..94c9addf45 100644 --- a/packages/adp-tooling/test/unit/prompts/add-annotations-to-odata/index.test.ts +++ b/packages/adp-tooling/test/unit/prompts/add-annotations-to-odata/index.test.ts @@ -142,39 +142,39 @@ describe('getPrompts', () => { }); describe('file path validations', () => { - test('should fail with input cannot be empty message', () => { + test('should fail with input cannot be empty message', async () => { jest.spyOn(validators, 'validateEmptyString').mockReturnValueOnce('Input cannot be empty'); - const filePathValidator = (getPrompts(mockBasePath, dataSources)[2] as any).validate; + const filePathValidator = (getPrompts(mockBasePath, dataSources)[2] as any).validate as Function; - expect(filePathValidator('')).toBe('Input cannot be empty'); + expect(await filePathValidator('')).toBe('Input cannot be empty'); }); - test('should fail with file doesn not exist message', () => { + test('should fail with file doesn not exist message', async () => { jest.spyOn(validators, 'validateEmptyString').mockReturnValueOnce(true); jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false); - const filePathValidator = (getPrompts(mockBasePath, dataSources)[2] as any).validate; + const filePathValidator = (getPrompts(mockBasePath, dataSources)[2] as any).validate as Function; - expect(filePathValidator('non-existing-file.xml')).toBe(i18n.t('validators.fileDoesNotExist')); + expect(await filePathValidator('non-existing-file.xml')).toBe(i18n.t('validators.fileDoesNotExist')); }); - test('should fail with file already exists in change directory message', () => { + test('should fail with file already exists in change directory message', async () => { jest.spyOn(validators, 'validateEmptyString').mockReturnValueOnce(true); jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true).mockReturnValueOnce(true); - const filePathValidator = (getPrompts(mockBasePath, dataSources)[2] as any).validate; + const filePathValidator = (getPrompts(mockBasePath, dataSources)[2] as any).validate as Function; - expect(filePathValidator('existing-file.xml')).toBe(i18n.t('validators.annotationFileAlreadyExists')); + expect(await filePathValidator('existing-file.xml')).toBe(i18n.t('validators.annotationFileAlreadyExists')); }); - test('should pass with relative file path input', () => { + test('should pass with relative file path input', async () => { jest.spyOn(validators, 'validateEmptyString').mockReturnValueOnce(true); jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true).mockReturnValueOnce(false); - const filePathValidator = (getPrompts(mockBasePath, dataSources)[2] as any).validate; + const filePathValidator = (getPrompts(mockBasePath, dataSources)[2] as any).validate as Function; - expect(filePathValidator('existing-file.xml')).toBeTruthy(); + expect(await filePathValidator('existing-file.xml')).toBeTruthy(); }); test('should pass with absolute file path input', () => { diff --git a/packages/create/src/cli/add/annotations-to-odata.ts b/packages/create/src/cli/add/annotations-to-odata.ts index 4cd513a98c..e69c394f93 100644 --- a/packages/create/src/cli/add/annotations-to-odata.ts +++ b/packages/create/src/cli/add/annotations-to-odata.ts @@ -44,7 +44,7 @@ async function addAnnotationsToOdata(basePath: string, simulate: boolean, yamlPa basePath = process.cwd(); } await validateAdpProject(basePath); - const variant = getVariant(basePath); + const variant = await getVariant(basePath); const { target, ignoreCertErrors = false } = await getAdpConfig(basePath, yamlPath); const provider = await createAbapServiceProvider( target, diff --git a/packages/create/src/cli/add/component-usages.ts b/packages/create/src/cli/add/component-usages.ts index 46c3bfff62..e24e7f220c 100644 --- a/packages/create/src/cli/add/component-usages.ts +++ b/packages/create/src/cli/add/component-usages.ts @@ -39,7 +39,7 @@ export async function addComponentUsages(basePath: string, simulate: boolean): P } await validateAdpProject(basePath); - const variant = getVariant(basePath); + const variant = await getVariant(basePath); const answers = await promptYUIQuestions(getPromptsForAddComponentUsages(basePath, variant.layer), false); diff --git a/packages/create/src/cli/add/navigation-config.ts b/packages/create/src/cli/add/navigation-config.ts index 813b9c3554..96d60c4107 100644 --- a/packages/create/src/cli/add/navigation-config.ts +++ b/packages/create/src/cli/add/navigation-config.ts @@ -59,7 +59,7 @@ async function addInboundNavigationConfig(basePath: string, simulate: boolean): const appType = await getAppType(basePath); const isAdp = appType === 'Fiori Adaptation'; - if (isAdp && flpConfigurationExists(basePath)) { + if (isAdp && (await flpConfigurationExists(basePath))) { logger.info('FLP Configuration already exists.'); return; } @@ -137,7 +137,7 @@ async function retrieveManifest(basePath: string, fs: Editor): Promise * @throws {Error} If the project is not CloudReady. */ async function retrieveMergedManifest(basePath: string, logger: ToolsLogger): Promise { - const variant = getVariant(basePath); + const variant = await getVariant(basePath); const { target, ignoreCertErrors = false } = await getAdpConfig(basePath, join(basePath, FileName.Ui5Yaml)); const provider = await createAbapServiceProvider(target, { ignoreCertErrors }, true, logger); diff --git a/packages/create/src/cli/add/new-model.ts b/packages/create/src/cli/add/new-model.ts index 9c51f80c2b..2b274fb68b 100644 --- a/packages/create/src/cli/add/new-model.ts +++ b/packages/create/src/cli/add/new-model.ts @@ -35,7 +35,7 @@ async function addNewModel(basePath: string, simulate: boolean): Promise { await validateAdpProject(basePath); - const variant = getVariant(basePath); + const variant = await getVariant(basePath); const answers = await promptYUIQuestions(getPromptsForNewModel(basePath, variant.layer), false); diff --git a/packages/create/src/cli/change/change-data-source.ts b/packages/create/src/cli/change/change-data-source.ts index 1f6f38611e..fe3774f113 100644 --- a/packages/create/src/cli/change/change-data-source.ts +++ b/packages/create/src/cli/change/change-data-source.ts @@ -42,7 +42,7 @@ async function changeDataSource(basePath: string, simulate: boolean, yamlPath: s basePath = process.cwd(); } await validateAdpProject(basePath); - const variant = getVariant(basePath); + const variant = await getVariant(basePath); const { target, ignoreCertErrors = false } = await getAdpConfig(basePath, yamlPath); const provider = await createAbapServiceProvider( target, diff --git a/packages/create/src/cli/change/change-inbound.ts b/packages/create/src/cli/change/change-inbound.ts index 9dffa6bfed..b65d0198a6 100644 --- a/packages/create/src/cli/change/change-inbound.ts +++ b/packages/create/src/cli/change/change-inbound.ts @@ -32,8 +32,8 @@ async function changeInbound(basePath: string, simulate: boolean): Promise } await validateAdpProject(basePath); - validateCloudAdpProject(basePath); - const variant = getVariant(basePath); + await validateCloudAdpProject(basePath); + const variant = await getVariant(basePath); const change = variant.content.find( (change: DescriptorVariantContent) => change.changeType === 'appdescr_app_removeAllInboundsExceptOne' ); diff --git a/packages/create/src/validation/validation.ts b/packages/create/src/validation/validation.ts index e1dfcab67d..be3b5c77fc 100644 --- a/packages/create/src/validation/validation.ts +++ b/packages/create/src/validation/validation.ts @@ -53,8 +53,8 @@ export async function validateAdpProject(basePath: string): Promise { * * @param basePath - path to the adaptation project */ -export function validateCloudAdpProject(basePath: string): void { - const manifest = getVariant(basePath); +export async function validateCloudAdpProject(basePath: string): Promise { + const manifest = await getVariant(basePath); if ( !manifest?.content?.some( (change: DescriptorVariantContent) => change.changeType === 'appdescr_app_removeAllInboundsExceptOne' diff --git a/packages/create/test/unit/validation/validation.test.ts b/packages/create/test/unit/validation/validation.test.ts index 6e863400c6..d537c9a58b 100644 --- a/packages/create/test/unit/validation/validation.test.ts +++ b/packages/create/test/unit/validation/validation.test.ts @@ -26,10 +26,10 @@ describe('validation', () => { const descriptorVariant = JSON.parse( readFileSync(join(__dirname, '../../fixtures/adaptation-project', 'manifest.appdescr_variant'), 'utf-8') ); - test('throw error for omPremise project', () => { + test('throw error for omPremise project', async () => { jest.spyOn(adp, 'getVariant').mockReturnValue(descriptorVariant); try { - validateCloudAdpProject(''); + await validateCloudAdpProject(''); fail('The function should have thrown an error.'); } catch (error) { expect(error.message).toBe('This command can only be used for Cloud Adaptation Project.'); diff --git a/packages/i18n/README.md b/packages/i18n/README.md index 82ec97c4ff..22ef634659 100644 --- a/packages/i18n/README.md +++ b/packages/i18n/README.md @@ -42,9 +42,10 @@ For detailed example usage check unit test of [`getCapI18nBundle`](./test/unit/r ```typescript import { getPropertiesI18nBundle } from '@sap-ux/i18n'; import { join } from 'path'; +import { getWebappPath } from '@sap-ux/project-access'; const PROJECT_ROOT = 'absolute/path/to/project'; -const i18nFilePath = join(PROJECT_ROOT, 'webapp', 'i18n', 'i18n.properties'); +const i18nFilePath = join(await getWebappPath(PROJECT_ROOT), 'i18n', 'i18n.properties'); const bundle = await getPropertiesI18nBundle(i18nFilePath); ``` @@ -82,6 +83,7 @@ For detailed example usage check unit test of [`createCapI18nEntries`](./test/un ```typescript import { createPropertiesI18nEntries } from '@sap-ux/i18n'; import { join } from 'path'; +import { getWebappPath } from '@sap-ux/project-access'; const newEntries = [ { @@ -90,7 +92,7 @@ const newEntries = [ } ]; const PROJECT_ROOT = 'absolute/path/to/project'; -const i18nFilePath = join(PROJECT_ROOT, 'webapp', 'i18n', 'i18n.properties'); +const i18nFilePath = join(getWebappPath(PROJECT_ROOT), 'i18n', 'i18n.properties'); const result = await createPropertiesI18nEntries(i18nFilePath, newEntries, PROJECT_ROOT); ``` diff --git a/packages/mockserver-config-writer/src/mockserver-config/ui5-mock-yaml.ts b/packages/mockserver-config-writer/src/mockserver-config/ui5-mock-yaml.ts index db321095b7..8966d8e6b1 100644 --- a/packages/mockserver-config-writer/src/mockserver-config/ui5-mock-yaml.ts +++ b/packages/mockserver-config-writer/src/mockserver-config/ui5-mock-yaml.ts @@ -1,4 +1,4 @@ -import { join } from 'path'; +import { join, posix, relative, sep } from 'path'; import type { Editor } from 'mem-fs-editor'; import { UI5Config } from '@sap-ux/ui5-config'; import type { CustomMiddleware, DataSourceConfig } from '@sap-ux/ui5-config'; @@ -46,8 +46,11 @@ export async function enhanceYaml( annotationSource.forEach((annotation) => { // Ignore local annotations from YAML file, those are linked through manifest file if (annotation.settings?.localUri !== annotation.uri) { + const localUri = annotation.settings?.localUri; annotationsConfig.push({ - localPath: `./webapp/${annotation.settings?.localUri}`, + localPath: localUri + ? `.${posix.sep}${relative(basePath, join(webappPath, localUri)).replaceAll(sep, posix.sep)}` + : undefined, urlPath: annotation.uri }); } @@ -60,7 +63,9 @@ export async function enhanceYaml( dataSourcesConfig.push({ serviceName: dataSource, servicePath: dataSources[dataSource].uri, - metadataPath: localUri ? `./webapp/${localUri}` : undefined + metadataPath: localUri + ? `.${posix.sep}${relative(basePath, join(webappPath, localUri)).replaceAll(sep, posix.sep)}` + : undefined }); } diff --git a/packages/odata-service-writer/src/data/annotations.ts b/packages/odata-service-writer/src/data/annotations.ts index e50185d755..517c4fee3c 100644 --- a/packages/odata-service-writer/src/data/annotations.ts +++ b/packages/odata-service-writer/src/data/annotations.ts @@ -4,6 +4,7 @@ import { XMLParser } from 'fast-xml-parser'; import { t } from '../i18n'; import type { NamespaceAlias, OdataService, EdmxAnnotationsInfo, EdmxOdataService, CdsAnnotationsInfo } from '../types'; import prettifyXml from 'prettify-xml'; +import { getWebappPath, DirName } from '@sap-ux/project-access'; /** * Updates the cds index or service file with the provided annotations. @@ -15,7 +16,7 @@ import prettifyXml from 'prettify-xml'; * @returns {Promise} A promise that resolves when the cds files have been updated. */ async function updateCdsIndexOrServiceFile(fs: Editor, annotations: CdsAnnotationsInfo): Promise { - const dirPath = join(annotations.projectName, 'annotations'); + const dirPath = join(annotations.projectName, DirName.Annotations); const annotationPath = normalize(dirPath).split(/[\\/]/g).join(posix.sep); const annotationConfig = `\nusing from './${annotationPath}';`; // get index and service file paths @@ -84,7 +85,7 @@ export async function updateCdsFilesWithAnnotations( * @returns {Promise} A promise that resolves when the cds files have been updated. */ async function removeCdsIndexOrServiceFile(fs: Editor, annotations: CdsAnnotationsInfo): Promise { - const dirPath = join(annotations.projectName, 'annotations'); + const dirPath = join(annotations.projectName, DirName.Annotations); const annotationPath = normalize(dirPath).split(/[\\/]/g).join(posix.sep); const annotationConfig = `\nusing from './${annotationPath}';`; // Get index and service file paths @@ -157,14 +158,12 @@ export async function removeAnnotationsFromCDSFiles( * Writes local copies of metadata.xml and local annotations. * * @param {Editor} fs - the memfs editor instance - * @param {string} basePath - the root path of an existing UI5 application * @param {string} webappPath - the webapp path of an existing UI5 application * @param {string} templateRoot - path to the file templates * @param {OdataService} service - the OData service instance with EDMX type */ export async function writeLocalServiceAnnotationXMLFiles( fs: Editor, - basePath: string, webappPath: string, templateRoot: string, service: EdmxOdataService @@ -177,7 +176,7 @@ export async function writeLocalServiceAnnotationXMLFiles( const namespaces = getAnnotationNamespaces(service); fs.copyTpl( join(templateRoot, 'add', 'annotation.xml'), - join(basePath, 'webapp', 'annotations', `${service.localAnnotationsName}.xml`), + join(webappPath, DirName.Annotations, `${service.localAnnotationsName}.xml`), { ...service, namespaces } ); } @@ -194,7 +193,7 @@ export async function writeMetadata(fs: Editor, webappPath: string, service: Edm if (service.metadata) { // mainService should be used in case there is no name defined for service fs.write( - join(webappPath, 'localService', service.name ?? 'mainService', 'metadata.xml'), + join(webappPath, DirName.LocalService, service.name ?? 'mainService', 'metadata.xml'), prettifyXml(service.metadata, { indent: 4 }) ); } @@ -208,20 +207,20 @@ export async function writeMetadata(fs: Editor, webappPath: string, service: Edm * @param {string} serviceName - Name of The OData service. * @param {OdataService} edmxAnnotations - The OData service annotations. */ -export function removeRemoteServiceAnnotationXmlFiles( +export async function removeRemoteServiceAnnotationXmlFiles( fs: Editor, basePath: string, serviceName: string, edmxAnnotations: EdmxAnnotationsInfo | EdmxAnnotationsInfo[] -): void { +): Promise { + const webappPath = await getWebappPath(basePath, fs); // Write annotation xml if annotations are provided and service type is EDMX if (Array.isArray(edmxAnnotations)) { for (const annotationName in edmxAnnotations) { const annotation = edmxAnnotations[annotationName]; const pathToAnnotationFile = join( - basePath, - 'webapp', - 'localService', + webappPath, + DirName.LocalService, serviceName, `${annotation.technicalName}.xml` ); @@ -231,9 +230,8 @@ export function removeRemoteServiceAnnotationXmlFiles( } } else if (edmxAnnotations?.xml) { const pathToAnnotationFile = join( - basePath, - 'webapp', - 'localService', + webappPath, + DirName.LocalService, serviceName, `${edmxAnnotations.technicalName}.xml` ); @@ -251,26 +249,27 @@ export function removeRemoteServiceAnnotationXmlFiles( * @param {string} serviceName - Name of The OData service. * @param {OdataService} edmxAnnotations - The OData service annotations. */ -export function writeRemoteServiceAnnotationXmlFiles( +export async function writeRemoteServiceAnnotationXmlFiles( fs: Editor, basePath: string, serviceName: string, edmxAnnotations: EdmxAnnotationsInfo | EdmxAnnotationsInfo[] -): void { +): Promise { + const webappPath = await getWebappPath(basePath, fs); // Write annotation xml if annotations are provided and service type is EDMX if (Array.isArray(edmxAnnotations)) { for (const annotationName in edmxAnnotations) { const annotation = edmxAnnotations[annotationName]; if (annotation?.xml) { fs.write( - join(basePath, 'webapp', 'localService', serviceName, `${annotation.technicalName}.xml`), + join(webappPath, DirName.LocalService, serviceName, `${annotation.technicalName}.xml`), prettifyXml(annotation.xml, { indent: 4 }) ); } } } else if (edmxAnnotations?.xml) { fs.write( - join(basePath, 'webapp', 'localService', serviceName, `${edmxAnnotations.technicalName}.xml`), + join(webappPath, DirName.LocalService, serviceName, `${edmxAnnotations.technicalName}.xml`), prettifyXml(edmxAnnotations.xml, { indent: 4 }) ); } diff --git a/packages/odata-service-writer/src/delete.ts b/packages/odata-service-writer/src/delete.ts index 08bb0e0d4c..1d067ba156 100644 --- a/packages/odata-service-writer/src/delete.ts +++ b/packages/odata-service-writer/src/delete.ts @@ -73,7 +73,7 @@ export async function deleteServiceData( ui5MockConfig.removeServiceFromMockServerMiddleware(service.path, serviceAnnotationPaths); fs.write(paths.ui5MockYaml, ui5MockConfig.toString()); } - removeRemoteServiceAnnotationXmlFiles( + await removeRemoteServiceAnnotationXmlFiles( fs, basePath, service.name, diff --git a/packages/odata-service-writer/src/update.ts b/packages/odata-service-writer/src/update.ts index afee7de19a..1e12b79a1d 100644 --- a/packages/odata-service-writer/src/update.ts +++ b/packages/odata-service-writer/src/update.ts @@ -116,7 +116,7 @@ export async function addServicesData( extendBackendMiddleware(fs, service, ui5MockConfig, paths.ui5MockYaml); } } - await writeLocalServiceAnnotationXMLFiles(fs, basePath, webappPath, templateRoot, service); + await writeLocalServiceAnnotationXMLFiles(fs, webappPath, templateRoot, service); } // service update should not trigger the package.json update if (paths.packageJson && paths.ui5Yaml) { @@ -125,7 +125,7 @@ export async function addServicesData( if (paths.ui5LocalYaml && ui5LocalConfig) { fs.write(paths.ui5LocalYaml, ui5LocalConfig.toString()); } - writeRemoteServiceAnnotationXmlFiles(fs, basePath, service.name ?? 'mainService', service.annotations); + await writeRemoteServiceAnnotationXmlFiles(fs, basePath, service.name ?? 'mainService', service.annotations); } /** @@ -188,5 +188,5 @@ export async function updateServicesData( await writeMetadata(fs, webappPath, service); } // Write new annotations files - writeRemoteServiceAnnotationXmlFiles(fs, basePath, service.name ?? 'mainService', service.annotations); + await writeRemoteServiceAnnotationXmlFiles(fs, basePath, service.name ?? 'mainService', service.annotations); } diff --git a/packages/odata-service-writer/test/__snapshots__/index.test.ts.snap b/packages/odata-service-writer/test/__snapshots__/index.test.ts.snap index e2f41ce215..8b36a536e4 100644 --- a/packages/odata-service-writer/test/__snapshots__/index.test.ts.snap +++ b/packages/odata-service-writer/test/__snapshots__/index.test.ts.snap @@ -2821,7 +2821,7 @@ Object { mockdataPath: ./webapp/localService/mainService/data generateMockData: true annotations: - - localPath: ./webapp/localService/mainService//SEPM_XYZ/SERVICE.xml + - localPath: ./webapp/localService/mainService/SEPM_XYZ/SERVICE.xml urlPath: /sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='%2FSEPM_XYZ%2FSERVICE',Version='0001')/$value/ ", "state": "modified", @@ -2851,7 +2851,7 @@ Object { mockdataPath: ./webapp/localService/mainService/data generateMockData: true annotations: - - localPath: ./webapp/localService/mainService//SEPM_XYZ/SERVICE.xml + - localPath: ./webapp/localService/mainService/SEPM_XYZ/SERVICE.xml urlPath: /sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='%2FSEPM_XYZ%2FSERVICE',Version='0001')/$value/ ", "state": "modified", diff --git a/packages/telemetry/src/tooling-telemetry/data-processor.ts b/packages/telemetry/src/tooling-telemetry/data-processor.ts index 8e69b45423..5b01633987 100644 --- a/packages/telemetry/src/tooling-telemetry/data-processor.ts +++ b/packages/telemetry/src/tooling-telemetry/data-processor.ts @@ -24,6 +24,7 @@ import { spawn } from 'child_process'; import os from 'os'; import type { CustomTask } from '@sap-ux/ui5-config'; import { ToolingTelemetrySettings } from './config-state'; +import { getWebappPath } from '@sap-ux/project-access'; /** * Collect commone properties that needs to be added to telemetry event. @@ -270,9 +271,10 @@ function getInternalVsExternal(): InternalFeature { * @returns {Promise} A promise that resolves to the source template configuration object. */ async function getSourceTemplate(appPath: string): Promise { + const webappPath = await getWebappPath(appPath); const paths = { - manifest: path.join(appPath, 'webapp', 'manifest.json'), - appdescr: path.join(appPath, 'webapp', 'manifest.appdescr_variant'), + manifest: path.join(webappPath, 'manifest.json'), + appdescr: path.join(webappPath, 'manifest.appdescr_variant'), ui5Yaml: path.join(appPath, 'ui5.yaml') };