@sap-ux/project-access
Version:
Library to access SAP Fiori tools projects
383 lines • 14.1 kB
JavaScript
import { join, relative } from 'node:path';
import { getI18nBundles, getI18nPropertiesPaths, createCapI18nEntries, createManifestI18nEntries, createUI5I18nEntries, createAnnotationI18nEntries } from './i18n/index.js';
import { getProject } from './info.js';
import { findAllApps } from './search.js';
import { readFile, readJSON, updateManifestJSON, updatePackageJSON } from '../file/index.js';
import { FileName } from '../constants.js';
import { getSpecification } from './specification.js';
import { readFlexChanges } from './flex-changes.js';
import { readCapServiceMetadataEdmx } from './cap.js';
/**
*
*/
class ApplicationAccessImp {
_project;
appId;
options;
/**
* Constructor for ApplicationAccess.
*
* @param _project - Project structure
* @param appId - Application ID
* @param options - optional options, see below
* @param options.fs - optional `mem-fs-editor` instance
*/
constructor(_project, appId, options) {
this._project = _project;
this.appId = appId;
this.options = options;
}
/**
* Returns the application structure.
*
* @returns ApplicationStructure
*/
get app() {
return this.project.apps[this.appId];
}
/**
* Maintains new translation entries in an existing i18n file or in a new i18n properties file if it does not exist.
*
* @param newEntries - translation entries to write in the `.properties` file
* @returns - boolean or exception
* @description It also update `manifest.json` file if `@i18n` entry is missing from `"sap.ui5":{"models": {}}`
* as
* ```JSON
* {
* "sap.ui5": {
* "models": {
* "@i18n": {
* "type": "sap.ui.model.resource.ResourceModel",
* "uri": "i18n/i18n.properties"
* }
* }
* }
* }
* ```
*/
createAnnotationI18nEntries(newEntries) {
return createAnnotationI18nEntries(this.project.root, this.app.manifest, this.app.i18n, newEntries, this.options?.fs);
}
/**
* Maintains new translation entries in an existing i18n file or in a new i18n properties file if it does not exist.
*
* @param newEntries - translation entries to write in the `.properties` file
* @param modelKey - i18n model key. Default key is `i18n`
* @returns boolean or exception
* @description It also update `manifest.json` file if `<modelKey>` entry is missing from `"sap.ui5":{"models": {}}`
* as
* ```JSON
* {
* "sap.ui5": {
* "models": {
* "<modelKey>": {
* "type": "sap.ui.model.resource.ResourceModel",
* "uri": "i18n/i18n.properties"
* }
* }
* }
* }
* ```
*/
createUI5I18nEntries(newEntries, modelKey = 'i18n') {
return createUI5I18nEntries(this.project.root, this.app.manifest, this.app.i18n, newEntries, modelKey, this.options?.fs);
}
/**
* Maintains new translation entries in an existing i18n file or in a new i18n properties file if it does not exist.
*
* @param newEntries translation entries to write in the `.properties` file
* @returns boolean or exception
* @description If `i18n` entry is missing from `"sap.app":{}`, default `i18n/i18n.properties` is used. Update of `manifest.json` file is not needed.
*/
createManifestI18nEntries(newEntries) {
return createManifestI18nEntries(this.project.root, this.app.i18n, newEntries, this.options?.fs);
}
/**
* Maintains new translation entries in CAP i18n files.
*
* @param filePath absolute path to file in which the translation entry will be used.
* @param newI18nEntries translation entries to write in the i18n file.
* @returns boolean or exception
*/
createCapI18nEntries(filePath, newI18nEntries) {
return createCapI18nEntries(this.project.root, filePath, newI18nEntries, this.options?.fs);
}
/**
* Return the application id of this app, which is the relative path from the project root
* to the app root.
*
* @returns - Application root path
*/
getAppId() {
return this.appId;
}
/**
* Return the absolute application root path.
*
* @returns - Application root path
*/
getAppRoot() {
return this.app.appRoot;
}
/**
* For a given app in project, retrieves i18n bundles for 'sap.app' namespace,`models` of `sap.ui5` namespace and service for cap services.
*
* @returns i18n bundles or exception captured in optional errors object
*/
getI18nBundles() {
return getI18nBundles(this.project.root, this.app.i18n, this.project.projectType, this.options?.fs);
}
/**
* Return absolute paths to i18n.properties files from manifest.
*
* @returns absolute paths to i18n.properties
*/
getI18nPropertiesPaths() {
return getI18nPropertiesPaths(this.app.manifest);
}
/**
* Return an instance of @sap/ux-specification specific to the application version.
*
* @returns - instance of @sap/ux-specification
*/
async getSpecification() {
return getSpecification(this.app.appRoot);
}
/**
* Updates package.json file asynchronously by keeping the previous indentation.
*
* @param packageJson - updated package.json file content
* @param memFs - optional mem-fs-editor instance
*/
async updatePackageJSON(packageJson, memFs) {
await updatePackageJSON(join(this.app.appRoot, FileName.Package), packageJson, memFs ?? this.options?.fs);
}
/**
* Updates manifest.json file asynchronously by keeping the previous indentation.
*
* @param manifest - updated manifest.json file content
* @param memFs - optional mem-fs-editor instance
*/
async updateManifestJSON(manifest, memFs) {
await updateManifestJSON(this.app.manifest, manifest, memFs ?? this.options?.fs);
}
/**
* Reads and returns the parsed `manifest.json` file for the application.
*
* @param memFs - optional mem-fs-editor instance
* @returns A promise resolving to the parsed `manifest.json` content.
*/
async readManifest(memFs) {
return readJSON(this.app.manifest, memFs ?? this.options?.fs);
}
/**
* Reads and returns all Flex Changes (`*.change` files) associated with the application.
*
* @param memFs - optional mem-fs-editor instance
* @returns A promise that resolves to an array of flex change files.
*/
async readFlexChanges(memFs) {
return readFlexChanges(this.app.changes, memFs ?? this.options?.fs);
}
/**
* Reads and returns all annotation files associated with the application's main service.
*
* @param memFs - optional mem-fs-editor instance
* @returns A promise resolving to an array of annotation file descriptors.
*/
async readAnnotationFiles(memFs) {
const annotationData = [];
const mainServiceName = this.app.mainService ?? 'mainService';
const mainService = this.app?.services?.[mainServiceName];
if (!mainService) {
return [];
}
if (mainService.uri && (this.projectType === 'CAPJava' || this.projectType === 'CAPNodejs')) {
const serviceUri = mainService?.uri ?? '';
if (serviceUri) {
const edmx = await readCapServiceMetadataEdmx(this.root, serviceUri);
annotationData.push({
fileContent: edmx,
dataSourceUri: serviceUri
});
}
}
else {
if (mainService.local) {
const serviceFile = await readFile(mainService.local, memFs ?? this.options?.fs);
annotationData.push({
dataSourceUri: mainService.local,
fileContent: serviceFile.toString()
});
}
const { annotations = [] } = mainService;
for (const annotation of annotations) {
if (annotation.local) {
const annotationFile = await readFile(annotation.local, memFs ?? this.options?.fs);
annotationData.push({
dataSourceUri: annotation.local,
fileContent: annotationFile.toString()
});
}
}
}
return annotationData;
}
/**
* Project structure.
*
* @returns - Project structure
*/
get project() {
return this._project;
}
/**
* Project type.
*
* @returns - Project type, like EDMXBackend, CAPJava, or CAPNodejs
*/
get projectType() {
return this.project.projectType;
}
/**
* Project root path.
*
* @returns - Project root path
*/
get root() {
return this.project.root;
}
}
/**
* Class that implements ProjectAccess interface.
* It can be used to retrieve information about the project, like applications, paths, services.
*/
class ProjectAccessImp {
_project;
options;
/**
* Constructor for ProjectAccess.
*
* @param _project - Project structure
* @param options - optional options, like logger
*/
constructor(_project, options) {
this._project = _project;
this.options = options;
}
/**
* Returns list of application IDs.
*
* @returns - array of application IDs. For single application projects it will return ['']
*/
getApplicationIds() {
return Object.keys(this._project.apps);
}
/**
* Get application ID (the relative path from project root to app root) for a given 'sap.app.id' from the manifest.
*
* @param manifestAppId - The 'sap.app.id' from the manifest
* @returns - application ID (the relative path from project root to app root) or undefined if not found
*/
async getApplicationIdByManifestAppId(manifestAppId) {
for (const [appId, { manifest: manifestPath }] of Object.entries(this._project.apps)) {
const manifestContent = await readJSON(manifestPath, this.options?.memFs);
if (manifestContent['sap.app']?.id === manifestAppId) {
return appId;
}
}
return undefined;
}
/**
* Returns an instance of an application for a given application ID (the relative path from project root to app root, NOT the 'sap.app.id' from the manifest).
* It contains information about the application, like paths and services.
*
* @param appId - application ID (the relative path from project root to app root, NOT the 'sap.app.id' from the manifest)
* @returns - Instance of ApplicationAccess that contains information about the application, like paths and services
*/
getApplication(appId) {
if (!this.project.apps[appId]) {
throw new Error(`Could not find app with id ${appId}`);
}
return new ApplicationAccessImp(this.project, appId, this.options);
}
/**
* Project structure.
*
* @returns - Project structure
*/
get project() {
return this._project;
}
/**
* Project type.
*
* @returns - Project type, like EDMXBackend, CAPJava, or CAPNodejs
*/
get projectType() {
return this.project.projectType;
}
/**
* Project root path.
*
* @returns - Project root path
*/
get root() {
return this.project.root;
}
}
/**
* Type guard for Editor or ApplicationAccessOptions.
*
* @param argument - argument to check
* @returns true if argument is Editor, false if it is ApplicationAccessOptions
*/
function isEditor(argument) {
return argument.commit !== undefined;
}
/**
* Create an instance of ApplicationAccess that contains information about the application, like paths and services.
*
* @param appRoot - Application root path
* @param fs optional `mem-fs-editor` instance. If provided, `mem-fs-editor` api is used instead of `fs` of node.
* In case of CAP project, some CDS APIs are used internally which depends on `fs` of node and not `mem-fs-editor`.
* When calling this function, adding or removing a CDS file in memory or changing CDS configuration will not be considered until present on real file system.
* @returns - Instance of ApplicationAccess that contains information about the application, like paths and services
*/
export async function createApplicationAccess(appRoot, fs) {
try {
let options;
if (fs) {
options = isEditor(fs) ? { fs } : fs;
}
const apps = await findAllApps([appRoot], options?.fs);
const app = apps.find((app) => app.appRoot === appRoot);
if (!app) {
throw new Error(`Could not find app with root ${appRoot}`);
}
const project = await getProject(app.projectRoot, options?.fs);
const appId = relative(project.root, appRoot);
return new ApplicationAccessImp(project, appId, options);
}
catch (error) {
throw Error(`Error when creating application access for ${appRoot}: ${error}`);
}
}
/**
* Create an instance of ProjectAccess that contains information about the project, like applications, paths, services.
*
* @param root - Project root path
* @param options - optional options, e.g. logger instance.
* @returns - Instance of ProjectAccess that contains information about the project
*/
export async function createProjectAccess(root, options) {
try {
const project = await getProject(root, options?.memFs);
const projectAccess = new ProjectAccessImp(project, options);
return projectAccess;
}
catch (error) {
throw Error(`Error when creating project access for ${root}: ${error}`);
}
}
//# sourceMappingURL=access.js.map