UNPKG

@sap-ux/project-access

Version:

Library to access SAP Fiori tools projects

857 lines 35.3 kB
import { spawn } from 'node:child_process'; import { basename, dirname, join, normalize, relative, sep } from 'node:path'; import { FileName, MinCdsVersion } from '../constants.js'; import { deleteDirectory, deleteFile, fileExists, findBy, readDirectory, readFile, readJSON, updatePackageJSON, writeFile } from '../file/index.js'; import { loadModuleFromProject } from './module-loader.js'; import { findCapProjectRoot } from './search.js'; import { coerce, gte, satisfies } from 'semver'; import { create as createStorage } from 'mem-fs'; import { create } from 'mem-fs-editor'; import { hasDependency } from './dependencies.js'; /** * Returns true if the project is a CAP Node.js project. * * @param packageJson - the parsed package.json object * @returns - true if the project is a CAP Node.js project */ export function isCapNodeJsProject(packageJson) { return !!(packageJson.cds ?? packageJson.dependencies?.['@sap/cds']); } /** * Returns true if the project is a CAP Java project. * * @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, capCustomPaths, memFs) { const srv = capCustomPaths?.srv ?? (await getCapCustomPaths(projectRoot)).srv; 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, memFs) { try { return (await findBy({ root: srvFolderPath, memFs })).length > 0; } catch (error) { return false; } } /** * 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, memFs) { const capCustomPaths = await getCapCustomPaths(projectRoot); if (!(await checkFilesInSrvFolder(join(projectRoot, capCustomPaths.srv), memFs))) { return undefined; } if (await isCapJavaProject(projectRoot, capCustomPaths, memFs)) { return 'CAPJava'; } let packageJson; try { packageJson = await readJSON(join(projectRoot, FileName.Package), memFs); } catch { // Ignore errors while reading the package.json file } if (packageJson && isCapNodeJsProject(packageJson)) { return 'CAPNodejs'; } return undefined; } /** * Returns true if the project is either a CAP Node.js or a CAP Java project. * * @param projectRoot - the root path of the project * @returns - true if the project is a CAP project */ export async function isCapProject(projectRoot) { return !!(await getCapProjectType(projectRoot)); } /** * Get CAP CDS project custom paths for project root. * * @param capProjectPath - project root of cap project * @returns - paths to app, db, and srv for CAP project */ export async function getCapCustomPaths(capProjectPath) { const result = { app: 'app/', db: 'db/', srv: 'srv/' }; try { const cdsCustomPaths = await getCapEnvironment(capProjectPath); if (cdsCustomPaths.folders) { result.app = cdsCustomPaths.folders.app; result.db = cdsCustomPaths.folders.db; result.srv = cdsCustomPaths.folders.srv; } } catch (error) { // In case of issues, fall back to the defaults } return result; } /** * Filters service endpoints to include only OData endpoints. * * @param endpoint The endpoint object to check. * @param endpoint.kind The type of the endpoint. * @returns `true` if the endpoint is of kind 'odata' or 'odata-v4'. */ function filterCapServiceEndpoints(endpoint) { return endpoint.kind === 'odata' || endpoint.kind === 'odata-v4'; } /** * Return the CAP model and all services. The cds.root will be set to the provided project root path. * * @param projectRoot - CAP project root where package.json resides or object specifying project root and optional logger to log additional info * @returns {Promise<{ model: csn; services: ServiceInfo[]; cdsVersionInfo: CdsVersionInfo }>} - CAP Model and Services */ export async function getCapModelAndServices(projectRoot) { let _projectRoot; let _logger; let _pathSelection; const defaultPathSelection = new Set(['app', 'srv', 'db']); if (typeof projectRoot === 'object') { _projectRoot = projectRoot.projectRoot; _logger = projectRoot.logger; _pathSelection = projectRoot.pathSelection ? projectRoot.pathSelection : defaultPathSelection; } else { _pathSelection = defaultPathSelection; _projectRoot = projectRoot; } const cds = await loadCdsModuleFromProject(_projectRoot, true); const capProjectPaths = await getCapCustomPaths(_projectRoot); const modelPaths = []; _pathSelection?.forEach((path) => { modelPaths.push(join(_projectRoot, capProjectPaths[path])); }); const model = await cds.load(modelPaths, { root: _projectRoot }); _logger?.info(`@sap-ux/project-access:getCapModelAndServices - Using 'cds.home': ${cds.home}`); _logger?.info(`@sap-ux/project-access:getCapModelAndServices - Using 'cds.version': ${cds.version}`); _logger?.info(`@sap-ux/project-access:getCapModelAndServices - Using 'cds.root': ${cds.root}`); _logger?.info(`@sap-ux/project-access:getCapModelAndServices - Using 'projectRoot': ${_projectRoot}`); let services = cds.compile.to.serviceinfo(model, { root: _projectRoot }) ?? []; services = processServices(services); return { model, services, cdsVersionInfo: { home: cds.home, version: cds.version, root: cds.root } }; } /** * Filter and normalize service definitions from CAP project. * * @param services - list of services from cds.compile.to['serviceinfo'](model) * @returns list of normalized service info */ export function processServices(services) { // filter services that have ( urlPath defined AND no endpoints) OR have endpoints with kind 'odata' // i.e. ignore services for websockets and other unsupported protocols if (services && Array.isArray(services)) { return services .filter((service) => (service.urlPath && service.endpoints === undefined) || service.endpoints?.find(filterCapServiceEndpoints)) .map((value) => { const { endpoints, urlPath } = value; const odataEndpoint = endpoints?.find(filterCapServiceEndpoints); const endpointPath = odataEndpoint?.path ?? urlPath; return { name: value.name, urlPath: uniformUrl(endpointPath), runtime: value.runtime }; }); } return []; } /** * Returns a list of cds file paths (layers). By default return list of all, but you can also restrict it to one envRoot. * * @param projectRoot - root of the project, where the package.json is * @param [ignoreErrors] - optionally, default is false; if set to true the thrown error will be checked for CDS file paths in model and returned * @param [envRoot] - optionally, the root folder or CDS file to get the layer files * @returns - array of strings containing cds file paths */ export async function getCdsFiles(projectRoot, ignoreErrors = false, envRoot) { let cdsFiles = []; try { let csn; envRoot ??= await getCdsRoots(projectRoot); try { const cds = await loadCdsModuleFromProject(projectRoot); csn = await cds.load(envRoot, { root: projectRoot }); cdsFiles = [...(csn['$sources'] ?? [])]; } catch (e) { if (ignoreErrors && e.model?.sources && typeof e.model.sources === 'object') { cdsFiles.push(...extractCdsFilesFromMessage(e.model.sources)); } else { throw e; } } } catch (error) { throw Error(`Error while retrieving the list of cds files for project ${projectRoot}, envRoot ${envRoot}. Error was: ${error}`); } return cdsFiles; } /** * Returns a list of filepaths to CDS files in root folders. Same what is done if you execute cds.resolve('*') on command line in a project. * * @param projectRoot - root of the project, where the package.json is * @param [clearCache] - optionally, clear the cache, default false * @returns - array of root paths */ export async function getCdsRoots(projectRoot, clearCache = false) { const roots = []; const capCustomPaths = await getCapCustomPaths(projectRoot); const cdsEnvRoots = [capCustomPaths.db, capCustomPaths.srv, capCustomPaths.app, 'schema', 'services']; // clear cache is enforced to also resolve newly created cds file at design time const cds = await loadCdsModuleFromProject(projectRoot); if (clearCache) { clearCdsResolveCache(cds); } for (const cdsEnvRoot of cdsEnvRoots) { const resolvedRoots = cds.resolve(join(projectRoot, cdsEnvRoot), { skipModelCache: true }) || []; for (const resolvedRoot of resolvedRoots) { roots.push(resolvedRoot); } } return roots; } /** * Return a list of services in a CAP project. * * @param projectRoot - root of the CAP project, where the package.json is * @param ignoreErrors - in case loading the cds model throws an error, try to use the model from the exception object * @returns - array of service definitions */ export async function getCdsServices(projectRoot, ignoreErrors = true) { let cdsServices = []; try { const cds = await loadCdsModuleFromProject(projectRoot); const roots = await getCdsRoots(projectRoot); let model; try { model = await cds.load(roots, { root: projectRoot }); } catch (e) { if (ignoreErrors && e.model) { model = e.model; } else { throw e; } } const linked = cds.linked(model); if (Array.isArray(linked.services)) { cdsServices = linked.services; } else { Object.keys(linked.services).forEach((service) => { cdsServices.push(linked.services[service]); }); } } catch (error) { throw Error(`Error while resolving cds roots for '${projectRoot}'. ${error}`); } return cdsServices; } /** * When an error occurs while trying to read cds files, the error object contains the source file * information. This function extracts this file paths. * * @param sources - map containing the file name * @returns - array of strings containing cds file paths */ function extractCdsFilesFromMessage(sources) { const cdsFiles = []; for (const source in sources) { let filename = sources[source].filename; if (typeof filename === 'string' && !filename.startsWith(sep)) { filename = join(sep, filename); } if (filename) { cdsFiles.push(filename); } } return cdsFiles; } /** * Remove rogue '\\' - cds windows if needed. * Replaces all backslashes with forward slashes, removes double slashes, and trailing slashes. * * @param url - url to uniform * @returns - uniform url */ function uniformUrl(url) { if (!url) { return ''; } return url .replace(/\\/g, '/') .replace(/\/\//g, '/') .replace(/(?:^\/)/g, ''); } /** * Return the EDMX string of a CAP service. * * @param root - CAP project root where package.json resides * @param uri - service path, e.g 'incident/' * @param version - optional OData version v2 or v4 * @returns - string containing the edmx */ export async function readCapServiceMetadataEdmx(root, uri, version = 'v4') { try { const { model, services } = await getCapModelAndServices(root); const service = findServiceByUri(services, uri); if (!service) { throw Error(`Service for uri: '${uri}' not found. Available services: ${JSON.stringify(services)}`); } const cds = await loadCdsModuleFromProject(root); const edmx = cds.compile.to.edmx(model, { service: service.name, version }); return edmx; } catch (error) { throw Error(`Error while reading CAP service metadata. Path: '${root}', service uri: '${uri}', error: '${error.toString()}'}`); } } /** * Normalizes a service URL path by removing a leading and/or trailing slash. * * @param urlPath - The URL path to normalize. * @returns The normalized path without leading or trailing slashes. */ function normalizeServiceUrlPath(urlPath) { return urlPath.replaceAll(/(?:^\/)|(?:\/$)/g, ''); } /** * Checks whether a given URL path matches one of the supported service prefix patterns * and ends with the expected service suffix path. * * Currently method validates against DwC service patterns, supported patterns: * - `/ui/<inbound-service-name>/v<version>/<suffix>` * - `/<string>.<string>/external-ui/<inbound-service-name>/v<version>/<suffix>` * * The `<suffix>` (e.g. `odata/v4/myService`) must match exactly. * * @param path - The full request path to validate. * @param expectedSuffixPath - The expected service path (e.g. `odata/v4/myService`). * @returns `true` if the path matches one of the supported patterns and ends with the expected suffix. */ export function isMatchingServiceUri(path, expectedSuffixPath) { const normalizedPath = path.startsWith('/') ? path : `/${path}`; // Escapes special regex characters in a string so it can be embedded into regular expression const escapedSuffix = expectedSuffixPath.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`); const patterns = [ // regex for pattern -> /ui/<inbound-service-name>/v<version>/... String.raw `^/ui/[^/]+/v\d+/${escapedSuffix}$`, // regex for pattern -> /<string>.<string>/external-ui/<inbound-service-name>/v<version>/... String.raw `^/[^/]+\.[^/]+/external-ui/[^/]+/v\d+/${escapedSuffix}$` ]; return patterns.some((pattern) => new RegExp(pattern).test(normalizedPath)); } /** * Find a service in a list of services ignoring leading and trailing slashes. * * @param services - list of services from cds.compile.to['serviceinfo'](model) * @param uri - search uri (usually from data source in manifest.json) * @returns - name and uri of the service, undefined if service not found */ function findServiceByUri(services, uri) { const searchUri = normalizeServiceUrlPath(uniformUrl(uri)); // Try to find a service by exact path match let service = services.find((srv) => normalizeServiceUrlPath(srv.urlPath) === searchUri); // If no exact match is found, try matching while ignoring the service prefix service ??= services.find((srv) => { const normalizedServiceUrlPath = normalizeServiceUrlPath(srv.urlPath); return isMatchingServiceUri(searchUri, normalizedServiceUrlPath); }); return service; } /** * Get CAP CDS project environment config for project root. * * @param capProjectPath - project root of a CAP project * @returns - environment config for a CAP project */ export async function getCapEnvironment(capProjectPath) { const cds = await loadCdsModuleFromProject(capProjectPath); return cds.env.for('cds', capProjectPath); } /** * Load CAP CDS module. First attempt loads @sap/cds for a project based on its root. * Second attempt loads @sap/cds from global installed @sap/cds-dk. * Throws error if module could not be loaded or strict mode is true and there is a version mismatch. * * @param capProjectPath - project root of a CAP project * @param [strict] - optional, when set true an error is thrown, if global loaded cds version does not match the cds version from package.json dependency. Default is false. * @returns - CAP CDS module for a CAP project */ async function loadCdsModuleFromProject(capProjectPath, strict = false) { let module; let loadProjectError; let loadError; try { // First approach, load @sap/cds from project module = await loadModuleFromProject(capProjectPath, '@sap/cds'); } catch (error) { loadProjectError = error; } if (!module) { try { // Second approach, load @sap/cds from @sap/cds-dk module = await loadGlobalCdsModule(); } catch (error) { loadError = error; } } if (!module) { throw Error(`Could not load cds module. Attempt to load module @sap/cds from project threw error '${loadProjectError}', attempt to load module @sap/cds from @sap/cds-dk threw error '${loadError}'`); } const cds = 'default' in module ? module.default : module; // In case strict is true and there was a fallback to global cds installation for a project that has a cds dependency, check if major versions match if (strict && loadProjectError) { const cdsDependencyVersion = await getCdsVersionFromPackageJson(join(capProjectPath, FileName.Package)); if (typeof cdsDependencyVersion === 'string') { const globalCdsVersion = cds.version; if (getMajorVersion(cdsDependencyVersion) !== getMajorVersion(globalCdsVersion)) { const error = new Error(`The @sap/cds major version (${cdsDependencyVersion}) specified in your CAP project is different to the @sap/cds version you have installed globally (${globalCdsVersion}). Please run 'npm install' on your CAP project to ensure that the correct CDS version is loaded.`); error.code = 'CDS_VERSION_MISMATCH'; throw error; } } } // Fix when switching cds versions dynamically if (global) { global.cds = cds; } // correct cds.env for current project root. Especially needed CAP Java projects loading cds dependency from jar file cds.env = cds.env.for('cds', capProjectPath); return cds; } /** * Method to clear CAP CDS module cache for passed project path. * * @param projectRoot root of a CAP project. * @returns True if cache cleared successfully. */ export async function clearCdsModuleCache(projectRoot) { let result = false; try { const cds = await loadCdsModuleFromProject(projectRoot); if (cds) { clearCdsResolveCache(cds); result = true; } } catch (e) { // ignore exception } return result; } /** * Method to clear CAP CDS module cache for passed cds module. * * @param cds CAP CDS module */ function clearCdsResolveCache(cds) { cds.resolve.cache = {}; } /** * Get absolute path to a resource. * * @param projectRoot - project root of a CAP project * @param relativeUri - relative resource path. * @returns {string} - absolute path. */ export const toAbsoluteUri = (projectRoot, relativeUri) => join(projectRoot, relativeUri); /** * Converts to referenced uri to be used in using statements. * * @param projectRoot - project root of a CAP project * @param relativeUriFrom - relative uri of from directory * @param relativeUriTo - relative uri of to directory * @returns {Promise<string>} - reference uri */ export const toReferenceUri = async (projectRoot, relativeUriFrom, relativeUriTo) => { let relativeUri = ''; const indexNodeModules = relativeUriTo.lastIndexOf('node_modules'); if (indexNodeModules >= 0) { // extract module name from fileUri - e.g. '@sap/cds/common' from '../../node_modules/@sap/cds/common.cds' const indexLastDot = relativeUriTo.lastIndexOf('.'); if (indexLastDot > indexNodeModules + 13) { relativeUri = relativeUriTo.slice(indexNodeModules + 13, indexLastDot); } else { relativeUri = relativeUriTo.slice(indexNodeModules + 13); } } else if (relativeUriTo.startsWith('../') || relativeUriTo.startsWith('..\\')) { // file outside current project (e.g. mono repo) const result = await getPackageNameInFolder(projectRoot, relativeUriTo); if (result.packageName) { relativeUri = result.packageName + relativeUriTo.slice(result.packageFolder.length); } } if (!relativeUri) { // build relative path const fromDir = dirname(toAbsoluteUri(projectRoot, relativeUriFrom)); relativeUri = relative(fromDir, toAbsoluteUri(projectRoot, relativeUriTo)); if (!relativeUri.startsWith('.')) { relativeUri = './' + relativeUri; } } // remove file extension const fileExtension = relativeUri.lastIndexOf('.') > 0 ? relativeUri.slice(relativeUri.lastIndexOf('.') + 1) : ''; if (['CDS', 'JSON'].includes(fileExtension.toUpperCase())) { relativeUri = relativeUri.slice(0, relativeUri.length - fileExtension.length - 1); } // always use '/' instead of platform specific separator return relativeUri.split(sep).join('/'); }; /** * Gets package name from the folder. * * @param baseUri - base uri of the cap project * @param relativeUri - relative uri to the resource folder * @returns {Promise<{ packageName: string; packageFolder: string }>} - package name and folder */ async function getPackageNameInFolder(baseUri, relativeUri) { const refUriParts = relativeUri.split(sep); const result = { packageName: '', packageFolder: relativeUri }; for (let i = refUriParts.length - 1; i >= 0 && !result.packageName; i--) { const currentFolder = refUriParts.slice(0, i).join(sep); result.packageName = await readPackageNameForFolder(baseUri, currentFolder); if (result.packageName) { result.packageFolder = currentFolder; } } return result; } /** * Reads package name from package json of the folder. * * @param baseUri - base uri of the cap project * @param relativeUri - relative uri to the resource folder * @returns {Promise<string>} - package name */ async function readPackageNameForFolder(baseUri, relativeUri) { let packageName = ''; try { const path = normalize(baseUri + '/' + relativeUri + '/' + FileName.Package); const content = await readJSON(path); if (typeof content?.name === 'string') { packageName = content.name; } } catch (e) { packageName = ''; } return packageName; } // Cache for request to load global cds. Cache the promise to avoid starting multiple identical requests in parallel. let globalCdsModulePromise; /** * Try to load global installation of @sap/cds, usually child of @sap/cds-dk. * * @returns - module @sap/cds from global installed @sap/cds-dk */ async function loadGlobalCdsModule() { globalCdsModulePromise = globalCdsModulePromise ?? new Promise((resolve, reject) => { return getGlobalCdsHomePath().then((home) => { if (home) { // "@sap/cds" module is inside node_modules of "@sap/cds-dk" resolve(loadModuleFromProject(join(home, 'node_modules', '@sap', 'cds'), '@sap/cds')); } else { reject(new Error('Can not find global installation of module @sap/cds, which should be part of @sap/cds-dk')); } }, reject); }); return globalCdsModulePromise; } /** * Clear cache of request to load global cds module. */ export function clearGlobalCdsModulePromiseCache() { globalCdsModulePromise = undefined; } /** * Get cds environment information, which includes the home path of cds-dk module. * * @param [cwd] - optional folder in which cds env --json should be executed * @returns - result of call 'cds env --json' */ async function getCdsEnvData(cwd) { return new Promise((resolve, reject) => { let out = ''; // call 'cds env --json' const cdsVersionInfo = spawn('cds', ['env', '--json'], { cwd, shell: true }); cdsVersionInfo.stdout.on('data', (data) => { out += data.toString(); }); cdsVersionInfo.on('close', () => { if (out) { try { resolve(JSON.parse(out)); } catch (e) { reject(new Error(`Unexpected output of "cds env --json": ${e.message}`)); } } else { reject(new Error('Module path not found')); } }); cdsVersionInfo.on('error', (error) => { reject(error); }); }); } /** * Retrieves the global CDS home path from the environment. * * Uses the output of `cds env --json` to determine the location of the global @sap/cds-dk installation. * * @returns {Promise<string | undefined>} The absolute path to the global CDS home directory, or undefined if not found. */ export async function getGlobalCdsHomePath() { const cdsEnvData = await getCdsEnvData(); // Handle output of `cds env --json` return cdsEnvData['_home_cds-dk']; } /** * Read the version string of the @sap/cds module from the package.json file. * * @param packageJsonPath - path to package.json * @returns - version of @sap/cds from package.json or undefined */ async function getCdsVersionFromPackageJson(packageJsonPath) { let version; try { if (await fileExists(packageJsonPath)) { const packageJson = await readJSON(packageJsonPath); version = packageJson?.dependencies?.['@sap/cds']; } } catch { // If we can't read or parse the package.json we return undefined } return version; } /** * Get major version from version string. * * @param versionString - version string * @returns - major version as number */ function getMajorVersion(versionString) { return Number.parseInt(/\d+/.exec(versionString.split('.')[0])?.[0] ?? '0', 10); } /** * Method resolves cap service name for passed project root and service uri. * * @param projectRoot - project root * @param datasourceUri - service uri * @returns - found cap service name */ export async function getCapServiceName(projectRoot, datasourceUri) { const services = (await getCapModelAndServices(projectRoot)).services; const service = findServiceByUri(services, datasourceUri); if (!service?.name) { const errorMessage = `Service for uri: '${datasourceUri}' not found. Available services: ${JSON.stringify(services)}`; throw Error(errorMessage); } return service.name; } /** * Method cleans up cds files after deletion of passed appName. * * @param cdsFilePaths - cds files to cleanup * @param appName - CAP application name * @param memFs - optional mem-fs-editor instance * @param logger - function to log messages (optional) */ async function cleanupCdsFiles(cdsFilePaths, appName, memFs, logger) { const usingEntry = `using from './${appName}/annotations';`; for (const cdsFilePath of cdsFilePaths) { if (await fileExists(cdsFilePath, memFs)) { try { let cdsFile = await readFile(cdsFilePath, memFs); if (cdsFile.indexOf(usingEntry) !== -1) { logger?.info(`Removing using statement for './${appName}/annotations' from '${cdsFilePath}'.`); cdsFile = cdsFile.replace(usingEntry, ''); if (cdsFile.replace(/\n/g, '').trim() === '') { logger?.info(`File '${cdsFilePath}' is now empty, removing it.`); await deleteFile(cdsFilePath, memFs); } else { await writeFile(cdsFilePath, cdsFile, memFs); } } } catch (error) { logger?.error(`Could not modify file '${cdsFilePath}'. Skipping this file.`); } } } } /** * Delete application from CAP project. * * @param appPath - path to the application in a CAP project * @param [memFs] - optional mem-fs-editor instance * @param [logger] - function to log messages (optional) */ export async function deleteCapApp(appPath, memFs, logger) { const appName = basename(appPath); const projectRoot = await findCapProjectRoot(appPath); if (!projectRoot) { const message = `Project root was not found for CAP application with path '${appPath}'`; logger?.error(message); throw Error(message); } const packageJsonPath = join(projectRoot, FileName.Package); const packageJson = await readJSON(packageJsonPath, memFs); const cdsFilePaths = [join(dirname(appPath), FileName.ServiceCds), join(dirname(appPath), FileName.IndexCds)]; logger?.info(`Deleting app '${appName}' from CAP project '${projectRoot}'.`); // Update `sapux` array if presented in package.json if (Array.isArray(packageJson.sapux)) { const posixAppPath = appPath.replace(/\\/g, '/'); packageJson.sapux = packageJson.sapux.filter((a) => !posixAppPath.endsWith(a.replace(/\\/g, '/'))); if (packageJson.sapux.length === 0) { logger?.info(`This was the last app in this CAP project. Deleting property 'sapux' from '${packageJsonPath}'.`); delete packageJson.sapux; } } if (packageJson.scripts?.[`watch-${appName}`]) { delete packageJson.scripts[`watch-${appName}`]; } await updatePackageJSON(packageJsonPath, packageJson, memFs); logger?.info(`File '${packageJsonPath}' updated.`); await deleteDirectory(appPath, memFs); logger?.info(`Directory '${appPath}' deleted.`); // Cleanup app/service.cds and app/index.cds files await cleanupCdsFiles(cdsFilePaths, appName, memFs, logger); // Check if app folder is now empty if ((await readDirectory(dirname(appPath))).length === 0) { logger?.info(`Directory '${dirname(appPath)}' is now empty. Deleting it.`); await deleteDirectory(dirname(appPath), memFs); } } /** * Implementation of the overloaded function. * Check if cds-plugin-ui5 is enabled on a CAP project. Checks also all prerequisites, like minimum @sap/cds version. * * @param basePath - root path of the CAP project, where package.json is located * @param [fs] - optional: the memfs editor instance * @param [moreInfo] if true return an object specifying detailed info about the cds and workspace state * @param {CdsVersionInfo} [cdsVersionInfo] - If provided will be used instead of parsing the package.json file to determine the cds version. * @returns false if package.json is not found at specified path or {@link CdsUi5PluginInfo} with additional info or true if * cds-plugin-ui5 and all prerequisites are fulfilled */ export async function checkCdsUi5PluginEnabled(basePath, fs, moreInfo, cdsVersionInfo) { if (!fs) { fs = create(createStorage()); } const packageJsonPath = join(basePath, 'package.json'); if (!fs.exists(packageJsonPath)) { return false; } const packageJson = fs.readJSON(packageJsonPath); const { workspaceEnabled } = await getWorkspaceInfo(basePath, packageJson); const cdsInfo = { // Below line checks if 'cdsVersionInfo' is available and contains version information. // If it does, it uses that version information to determine if it satisfies the minimum CDS version required. // If 'cdsVersionInfo' is not available or does not contain version information,it falls back to check the version specified in the package.json file. hasMinCdsVersion: cdsVersionInfo?.version ? satisfies(cdsVersionInfo?.version, `>=${MinCdsVersion}`) : satisfiesMinCdsVersion(packageJson), isWorkspaceEnabled: workspaceEnabled, hasCdsUi5Plugin: hasDependency(packageJson, 'cds-plugin-ui5'), isCdsUi5PluginEnabled: false }; cdsInfo.isCdsUi5PluginEnabled = cdsInfo.hasMinCdsVersion && cdsInfo.isWorkspaceEnabled && cdsInfo.hasCdsUi5Plugin; return moreInfo ? cdsInfo : cdsInfo.isCdsUi5PluginEnabled; } /** * Get information about the workspaces in the CAP project. * * @param basePath - root path of the CAP project, where package.json is located * @param packageJson - the parsed package.json * @returns - appWorkspace containing the path to the appWorkspace including wildcard; workspaceEnabled: boolean that states whether workspace for apps are enabled */ export async function getWorkspaceInfo(basePath, packageJson) { const capPaths = await getCapCustomPaths(basePath); const appWorkspace = capPaths.app.endsWith('/') ? `${capPaths.app}*` : `${capPaths.app}/*`; const workspacePackages = getWorkspacePackages(packageJson) ?? []; const workspaceEnabled = workspacePackages.includes(appWorkspace); return { appWorkspace, workspaceEnabled, workspacePackages }; } /** * Return the reference to the array of workspace packages or undefined if not defined. * The workspace packages can either be defined directly as workspaces in package.json * or in workspaces.packages, e.g. in yarn workspaces. * * @param packageJson - the parsed package.json * @returns ref to the packages in workspaces or undefined */ function getWorkspacePackages(packageJson) { let workspacePackages; if (Array.isArray(packageJson.workspaces)) { workspacePackages = packageJson.workspaces; } else if (Array.isArray(packageJson.workspaces?.packages)) { workspacePackages = packageJson.workspaces?.packages; } return workspacePackages; } /** * Check if package.json has version or version range that satisfies the minimum version of @sap/cds. * * @param packageJson - the parsed package.json * @returns - true: cds version satisfies the min cds version; false: cds version does not satisfy min cds version */ export function satisfiesMinCdsVersion(packageJson) { return hasMinCdsVersion(packageJson) || satisfies(MinCdsVersion, packageJson.dependencies?.['@sap/cds'] ?? '0.0.0'); } /** * Check if package.json has dependency to the minimum min version of @sap/cds, * that is required to enable cds-plugin-ui. * * @param packageJson - the parsed package.json * @returns - true: min cds version is present; false: cds version needs update */ export function hasMinCdsVersion(packageJson) { return gte(coerce(packageJson.dependencies?.['@sap/cds']) ?? '0.0.0', MinCdsVersion); } //# sourceMappingURL=cap.js.map