UNPKG

@sap-ux/project-access

Version:

Library to access SAP Fiori tools projects

538 lines 23.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.findProjectRoot = findProjectRoot; exports.getAppRootFromWebappPath = getAppRootFromWebappPath; exports.findRootsForPath = findRootsForPath; exports.findCapProjectRoot = findCapProjectRoot; exports.findAllApps = findAllApps; exports.findFioriArtifacts = findFioriArtifacts; exports.findCapProjects = findCapProjects; const path_1 = require("path"); const constants_1 = require("../constants"); const file_1 = require("../file"); const dependencies_1 = require("./dependencies"); const cap_1 = require("./cap"); const ui5_config_1 = require("./ui5-config"); /** * Map artifact to file that is specific to the artifact type. Some artifacts can * be identified by the same file, like app and library have file 'manifest.json'. * Further filtering for specific artifact types happens in the filter{Artifact} * functions. */ const filterFileMap = { applications: [constants_1.FileName.Manifest], adaptations: [constants_1.FileName.ManifestAppDescrVar], components: [constants_1.FileName.Manifest], extensions: [constants_1.FileName.ExtConfigJson], libraries: [constants_1.FileName.Library, constants_1.FileName.Manifest] }; /** * Default folders to exclude from search. */ const excludeFolders = ['.git', 'node_modules', 'dist']; /** * WorkspaceFolder type guard. * * @param value - value to type check * @returns - true: is a vscode workspace array; no: not a vscode workspace array */ function isWorkspaceFolder(value) { return value && value.length > 0 && value[0].uri !== undefined; } /** * Convert workspace root folders to root paths. * * @param wsFolders - list of roots, either as vscode WorkspaceFolder[] or array of paths * @returns - root paths */ function wsFoldersToRootPaths(wsFolders) { // extract root path if provided as VSCode folder let wsRoots; if (wsFolders && isWorkspaceFolder(wsFolders)) { wsRoots = []; wsFolders .filter((each) => each.uri.scheme === 'file') .forEach((folder) => { wsRoots.push(folder.uri.fsPath); }); } else { wsRoots = (wsFolders ?? []); } return wsRoots; } /** * Find root folder of the project containing the given file. * * @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<string>} - Project Root */ async function findProjectRoot(path, sapuxRequired = true, silent = false, memFs) { const packageJson = await (0, file_1.findFileUp)(constants_1.FileName.Package, path, memFs); if (!packageJson) { if (silent) { return ''; } throw new Error(`Could not find any project root for '${path}'. Search was done for ${sapuxRequired ? 'Fiori elements' : 'All'} projects.`); } let root = (0, path_1.dirname)(packageJson); if (sapuxRequired) { const sapux = (await (0, file_1.readJSON)(packageJson, memFs)).sapux; if (!sapux) { root = await findProjectRoot((0, path_1.dirname)(root), sapuxRequired, silent, memFs); } } return root; } /** * Find app root and project root from given paths and sapux entry. * * @param sapux - value of sapux in package.json, either boolean or string array * @param path - path where the search started from * @param root - root of the app or project, where package.json is located * @returns - appRoot and projectRoot or null */ function findRootsWithSapux(sapux, path, root) { if (typeof sapux === 'boolean' && sapux === true) { return { appRoot: root, projectRoot: root }; } else if (Array.isArray(sapux)) { // Backward compatibility for FE apps in CAP projects that have no app package.json, // but are listed in CAP root sapux array const pathWithSep = path.endsWith(path_1.sep) ? path : path + path_1.sep; const relAppPaths = sapux.map((a) => (0, path_1.join)(...a.split(/[\\/]/))); const relApp = relAppPaths.find((app) => pathWithSep.startsWith((0, path_1.join)(root, app) + path_1.sep)); if (relApp) { return { appRoot: (0, path_1.join)(root, relApp), projectRoot: root }; } } // The first package.json we found when searching up contains sapux, but not true -> not supported return null; } /** * Get the application root for a given webapp path. * * @param webappPath - path to webapp folder, where manifest.json is * @returns - root path of the application, where usually ui5.yaml and package.json are */ async function getAppRootFromWebappPath(webappPath) { const ui5YamlPath = await (0, file_1.findFileUp)(constants_1.FileName.Ui5Yaml, webappPath); let appRoot = (0, path_1.dirname)(webappPath); if (ui5YamlPath) { const candidate = (0, path_1.dirname)(ui5YamlPath); const webapp = await (0, ui5_config_1.getWebappPath)(candidate); if (webapp === webappPath) { appRoot = candidate; } } return appRoot; } /** * Type guard for Editor or Editor and FileMapAndCache. * * @param argument - argument to check * @returns true if argument is Editor, false if it is FileMapAndCache */ function isEditor(argument) { return argument.commit !== undefined; } /** * Internal helper function to resolve options for findRootsForPath() and findCapProjectRoot(). * * @param options - optional mem-fs-editor instance or options object with optional memFs and cache * @returns memFs and cache */ function getFindOptions(options) { let memFs; let cache = { files: {}, capProjectType: new Map() }; if (options) { // Check if options is Editor or { memFs?: Editor; cache?: FileMapAndCache } if (isEditor(options)) { memFs = options; } else { memFs = options.memFs; cache = options.cache ?? cache; } } return { memFs, cache }; } /** * Find the app root and project root folder for a given path. In case of apps in non CAP projects they are the same. * This function also validates if an app is supported by tools considering Fiori elements apps and SAPUI5 * freestyle apps. Only if project root and app root can be determined, they are returned, otherwise null is returned. * This function is used e.g. to get a filtered list of all manifest.json files in a workspace for tools * supported apps and retrieve the respective root paths. * * This function makes following assumptions: * - All applications have a package.json in root folder. * - If sapux=true in package.json the app is NOT inside a CAP project. * - Freestyle application (non CAP) has in package.json dependency to @sap/ux-ui5-tooling and <appRoot>/ui5-local.yaml. * * @param path - path to check, e.g. to the manifest.json * @param options - optional mem-fs-editor instance or options object with optional memFs and cache * @returns - in case a supported app is found this function returns the appRoot and projectRoot path */ async function findRootsForPath(path, options) { try { const { memFs, cache } = getFindOptions(options); // Get the root of the app, that is where the package.json is, otherwise not supported const appRoot = await findProjectRoot(path, false, false, memFs); if (!appRoot) { return null; } cache.files[path] ??= await (0, file_1.readJSON)((0, path_1.join)(appRoot, constants_1.FileName.Package), memFs); const appPckJson = cache.files[path]; // Check for most common app, Fiori elements with sapux=true in package.json if (appPckJson.sapux) { return findRootsWithSapux(appPckJson.sapux, path, appRoot); } // Check if app is included in CAP project const projectRoot = await findCapProjectRoot(appRoot, undefined, { memFs, cache }); if (projectRoot) { // App included in CAP return { appRoot, projectRoot }; } else if ( // Check for freestyle non CAP (await (0, file_1.fileExists)((0, path_1.join)(appRoot, constants_1.FileName.Ui5LocalYaml), memFs)) && (0, dependencies_1.hasDependency)(appPckJson, '@sap/ux-ui5-tooling')) { return { appRoot, projectRoot: appRoot }; } } catch { // Finding root should not throw error. Return null instead. } return null; } /** * Find CAP project root path. * * @param path - path inside CAP project * @param checkForAppRouter - if true, checks for app router in CAP project app folder * @param options - optional mem-fs-editor instance or options object with optional memFs and cache * @returns - CAP project root path */ async function findCapProjectRoot(path, checkForAppRouter = true, options) { try { if (!(0, path_1.isAbsolute)(path)) { return null; } const { memFs, cache } = getFindOptions(options); const { root } = (0, path_1.parse)(path); let projectRoot = (0, path_1.dirname)(path); while (projectRoot !== root) { if (!cache.capProjectType.has(projectRoot)) { cache.capProjectType.set(projectRoot, await (0, cap_1.getCapProjectType)(projectRoot, memFs)); } const capProjectType = cache.capProjectType.get(projectRoot); if (capProjectType) { // 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 <CAP_ROOT>/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. if ((checkForAppRouter && (0, path_1.join)(projectRoot, 'app') !== path) || !checkForAppRouter) { return projectRoot; } } projectRoot = (0, path_1.dirname)(projectRoot); } } catch { // No project root can be found at parent folder. } return null; } /** * Find all app that are supported by Fiori tools for a given list of roots (workspace folders). * This is a convenient function to retrieve all apps. Same result can be achieved with call * 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 */ async function findAllApps(wsFolders, memFs) { const findResults = await findFioriArtifacts({ wsFolders, artifacts: ['applications'], memFs }); return findResults.applications ?? []; } /** * 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, memFs) { const result = []; const manifestPaths = Object.keys(pathMap.files).filter((path) => (0, path_1.basename)(path) === constants_1.FileName.Manifest); for (const manifestPath of manifestPaths) { try { // All UI5 apps have at least sap.app: { id: <ID>, type: "application" } in manifest.json pathMap.files[manifestPath] ??= await (0, file_1.readJSON)(manifestPath, memFs); const manifest = pathMap.files[manifestPath]; if (!manifest['sap.app']?.id || manifest['sap.app'].type !== 'application') { continue; } const roots = await findRootsForPath((0, path_1.dirname)(manifestPath), { memFs, cache: pathMap }); if (roots && !(await (0, file_1.fileExists)((0, path_1.join)(roots.appRoot, '.adp', constants_1.FileName.AdaptationConfig), memFs))) { result.push({ appRoot: roots.appRoot, projectRoot: roots.projectRoot, manifest, manifestPath }); } } catch { // ignore exceptions for invalid manifests } } return result; } /** * Filter adaptation projects 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 array of found adaptation projects. */ async function filterAdaptations(pathMap, memFs) { const results = []; const manifestAppDescrVars = Object.keys(pathMap.files).filter((path) => path.endsWith(constants_1.FileName.ManifestAppDescrVar)); for (const manifestAppDescrVar of manifestAppDescrVars) { const packageJsonPath = await (0, file_1.findFileUp)(constants_1.FileName.Package, (0, path_1.dirname)(manifestAppDescrVar), memFs); const projectRoot = packageJsonPath ? (0, path_1.dirname)(packageJsonPath) : null; if (projectRoot && (await (0, file_1.fileExists)((0, path_1.join)(projectRoot, 'webapp', constants_1.FileName.ManifestAppDescrVar), memFs))) { results.push({ appRoot: projectRoot, manifestAppdescrVariantPath: manifestAppDescrVar }); } } return results; } /** * Filter extensions projects 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 array of found extension projects. */ async function filterExtensions(pathMap, memFs) { const results = []; const extensionConfigs = Object.keys(pathMap.files).filter((path) => (0, path_1.basename)(path) === constants_1.FileName.ExtConfigJson); for (const extensionConfig of extensionConfigs) { try { let manifest = null; let manifestPath = Object.keys(pathMap).find((path) => path.startsWith((0, path_1.dirname)(extensionConfig) + path_1.sep) && (0, path_1.basename)(path) === constants_1.FileName.Manifest); if (manifestPath) { pathMap.files[manifestPath] ??= await (0, file_1.readJSON)(manifestPath, memFs); manifest = pathMap.files[manifestPath]; } else { const manifests = await (0, file_1.findBy)({ fileNames: [constants_1.FileName.Manifest], root: (0, path_1.dirname)(extensionConfig), excludeFolders, memFs }); if (manifests.length === 1) { [manifestPath] = manifests; manifest = await (0, file_1.readJSON)(manifestPath, memFs); } } if (manifestPath && manifest) { results.push({ appRoot: (0, path_1.dirname)(extensionConfig), manifest, manifestPath }); } } catch { // ignore exceptions for invalid manifests } } return results; } /** * Find and filter libraries with only a `.library` and no `manifest.json`. * * @param pathMap - path to files * @param manifestPaths - paths to manifest.json files * @param memFs - optional mem-fs-editor instance * @returns - results as array of found .library projects. */ async function filterDotLibraries(pathMap, manifestPaths, memFs) { const dotLibraries = []; const dotLibraryPaths = Object.keys(pathMap.files) .filter((path) => (0, path_1.basename)(path) === constants_1.FileName.Library) .map((path) => (0, path_1.dirname)(path)) .filter((path) => !manifestPaths.map((manifestPath) => (0, path_1.dirname)(manifestPath)).includes(path)); if (dotLibraryPaths) { for (const libraryPath of dotLibraryPaths) { const projectRoot = (0, path_1.dirname)((await (0, file_1.findFileUp)(constants_1.FileName.Package, (0, path_1.dirname)(libraryPath), memFs)) ?? libraryPath); dotLibraries.push({ projectRoot, libraryPath }); } } return dotLibraries; } /** * Filter libraries 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, memFs) { const results = []; const manifestPaths = Object.keys(pathMap.files).filter((path) => (0, path_1.basename)(path) === constants_1.FileName.Manifest); results.push(...(await filterDotLibraries(pathMap, manifestPaths, memFs))); for (const manifestPath of manifestPaths) { try { pathMap.files[manifestPath] ??= await (0, file_1.readJSON)(manifestPath, memFs); const manifest = pathMap.files[manifestPath]; if (manifest['sap.app'] && manifest['sap.app'].type === 'library') { const packageJsonPath = await (0, file_1.findFileUp)(constants_1.FileName.Package, (0, path_1.dirname)(manifestPath), memFs); const projectRoot = packageJsonPath ? (0, path_1.dirname)(packageJsonPath) : null; if (projectRoot && (await (0, file_1.fileExists)((0, path_1.join)(projectRoot, constants_1.FileName.Ui5Yaml), memFs))) { results.push({ projectRoot, manifestPath, manifest }); } } } catch { // ignore exceptions for invalid manifests } } return results; } /** * Filter components from a list of files. * * @param pathMap - path to files * @param memFs - optional mem-fs-editor instance * @returns - results as array of found components. */ async function filterComponents(pathMap, memFs) { const results = []; const manifestPaths = Object.keys(pathMap.files).filter((path) => (0, path_1.basename)(path) === constants_1.FileName.Manifest); for (const manifestPath of manifestPaths) { try { pathMap.files[manifestPath] ??= await (0, file_1.readJSON)(manifestPath, memFs); const manifest = pathMap.files[manifestPath]; if (manifest['sap.app'] && manifest['sap.app'].type === 'component') { const packageJsonPath = await (0, file_1.findFileUp)(constants_1.FileName.Package, (0, path_1.dirname)(manifestPath), memFs); const projectRoot = packageJsonPath ? (0, path_1.dirname)(packageJsonPath) : null; if (projectRoot) { results.push({ projectRoot, manifestPath, manifest }); } } } catch { // ignore exceptions for invalid components } } return results; } /** * Get the files to search for according to requested artifact type. * * @param artifacts - requests artifacts like apps, adaptations, extensions * @returns - array of filenames to search for */ function getFilterFileNames(artifacts) { const uniqueFilterFiles = new Set(); for (const artifact of artifacts) { if (filterFileMap[artifact]) { filterFileMap[artifact].forEach((artifactFile) => uniqueFilterFiles.add(artifactFile)); } } return Array.from(uniqueFilterFiles); } /** * Find all requested Fiori artifacts like apps, adaptations, extensions, that are supported by Fiori tools, for a given list of roots (workspace folders). * * @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 - optional mem-fs-editor instance * @returns - data structure containing the search results, for app e.g. as path to app plus files already parsed, e.g. manifest.json */ async function findFioriArtifacts(options) { const results = {}; const fileNames = getFilterFileNames(options.artifacts); const wsRoots = wsFoldersToRootPaths(options.wsFolders); const pathMap = { files: {}, capProjectType: new Map() }; for (const root of wsRoots) { try { const foundFiles = await (0, file_1.findBy)({ fileNames, root, excludeFolders, memFs: options.memFs }); foundFiles.forEach((path) => (pathMap.files[path] = null)); } catch { // ignore exceptions during find } } if (options.artifacts.includes('applications')) { results.applications = await filterApplications(pathMap, options.memFs); } if (options.artifacts.includes('adaptations')) { results.adaptations = await filterAdaptations(pathMap, options.memFs); } if (options.artifacts.includes('extensions')) { results.extensions = await filterExtensions(pathMap, options.memFs); } if (options.artifacts.includes('libraries')) { results.libraries = await filterLibraries(pathMap, options.memFs); } if (options.artifacts.includes('components')) { results.components = await filterComponents(pathMap, options.memFs); } return results; } /** * Find all CAP project roots by locating pom.xml or package.json in a given workspace. * * @param options - find options * @param options.wsFolders - list of roots, either as vscode WorkspaceFolder[] or array of paths * @returns - root file paths that may contain a CAP project */ async function findCapProjects(options) { const result = new Set(); const excludeFolders = ['node_modules', 'dist', 'webapp', 'MDKModule', 'gen']; const fileNames = [constants_1.FileName.Pom, constants_1.FileName.Package, constants_1.FileName.CapJavaApplicationYaml]; const wsRoots = wsFoldersToRootPaths(options.wsFolders); for (const root of wsRoots) { const filesToCheck = await (0, file_1.findBy)({ fileNames, root, excludeFolders }); const appYamlsToCheck = Array.from(new Set(filesToCheck .filter((file) => (0, path_1.basename)(file) === constants_1.FileName.CapJavaApplicationYaml) .map((file) => (0, path_1.dirname)(file)))); const foldersToCheck = Array.from(new Set(filesToCheck .filter((file) => (0, path_1.basename)(file) !== constants_1.FileName.CapJavaApplicationYaml) .map((file) => (0, path_1.dirname)(file)))); for (const appYamlToCheck of appYamlsToCheck) { const capRoot = await findCapProjectRoot(appYamlToCheck); if (capRoot) { result.add(capRoot); } } for (const folderToCheck of foldersToCheck) { if ((await (0, cap_1.getCapProjectType)(folderToCheck)) !== undefined) { result.add(folderToCheck); } } } return Array.from(result); } //# sourceMappingURL=search.js.map