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

Add Optional mem-fs-editor to getProjectType and underlying functions #2746

Merged
merged 16 commits into from
Jan 16, 2025
Merged
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
5 changes: 5 additions & 0 deletions .changeset/famous-moons-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ux/project-access': minor
---

Add optional mem-fs editor to function in project-access
6 changes: 3 additions & 3 deletions packages/project-access/src/project/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,12 +322,12 @@ export async function createApplicationAccess(
if (!app) {
throw new Error(`Could not find app with root ${appRoot}`);
}
const project = await getProject(app.projectRoot);
const appId = relative(project.root, appRoot);
let options: ApplicationAccessOptions | undefined;
if (fs) {
options = isEditor(fs) ? { fs } : fs;
}
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}`);
Expand All @@ -343,7 +343,7 @@ export async function createApplicationAccess(
*/
export async function createProjectAccess(root: string, options?: ProjectAccessOptions): Promise<ProjectAccess> {
try {
const project = await getProject(root);
const project = await getProject(root, options?.memFs);
const projectAccess = new ProjectAccessImp(project, options);
return projectAccess;
} catch (error) {
Expand Down
54 changes: 47 additions & 7 deletions packages/project-access/src/project/cap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { spawn } from 'child_process';
import { basename, dirname, join, normalize, relative, sep } from 'path';
import { basename, dirname, join, normalize, relative, sep, resolve } from 'path';
import type { Logger } from '@sap-ux/logger';
import type { Editor } from 'mem-fs-editor';
import { FileName } from '../constants';
Expand Down Expand Up @@ -63,30 +63,70 @@ export function isCapNodeJsProject(packageJson: Package): boolean {
*
* @param projectRoot - the root path of the project
* @param [capCustomPaths] - optional, relative CAP paths like app, db, srv
* @param memFs - optional mem-fs-editor instance
* @returns - true if the project is a CAP project
*/
export async function isCapJavaProject(projectRoot: string, capCustomPaths?: CapCustomPaths): Promise<boolean> {
export async function isCapJavaProject(
projectRoot: string,
capCustomPaths?: CapCustomPaths,
memFs?: Editor
): Promise<boolean> {
const srv = capCustomPaths?.srv ?? (await getCapCustomPaths(projectRoot)).srv;
return fileExists(join(projectRoot, srv, 'src', 'main', 'resources', FileName.CapJavaApplicationYaml));
return fileExists(join(projectRoot, srv, 'src', 'main', 'resources', FileName.CapJavaApplicationYaml), memFs);
}

/**
* Checks if there are files in the `srv` folder, using node fs or mem-fs.
*
* @param {string} srvFolderPath - The path to the `srv` folder to check for files.
* @param {Editor} [memFs] - An optional `mem-fs-editor` instance. If provided, the function checks files within the in-memory file system.
* @returns {Promise<boolean>} - Resolves to `true` if files are found in the `srv` folder; otherwise, `false`.
*/
async function checkFilesInSrvFolder(srvFolderPath: string, memFs?: Editor): Promise<boolean> {
if (!memFs) {
return await fileExists(srvFolderPath);
}
// Load the srv folder and its files into mem-fs
// This is necessary as mem-fs operates in-memory and doesn't automatically include files from disk.
// By loading the files, we ensure they are available within mem-fs.
if (await fileExists(srvFolderPath)) {
const fileSystemFiles = await readDirectory(srvFolderPath);
for (const file of fileSystemFiles) {
const filePath = join(srvFolderPath, file);
if (await fileExists(filePath)) {
const fileContent = await readFile(filePath);
memFs.write(filePath, fileContent);
}
}
}
// Dump the mem-fs state
const memFsDump = memFs.dump();
const memFsFiles = Object.keys(memFsDump).filter((filePath) => {
const normalisedFilePath = resolve(filePath);
const normalisedSrvPath = resolve(srvFolderPath);
return normalisedFilePath.startsWith(normalisedSrvPath);
});
return memFsFiles.length > 0;
}

/**
* Returns the CAP project type, undefined if it is not a CAP project.
*
* @param projectRoot - root of the project, where the package.json resides.
* @param memFs - optional mem-fs-editor instance
* @returns - CAPJava for Java based CAP projects; CAPNodejs for node.js based CAP projects; undefined if it is no CAP project
*/
export async function getCapProjectType(projectRoot: string): Promise<CapProjectType | undefined> {
export async function getCapProjectType(projectRoot: string, memFs?: Editor): Promise<CapProjectType | undefined> {
const capCustomPaths = await getCapCustomPaths(projectRoot);
if (!(await fileExists(join(projectRoot, capCustomPaths.srv)))) {
if (!(await checkFilesInSrvFolder(join(projectRoot, capCustomPaths.srv), memFs))) {
return undefined;
}
if (await isCapJavaProject(projectRoot, capCustomPaths)) {
if (await isCapJavaProject(projectRoot, capCustomPaths, memFs)) {
return 'CAPJava';
}
let packageJson;
try {
packageJson = await readJSON<Package>(join(projectRoot, FileName.Package));
packageJson = await readJSON<Package>(join(projectRoot, FileName.Package), memFs);
} catch {
// Ignore errors while reading the package.json file
}
Expand Down
10 changes: 8 additions & 2 deletions packages/project-access/src/project/i18n/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { dirname, join } from 'path';
import type { I18nPropertiesPaths, Manifest } from '../../types';
import { readJSON } from '../../file';
import type { Editor } from 'mem-fs-editor';

/**
* Return absolute paths to i18n.properties files from manifest.
*
* @param manifestPath - path to manifest.json; used to parse manifest.json if not provided as second argument and to resolve absolute paths
* @param manifest - optionally, parsed content of manifest.json, pass to avoid reading it again.
* @param memFs - optional mem-fs-editor instance
* @returns - absolute paths to i18n.properties
*/
export async function getI18nPropertiesPaths(manifestPath: string, manifest?: Manifest): Promise<I18nPropertiesPaths> {
const parsedManifest = manifest ?? (await readJSON<Manifest>(manifestPath));
export async function getI18nPropertiesPaths(
manifestPath: string,
manifest?: Manifest,
memFs?: Editor
): Promise<I18nPropertiesPaths> {
const parsedManifest = manifest ?? (await readJSON<Manifest>(manifestPath, memFs));
const manifestFolder = dirname(manifestPath);
const relativeI18nPropertiesPaths = getRelativeI18nPropertiesPaths(parsedManifest);
const i18nPropertiesPaths: I18nPropertiesPaths = {
Expand Down
60 changes: 40 additions & 20 deletions packages/project-access/src/project/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ import { gte, valid } from 'semver';
* Returns the project structure for a given Fiori project.
*
* @param root - project root folder
* @param memFs - optional mem-fs-editor instance
* @returns - project structure with project info like project type, apps, root folder
*/
export async function getProject(root: string): Promise<Project> {
if (!(await fileExists(join(root, FileName.Package)))) {
export async function getProject(root: string, memFs?: Editor): Promise<Project> {
if (!(await fileExists(join(root, FileName.Package), memFs))) {
throw new Error(`The project root folder '${root}' is not a Fiori project. No 'package.json' found.`);
}
const capProjectType = await getCapProjectType(root);
const projectType = capProjectType ?? 'EDMXBackend';
const appFolders = await getAppFolders(root);
const apps = await getApps(root, appFolders);
const appFolders = await getAppFolders(root, memFs);
const apps = await getApps(root, appFolders, memFs);
return {
root,
projectType,
Expand All @@ -46,10 +47,11 @@ export async function getProject(root: string): Promise<Project> {
* array of operating system specific relative paths to the apps.
*
* @param root - project root folder
* @param memFs - optional mem-fs-editor instance
* @returns - array of operating specific application folders
*/
async function getAppFolders(root: string): Promise<string[]> {
const apps = await findAllApps([root]);
async function getAppFolders(root: string, memFs?: Editor): Promise<string[]> {
const apps = await findAllApps([root], memFs);
return apps.length > 0 ? apps.map((app) => relative(root, app.appRoot)) : [''];
}

Expand All @@ -58,12 +60,17 @@ async function getAppFolders(root: string): Promise<string[]> {
*
* @param root - project root folder
* @param appFolders - array of relative application folders
* @param memFs - optional mem-fs-editor instance
* @returns - map of application structures
*/
async function getApps(root: string, appFolders: string[]): Promise<{ [index: string]: ApplicationStructure }> {
async function getApps(
root: string,
appFolders: string[],
memFs?: Editor
): Promise<{ [index: string]: ApplicationStructure }> {
const apps: { [index: string]: ApplicationStructure } = {};
for (const appFolder of appFolders) {
const applicationStructure = await getApplicationStructure(root, appFolder);
const applicationStructure = await getApplicationStructure(root, appFolder, memFs);
if (applicationStructure) {
apps[appFolder] = applicationStructure;
}
Expand All @@ -76,21 +83,26 @@ async function getApps(root: string, appFolders: string[]): Promise<{ [index: st
*
* @param root - project root folder
* @param appFolder - relative application folder
* @param memFs - optional mem-fs-editor instance
* @returns - application structure with application info like manifest, changes, main service, services, annotations
*/
async function getApplicationStructure(root: string, appFolder: string): Promise<ApplicationStructure | undefined> {
async function getApplicationStructure(
root: string,
appFolder: string,
memFs?: Editor
): Promise<ApplicationStructure | undefined> {
const appRoot = join(root, appFolder);
const absoluteWebappPath = await getWebappPath(appRoot);
const appType = (await getAppType(appRoot)) as AppType;
const absoluteWebappPath = await getWebappPath(appRoot, memFs);
const appType = (await getAppType(appRoot, memFs)) as AppType;
const manifest = join(absoluteWebappPath, FileName.Manifest);
if (!(await fileExists(manifest))) {
if (!(await fileExists(manifest, memFs))) {
return undefined;
}
const manifestObject = await readJSON<Manifest>(manifest);
const manifestObject = await readJSON<Manifest>(manifest, memFs);
const changes = join(absoluteWebappPath, DirName.Changes);
const i18n = await getI18nPropertiesPaths(manifest, manifestObject);
const i18n = await getI18nPropertiesPaths(manifest, manifestObject, memFs);
const mainService = getMainService(manifestObject);
const services = await getServicesAndAnnotations(manifest, manifestObject);
const services = await getServicesAndAnnotations(manifest, manifestObject, memFs);
return {
appRoot,
appType,
Expand Down Expand Up @@ -134,14 +146,16 @@ export async function getAppProgrammingLanguage(appRoot: string, memFs?: Editor)
* Get the type of application or Fiori artifact.
*
* @param appRoot - path to application root
* @param memFs - optional mem-fs-editor instance
* @returns - type of application, e.g. SAP Fiori elements, SAPUI5 freestyle, SAPUI5 Extension, ... see AppType.
*/
export async function getAppType(appRoot: string): Promise<AppType | undefined> {
export async function getAppType(appRoot: string, memFs?: Editor): Promise<AppType | undefined> {
let appType: AppType | undefined;
try {
const artifacts = await findFioriArtifacts({
wsFolders: [appRoot],
artifacts: ['adaptations', 'applications', 'extensions', 'libraries']
artifacts: ['adaptations', 'applications', 'extensions', 'libraries'],
memFs
});
if (
(artifacts.applications?.length ?? 0) +
Expand All @@ -151,7 +165,7 @@ export async function getAppType(appRoot: string): Promise<AppType | undefined>
1
) {
if (artifacts.applications?.length === 1) {
appType = await getApplicationType(artifacts.applications[0]);
appType = await getApplicationType(artifacts.applications[0], memFs);
} else if (artifacts.adaptations?.length === 1) {
appType = 'Fiori Adaptation';
} else if (artifacts.extensions?.length === 1) {
Expand All @@ -170,12 +184,18 @@ export async function getAppType(appRoot: string): Promise<AppType | undefined>
* Get the application type from search results.
*
* @param application - application from findFioriArtifacts() results
* @param memFs - optional mem-fs-editor instance
* @returns - type of application: 'SAP Fiori elements' or 'SAPUI5 freestyle'
*/
async function getApplicationType(application: AllAppResults): Promise<'SAP Fiori elements' | 'SAPUI5 freestyle'> {
async function getApplicationType(
application: AllAppResults,
memFs?: Editor
): Promise<'SAP Fiori elements' | 'SAPUI5 freestyle'> {
let appType: 'SAP Fiori elements' | 'SAPUI5 freestyle';
const rootPackageJsonPath = join(application.projectRoot, FileName.Package);
const packageJson = (await fileExists(rootPackageJsonPath)) ? await readJSON<Package>(rootPackageJsonPath) : null;
const packageJson = (await fileExists(rootPackageJsonPath, memFs))
? await readJSON<Package>(rootPackageJsonPath, memFs)
: null;

if (application.projectRoot === application.appRoot) {
appType = packageJson?.sapux ? 'SAP Fiori elements' : 'SAPUI5 freestyle';
Expand Down
Loading
Loading