diff --git a/.changeset/famous-moons-pump.md b/.changeset/famous-moons-pump.md new file mode 100644 index 00000000000..115b2bb15cc --- /dev/null +++ b/.changeset/famous-moons-pump.md @@ -0,0 +1,5 @@ +--- +'@sap-ux/project-access': minor +--- + +Add optional mem-fs editor to function in project-access diff --git a/packages/project-access/src/project/access.ts b/packages/project-access/src/project/access.ts index 9454f1f6ae0..ad0009bf4a0 100644 --- a/packages/project-access/src/project/access.ts +++ b/packages/project-access/src/project/access.ts @@ -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}`); @@ -343,7 +343,7 @@ export async function createApplicationAccess( */ export async function createProjectAccess(root: string, options?: ProjectAccessOptions): Promise { try { - const project = await getProject(root); + const project = await getProject(root, options?.memFs); const projectAccess = new ProjectAccessImp(project, options); return projectAccess; } catch (error) { diff --git a/packages/project-access/src/project/cap.ts b/packages/project-access/src/project/cap.ts index c3511ff219a..dee9572da7f 100644 --- a/packages/project-access/src/project/cap.ts +++ b/packages/project-access/src/project/cap.ts @@ -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'; @@ -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 { +export async function isCapJavaProject( + projectRoot: string, + capCustomPaths?: CapCustomPaths, + memFs?: Editor +): Promise { 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} - Resolves to `true` if files are found in the `srv` folder; otherwise, `false`. + */ +async function checkFilesInSrvFolder(srvFolderPath: string, memFs?: Editor): Promise { + 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 { +export async function getCapProjectType(projectRoot: string, memFs?: Editor): Promise { 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(join(projectRoot, FileName.Package)); + packageJson = await readJSON(join(projectRoot, FileName.Package), memFs); } catch { // Ignore errors while reading the package.json file } diff --git a/packages/project-access/src/project/i18n/i18n.ts b/packages/project-access/src/project/i18n/i18n.ts index f97433f4128..e7747ec3825 100644 --- a/packages/project-access/src/project/i18n/i18n.ts +++ b/packages/project-access/src/project/i18n/i18n.ts @@ -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 { - const parsedManifest = manifest ?? (await readJSON(manifestPath)); +export async function getI18nPropertiesPaths( + manifestPath: string, + manifest?: Manifest, + memFs?: Editor +): Promise { + const parsedManifest = manifest ?? (await readJSON(manifestPath, memFs)); const manifestFolder = dirname(manifestPath); const relativeI18nPropertiesPaths = getRelativeI18nPropertiesPaths(parsedManifest); const i18nPropertiesPaths: I18nPropertiesPaths = { diff --git a/packages/project-access/src/project/info.ts b/packages/project-access/src/project/info.ts index 56a52d698ac..de5f6bd3929 100644 --- a/packages/project-access/src/project/info.ts +++ b/packages/project-access/src/project/info.ts @@ -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 { - if (!(await fileExists(join(root, FileName.Package)))) { +export async function getProject(root: string, memFs?: Editor): Promise { + 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, @@ -46,10 +47,11 @@ export async function getProject(root: string): Promise { * 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 { - const apps = await findAllApps([root]); +async function getAppFolders(root: string, memFs?: Editor): Promise { + const apps = await findAllApps([root], memFs); return apps.length > 0 ? apps.map((app) => relative(root, app.appRoot)) : ['']; } @@ -58,12 +60,17 @@ async function getAppFolders(root: string): Promise { * * @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; } @@ -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 { +async function getApplicationStructure( + root: string, + appFolder: string, + memFs?: Editor +): Promise { 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); + const manifestObject = await readJSON(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, @@ -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 { +export async function getAppType(appRoot: string, memFs?: Editor): Promise { 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) + @@ -151,7 +165,7 @@ export async function getAppType(appRoot: string): Promise 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) { @@ -170,12 +184,18 @@ export async function getAppType(appRoot: string): Promise * 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(rootPackageJsonPath) : null; + const packageJson = (await fileExists(rootPackageJsonPath, memFs)) + ? await readJSON(rootPackageJsonPath, memFs) + : null; if (application.projectRoot === application.appRoot) { appType = packageJson?.sapux ? 'SAP Fiori elements' : 'SAPUI5 freestyle'; diff --git a/packages/project-access/src/project/search.ts b/packages/project-access/src/project/search.ts index 8f4e6602257..54c1ce84fbb 100644 --- a/packages/project-access/src/project/search.ts +++ b/packages/project-access/src/project/search.ts @@ -1,4 +1,5 @@ import { basename, dirname, isAbsolute, join, parse, sep } from 'path'; +import type { Editor } from 'mem-fs-editor'; import type { AdaptationResults, AllAppResults, @@ -77,10 +78,16 @@ function wsFoldersToRootPaths(wsFolders: readonly WorkspaceFolder[] | string[] | * @param path path of a project file * @param sapuxRequired if true, only find sapux projects * @param silent if true, then does not throw an error but returns an empty path + * @param memFs - optional mem-fs-editor instance * @returns {*} {Promise} - Project Root */ -export async function findProjectRoot(path: string, sapuxRequired = true, silent = false): Promise { - const packageJson = await findFileUp(FileName.Package, path); +export async function findProjectRoot( + path: string, + sapuxRequired = true, + silent = false, + memFs?: Editor +): Promise { + const packageJson = await findFileUp(FileName.Package, path, memFs); if (!packageJson) { if (silent) { return ''; @@ -93,9 +100,9 @@ export async function findProjectRoot(path: string, sapuxRequired = true, silent } let root = dirname(packageJson); if (sapuxRequired) { - const sapux = (await readJSON(packageJson)).sapux; + const sapux = (await readJSON(packageJson, memFs)).sapux; if (!sapux) { - root = await findProjectRoot(dirname(root), sapuxRequired, silent); + root = await findProjectRoot(dirname(root), sapuxRequired, silent, memFs); } } return root; @@ -168,27 +175,31 @@ export async function getAppRootFromWebappPath(webappPath: string): Promise/ui5-local.yaml. * * @param path - path to check, e.g. to the manifest.json + * @param memFs - optional mem-fs-editor instance * @returns - in case a supported app is found this function returns the appRoot and projectRoot path */ -export async function findRootsForPath(path: string): Promise<{ appRoot: string; projectRoot: string } | null> { +export async function findRootsForPath( + path: string, + memFs?: Editor +): Promise<{ appRoot: string; projectRoot: string } | null> { try { // Get the root of the app, that is where the package.json is, otherwise not supported - const appRoot = await findProjectRoot(path, false); + const appRoot = await findProjectRoot(path, false, false, memFs); if (!appRoot) { return null; } - const appPckJson = await readJSON(join(appRoot, FileName.Package)); + const appPckJson = await readJSON(join(appRoot, FileName.Package), memFs); // Check for most common app, Fiori elements with sapux=true in package.json if (appPckJson.sapux) { return findRootsWithSapux(appPckJson.sapux, path, appRoot); } - if ((await getCapProjectType(appRoot)) !== undefined) { + if ((await getCapProjectType(appRoot, memFs)) !== undefined) { // App is part of a CAP project, but doesn't have own package.json and is not mentioned in sapux array // in root -> not supported return null; } // Check if app is included in CAP project - const projectRoot = await findCapProjectRoot(appRoot); + const projectRoot = await findCapProjectRoot(appRoot, undefined, memFs); if (projectRoot) { // App included in CAP return { @@ -197,7 +208,7 @@ export async function findRootsForPath(path: string): Promise<{ appRoot: string; }; } else if ( // Check for freestyle non CAP - (await fileExists(join(appRoot, FileName.Ui5LocalYaml))) && + (await fileExists(join(appRoot, FileName.Ui5LocalYaml), memFs)) && hasDependency(appPckJson, '@sap/ux-ui5-tooling') ) { return { @@ -216,9 +227,14 @@ export async function findRootsForPath(path: string): Promise<{ appRoot: string; * * @param path - path inside CAP project * @param checkForAppRouter - if true, checks for app router in CAP project app folder + * @param memFs - optional mem-fs-editor instance * @returns - CAP project root path */ -export async function findCapProjectRoot(path: string, checkForAppRouter = true): Promise { +export async function findCapProjectRoot( + path: string, + checkForAppRouter = true, + memFs?: Editor +): Promise { try { if (!isAbsolute(path)) { return null; @@ -226,7 +242,7 @@ export async function findCapProjectRoot(path: string, checkForAppRouter = true) const { root } = parse(path); let projectRoot = dirname(path); while (projectRoot !== root) { - if (await getCapProjectType(projectRoot)) { + if (await getCapProjectType(projectRoot, memFs)) { // We have found a CAP project as root. Check if the found app is not directly in CAP's 'app/' folder. // Sometime there is a /app/package.json file that is used for app router (not an app) // or skip app router check if checkForAppRouter is false and return the project root. @@ -248,12 +264,14 @@ export async function findCapProjectRoot(path: string, checkForAppRouter = true) * findFioriArtifacts({ wsFolders, artifacts: ['applications'] }); from same module. * * @param wsFolders - list of roots, either as vscode WorkspaceFolder[] or array of paths + * @param memFs - optional mem-fs-editor instance * @returns - results as path to apps plus files already parsed, e.g. manifest.json */ export async function findAllApps( - wsFolders: readonly WorkspaceFolder[] | string[] | undefined + wsFolders: readonly WorkspaceFolder[] | string[] | undefined, + memFs?: Editor ): Promise { - const findResults = await findFioriArtifacts({ wsFolders, artifacts: ['applications'] }); + const findResults = await findFioriArtifacts({ wsFolders, artifacts: ['applications'], memFs }); return findResults.applications ?? []; } @@ -261,16 +279,17 @@ export async function findAllApps( * Filter Fiori apps from a list of files. * * @param pathMap - map of files. Key is the path, on first read parsed content will be set as value to prevent multiple reads of a file. + * @param memFs - optional mem-fs-editor instance * @returns - results as path to apps plus files already parsed, e.g. manifest.json */ -async function filterApplications(pathMap: FileMapAndCache): Promise { +async function filterApplications(pathMap: FileMapAndCache, memFs?: Editor): Promise { const filterApplicationByManifest = async (manifestPath: string) => { - pathMap[manifestPath] ??= await readJSON(manifestPath); + pathMap[manifestPath] ??= await readJSON(manifestPath, memFs); const manifest: Manifest = pathMap[manifestPath] as Manifest; // cast needed as pathMap also allows strings and any other objects // cast allowed, as this is the only place pathMap is filled for manifests if (manifest['sap.app'].id && manifest['sap.app'].type === 'application') { - const roots = await findRootsForPath(dirname(manifestPath)); - if (roots && !(await fileExists(join(roots.appRoot, '.adp', FileName.AdaptationConfig)))) { + const roots = await findRootsForPath(dirname(manifestPath), memFs); + if (roots && !(await fileExists(join(roots.appRoot, '.adp', FileName.AdaptationConfig), memFs))) { return { appRoot: roots.appRoot, projectRoot: roots.projectRoot, manifest: manifest, manifestPath }; } } @@ -291,15 +310,16 @@ async function filterApplications(pathMap: FileMapAndCache): Promise { +async function filterAdaptations(pathMap: FileMapAndCache, memFs?: Editor): Promise { const results: AdaptationResults[] = []; const manifestAppDescrVars = Object.keys(pathMap).filter((path) => path.endsWith(FileName.ManifestAppDescrVar)); for (const manifestAppDescrVar of manifestAppDescrVars) { - const packageJsonPath = await findFileUp(FileName.Package, dirname(manifestAppDescrVar)); + const packageJsonPath = await findFileUp(FileName.Package, dirname(manifestAppDescrVar), memFs); const projectRoot = packageJsonPath ? dirname(packageJsonPath) : null; - if (projectRoot && (await fileExists(join(projectRoot, 'webapp', FileName.ManifestAppDescrVar)))) { + if (projectRoot && (await fileExists(join(projectRoot, 'webapp', FileName.ManifestAppDescrVar), memFs))) { results.push({ appRoot: projectRoot, manifestAppdescrVariantPath: manifestAppDescrVar }); } } @@ -310,9 +330,10 @@ async function filterAdaptations(pathMap: FileMapAndCache): Promise { +async function filterExtensions(pathMap: FileMapAndCache, memFs?: Editor): Promise { const results: ExtensionResults[] = []; const extensionConfigs = Object.keys(pathMap).filter((path) => basename(path) === FileName.ExtConfigJson); for (const extensionConfig of extensionConfigs) { @@ -322,17 +343,18 @@ async function filterExtensions(pathMap: FileMapAndCache): Promise path.startsWith(dirname(extensionConfig) + sep) && basename(path) === FileName.Manifest ); if (manifestPath) { - pathMap[manifestPath] ??= await readJSON(manifestPath); + pathMap[manifestPath] ??= await readJSON(manifestPath, memFs); manifest = pathMap[manifestPath] as Manifest; } else { const manifests = await findBy({ fileNames: [FileName.Manifest], root: dirname(extensionConfig), - excludeFolders + excludeFolders, + memFs }); if (manifests.length === 1) { [manifestPath] = manifests; - manifest = await readJSON(manifestPath); + manifest = await readJSON(manifestPath, memFs); } } if (manifestPath && manifest) { @@ -350,9 +372,14 @@ async function filterExtensions(pathMap: FileMapAndCache): Promise { +async function filterDotLibraries( + pathMap: FileMapAndCache, + manifestPaths: string[], + memFs?: Editor +): Promise { const dotLibraries: LibraryResults[] = []; const dotLibraryPaths = Object.keys(pathMap) .filter((path) => basename(path) === FileName.Library) @@ -360,7 +387,9 @@ async function filterDotLibraries(pathMap: FileMapAndCache, manifestPaths: strin .filter((path) => !manifestPaths.map((manifestPath) => dirname(manifestPath)).includes(path)); if (dotLibraryPaths) { for (const libraryPath of dotLibraryPaths) { - const projectRoot = dirname((await findFileUp(FileName.Package, dirname(libraryPath))) ?? libraryPath); + const projectRoot = dirname( + (await findFileUp(FileName.Package, dirname(libraryPath), memFs)) ?? libraryPath + ); dotLibraries.push({ projectRoot, libraryPath }); } } @@ -371,20 +400,21 @@ async function filterDotLibraries(pathMap: FileMapAndCache, manifestPaths: strin * Filter extensions projects from a list of files. * * @param pathMap - path to files + * @param memFs - optional mem-fs-editor instance * @returns - results as array of found library projects. */ -async function filterLibraries(pathMap: FileMapAndCache): Promise { +async function filterLibraries(pathMap: FileMapAndCache, memFs?: Editor): Promise { const results: LibraryResults[] = []; const manifestPaths = Object.keys(pathMap).filter((path) => basename(path) === FileName.Manifest); - results.push(...(await filterDotLibraries(pathMap, manifestPaths))); + results.push(...(await filterDotLibraries(pathMap, manifestPaths, memFs))); for (const manifestPath of manifestPaths) { try { - pathMap[manifestPath] ??= await readJSON(manifestPath); + pathMap[manifestPath] ??= await readJSON(manifestPath, memFs); const manifest = pathMap[manifestPath] as Manifest; if (manifest['sap.app'] && manifest['sap.app'].type === 'library') { - const packageJsonPath = await findFileUp(FileName.Package, dirname(manifestPath)); + const packageJsonPath = await findFileUp(FileName.Package, dirname(manifestPath), memFs); const projectRoot = packageJsonPath ? dirname(packageJsonPath) : null; - if (projectRoot && (await fileExists(join(projectRoot, FileName.Ui5Yaml)))) { + if (projectRoot && (await fileExists(join(projectRoot, FileName.Ui5Yaml), memFs))) { results.push({ projectRoot, manifestPath, manifest }); } } @@ -416,11 +446,13 @@ function getFilterFileNames(artifacts: FioriArtifactTypes[]): string[] { * @param options - find options * @param options.wsFolders - list of roots, either as vscode WorkspaceFolder[] or array of paths * @param options.artifacts - list of artifacts to search for: 'application', 'adaptation', 'extension' see FioriArtifactTypes + * @param options.memFs * @returns - data structure containing the search results, for app e.g. as path to app plus files already parsed, e.g. manifest.json */ export async function findFioriArtifacts(options: { wsFolders?: readonly WorkspaceFolder[] | string[]; artifacts: FioriArtifactTypes[]; + memFs?: Editor; }): Promise { const results: FoundFioriArtifacts = {}; const fileNames: string[] = getFilterFileNames(options.artifacts); @@ -431,7 +463,8 @@ export async function findFioriArtifacts(options: { const foundFiles = await findBy({ fileNames, root, - excludeFolders + excludeFolders, + memFs: options.memFs }); foundFiles.forEach((path) => (pathMap[path] = null)); } catch { @@ -439,16 +472,16 @@ export async function findFioriArtifacts(options: { } } if (options.artifacts.includes('applications')) { - results.applications = await filterApplications(pathMap); + results.applications = await filterApplications(pathMap, options.memFs); } if (options.artifacts.includes('adaptations')) { - results.adaptations = await filterAdaptations(pathMap); + results.adaptations = await filterAdaptations(pathMap, options.memFs); } if (options.artifacts.includes('extensions')) { - results.extensions = await filterExtensions(pathMap); + results.extensions = await filterExtensions(pathMap, options.memFs); } if (options.artifacts.includes('libraries')) { - results.libraries = await filterLibraries(pathMap); + results.libraries = await filterLibraries(pathMap, options.memFs); } return results; } diff --git a/packages/project-access/src/project/service.ts b/packages/project-access/src/project/service.ts index 97b356c13f0..29f5b1b75f3 100644 --- a/packages/project-access/src/project/service.ts +++ b/packages/project-access/src/project/service.ts @@ -1,6 +1,7 @@ import { dirname, join } from 'path'; import type { Manifest, ManifestNamespace, ServiceSpecification } from '../types'; import { readJSON } from '../file'; +import type { Editor } from 'mem-fs-editor'; /** * Get the main service name from the manifest. @@ -23,13 +24,15 @@ export function getMainService(manifest: Manifest): string | undefined { * * @param manifestPath - path to manifest.json * @param manifest - optionally, parsed content of manifest.json, pass to avoid reading it again. + * @param memFs - optional mem-fs-editor instance * @returns - service and annotation specification */ export async function getServicesAndAnnotations( manifestPath: string, - manifest: Manifest + manifest: Manifest, + memFs?: Editor ): Promise<{ [index: string]: ServiceSpecification }> { - const parsedManifest = manifest ?? (await readJSON(manifestPath)); + const parsedManifest = manifest ?? (await readJSON(manifestPath, memFs)); const manifestFolder = dirname(manifestPath); const services: { [index: string]: ServiceSpecification } = {}; diff --git a/packages/project-access/src/types/access/index.ts b/packages/project-access/src/types/access/index.ts index 4ce8bddfa5b..1c5d600ccf4 100644 --- a/packages/project-access/src/types/access/index.ts +++ b/packages/project-access/src/types/access/index.ts @@ -122,6 +122,7 @@ export interface ApplicationAccess extends BaseAccess { export interface ProjectAccessOptions { logger?: Logger; + memFs?: Editor; } export interface ProjectAccess extends BaseAccess { diff --git a/packages/project-access/test/project/access.test.ts b/packages/project-access/test/project/access.test.ts index 8c42a033245..552f278cb8d 100644 --- a/packages/project-access/test/project/access.test.ts +++ b/packages/project-access/test/project/access.test.ts @@ -223,7 +223,7 @@ describe('Test function createApplicationAccess()', () => { const createCapI18nEntriesMock = jest.spyOn(i18nMock, 'createCapI18nEntries').mockResolvedValue(true); const createUI5I18nEntriesMock = jest.spyOn(i18nMock, 'createUI5I18nEntries').mockResolvedValue(true); const projectRoot = join(sampleRoot, 'cap-project'); - const appRoot = join(projectRoot, 'apps/one'); + const appRoot = join(projectRoot, 'apps', 'one'); const appAccess = await createApplicationAccess(appRoot, memFs); // Test execution diff --git a/packages/project-access/test/project/cap.test.ts b/packages/project-access/test/project/cap.test.ts index b2dd0b9dbe2..2abc29c92d7 100644 --- a/packages/project-access/test/project/cap.test.ts +++ b/packages/project-access/test/project/cap.test.ts @@ -43,12 +43,25 @@ const jestMockEnv = { }; describe('Test getCapProjectType() & isCapProject()', () => { + let memFs: Editor; + + beforeAll(() => { + const store = createStorage(); + memFs = create(store); + }); + test('Test if valid CAP Node.js project is recognized', async () => { const capPath = join(__dirname, '..', 'test-data', 'project', 'find-all-apps', 'CAP', 'CAPnode_mix'); expect(await getCapProjectType(capPath)).toBe('CAPNodejs'); expect(await isCapProject(capPath)).toBe(true); }); + test('Test if valid CAP Node.js project is recognized using mem-fs', async () => { + const capPath = join(__dirname, '..', 'test-data', 'project', 'find-all-apps', 'CAP', 'CAPnode_mix'); + expect(await getCapProjectType(capPath, memFs)).toBe('CAPNodejs'); + expect(await isCapProject(capPath)).toBe(true); + }); + test('Test if valid CAP Java project is recognized', async () => { const capPath = join(__dirname, '..', 'test-data', 'project', 'find-all-apps', 'CAP', 'CAPJava_mix'); expect(await getCapProjectType(capPath)).toBe('CAPJava'); @@ -59,6 +72,16 @@ describe('Test getCapProjectType() & isCapProject()', () => { expect(await getCapProjectType('INVALID_PROJECT')).toBeUndefined(); expect(await isCapProject('INVALID_PROJECT')).toBe(false); }); + + test('Test if undefined is retuned for empty project', async () => { + const capPath = join(__dirname, '..', 'test-data', 'project', 'info', 'empty-project'); + expect(await getCapProjectType(capPath)).toBe(undefined); + }); + + test('Test if undefined is retuned for empty project using mem-fs', async () => { + const capPath = join(__dirname, '..', 'test-data', 'project', 'info', 'empty-project'); + expect(await getCapProjectType(capPath, memFs)).toBe(undefined); + }); }); describe('Test isCapNodeJsProject()', () => {