@sap-ux/project-access
Version:
Library to access SAP Fiori tools projects
538 lines • 23.3 kB
JavaScript
;
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