Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions jest.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ module.exports = {
'<rootDir>/dist',
'<rootDir>/coverage',
'<rootDir>/templates',
'<rootDir>/test/dist',
'<rootDir>/test/test-input',
'<rootDir>/test/test-output',
'<rootDir>/test/integration'
],
testPathIgnorePatterns: ['<rootDir>/test/dist', '<rootDir>/test/.*/dist'],
verbose: true,
snapshotFormat: {
escapeString: true,
Expand Down
1 change: 1 addition & 0 deletions packages/project-access/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"dependencies": {
"@sap-ux/i18n": "workspace:*",
"@sap-ux/ui5-config": "workspace:*",
"@sap-ux/project-analyser": "workspace:*",
"fast-xml-parser": "4.4.1",
"findit2": "2.2.3",
"json-parse-even-better-errors": "4.0.0",
Expand Down
28 changes: 28 additions & 0 deletions packages/project-access/src/project/access.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { join, relative } from 'node:path';
import type { Logger } from '@sap-ux/logger';
import type { NewI18nEntry } from '@sap-ux/i18n';
import { analyzeApp } from '@sap-ux/project-analyser';
import type { AnalysisResult } from '@sap-ux/project-analyser';
import type {
ApplicationAccess,
ApplicationAccessOptions,
Expand Down Expand Up @@ -30,6 +33,7 @@ import type { Editor } from 'mem-fs-editor';
import { updateManifestJSON, updatePackageJSON } from '../file';
import { FileName } from '../constants';
import { getSpecification } from './specification';
import { getWebappPath } from './ui5-config';

/**
*
Expand Down Expand Up @@ -182,6 +186,30 @@ class ApplicationAccessImp implements ApplicationAccess {
return getSpecification(this.app.appRoot);
}

/**
* Analyse the current application and return its bill of materials insights.
*
* @param logger - optional logger used during analysis
* @returns analysis result produced by @sap-ux/project-analyser
*/
async getAppAnalysis(logger?: Logger): Promise<AnalysisResult> {
const effectiveLogger = logger ?? this.options?.logger;
let webappPath: string;
try {
webappPath = await getWebappPath(this.app.appRoot, this.options?.fs);
} catch (error: unknown) {
effectiveLogger?.debug?.(`Unable to resolve webapp path via ui5.yaml: ${String(error)}`);
webappPath = join(this.app.appRoot, 'webapp');
}

return analyzeApp(
{
appPath: webappPath
},
effectiveLogger
);
}

/**
* Updates package.json file asynchronously by keeping the previous indentation.
*
Expand Down
7 changes: 7 additions & 0 deletions packages/project-access/src/types/access/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Logger } from '@sap-ux/logger';
import type { NewI18nEntry } from '@sap-ux/i18n';
import type { AnalysisResult } from '@sap-ux/project-analyser';
import type { I18nBundles } from '../i18n';
import type { ApplicationStructure, I18nPropertiesPaths, Project, ProjectType } from '../info';
import type { Editor } from 'mem-fs-editor';
Expand Down Expand Up @@ -104,6 +105,12 @@ export interface ApplicationAccess extends BaseAccess {
* Returns an instance of @sap/ux-specification for the application.
*/
getSpecification<T>(): Promise<T>;
/**
* Analyses the application and returns the bill of materials summary.
*
* @param logger - optional logger to capture diagnostics during analysis
*/
getAppAnalysis(logger?: Logger): Promise<AnalysisResult>;
/**
* Updates package.json file asynchronously by keeping the previous indentation.
*
Expand Down
48 changes: 48 additions & 0 deletions packages/project-access/test/project/access.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { join } from 'node:path';
import type { AnalysisResult } from '@sap-ux/project-analyser';
import { analyzeApp } from '@sap-ux/project-analyser';
import * as ui5Config from '../../src/project/ui5-config';
import type { Manifest, Package } from '../../src';
import { createApplicationAccess, createProjectAccess } from '../../src';
import * as i18nMock from '../../src/project/i18n/write';
Expand All @@ -7,10 +10,17 @@ import { create as createStorage } from 'mem-fs';
import { create } from 'mem-fs-editor';
import { promises } from 'node:fs';

jest.mock('@sap-ux/project-analyser', () => ({
analyzeApp: jest.fn()
}));

describe('Test function createApplicationAccess()', () => {
const memFs = create(createStorage());
const analyzeAppMock = analyzeApp as jest.MockedFunction<typeof analyzeApp>;
beforeEach(() => {
jest.restoreAllMocks();
analyzeAppMock.mockReset();
analyzeAppMock.mockResolvedValue({ status: 'not-implemented' });
});

const sampleRoot = join(__dirname, '../test-data/project/info');
Expand Down Expand Up @@ -365,6 +375,44 @@ describe('Test function createApplicationAccess()', () => {
expect(spec).toEqual({ test: 'specification' });
});

test('getAppAnalysis delegates to analyser with resolved webapp path', async () => {
const appRoot = join(sampleRoot, 'fiori_elements');
const appAccess = await createApplicationAccess(appRoot);
const expected: AnalysisResult = { status: 'success' };
analyzeAppMock.mockResolvedValueOnce(expected);

const result = await appAccess.getAppAnalysis();

expect(result).toBe(expected);
expect(analyzeAppMock).toHaveBeenCalledWith({ appPath: join(appRoot, 'webapp') }, undefined);
});

test('getAppAnalysis respects custom webapp path from ui5.yaml', async () => {
const projectRoot = join(__dirname, '../test-data/project/webapp-path/custom-webapp-path');
const projectAccess = await createProjectAccess(projectRoot);
const appAccess = projectAccess.getApplication(projectAccess.getApplicationIds()[0]);
const expected: AnalysisResult = { status: 'success' };
analyzeAppMock.mockResolvedValueOnce(expected);

const result = await appAccess.getAppAnalysis();

expect(result).toBe(expected);
expect(analyzeAppMock).toHaveBeenCalledWith({ appPath: join(projectRoot, 'src/webapp') }, undefined);
});

test('getAppAnalysis falls back to default path when ui5.yaml cannot be parsed', async () => {
const appRoot = join(sampleRoot, 'fiori_elements');
const appAccess = await createApplicationAccess(appRoot);
const expected: AnalysisResult = { status: 'success' };
analyzeAppMock.mockResolvedValueOnce(expected);
jest.spyOn(ui5Config, 'getWebappPath').mockRejectedValueOnce(new Error('invalid yaml'));

const result = await appAccess.getAppAnalysis();

expect(result).toBe(expected);
expect(analyzeAppMock).toHaveBeenCalledWith({ appPath: join(appRoot, 'webapp') }, undefined);
});

test('Error handling for non existing app', async () => {
try {
await createApplicationAccess('non-existing-app');
Expand Down
38 changes: 22 additions & 16 deletions packages/project-access/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
{
"extends": "../../tsconfig.json",
"include": ["../../types/mem-fs-editor.d.ts", "src"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
"extends": "../../tsconfig.json",
"include": [
"../../types/mem-fs-editor.d.ts",
"src"
],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"references": [
{
"path": "../i18n"
},
"references": [
{
"path": "../i18n"
},
{
"path": "../logger"
},
{
"path": "../ui5-config"
}
]
{
"path": "../logger"
},
{
"path": "../project-analyser"
},
{
"path": "../ui5-config"
}
]
}
8 changes: 8 additions & 0 deletions packages/project-analyser/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
extends: ['../../.eslintrc'],
parserOptions: {
project: './tsconfig.eslint.json',
tsconfigRootDir: __dirname
}
};

Loading
Loading