diff --git a/.changeset/long-planets-sing.md b/.changeset/long-planets-sing.md new file mode 100644 index 0000000000..c588dcaf69 --- /dev/null +++ b/.changeset/long-planets-sing.md @@ -0,0 +1,7 @@ +--- +'@sap-ux/app-config-writer': patch +'@sap-ux/project-access': patch +'@sap-ux/ui5-application-inquirer': patch +--- + +fix: wrong convert preview-config prerequisites check for usage of cds-plugin-ui5 diff --git a/packages/app-config-writer/src/preview-config/package-json.ts b/packages/app-config-writer/src/preview-config/package-json.ts index caeca66a7d..6b9ae47217 100644 --- a/packages/app-config-writer/src/preview-config/package-json.ts +++ b/packages/app-config-writer/src/preview-config/package-json.ts @@ -3,7 +3,7 @@ import { extractYamlConfigFileName, isTestPath } from './ui5-yaml'; import { generateVariantsConfig } from '../variants-config'; import type { Editor } from 'mem-fs-editor'; import type { ToolsLogger } from '@sap-ux/logger'; -import type { Package } from '@sap-ux/project-access'; +import { type Package, hasDependency } from '@sap-ux/project-access'; import type { FlpConfig } from '@sap-ux/preview-middleware'; import type { Script } from './ui5-yaml'; @@ -22,11 +22,8 @@ export function ensurePreviewMiddlewareDependency(fs: Editor, basePath: string): return; } - const hasDependency = (dependency: string): boolean => - !!packageJson?.devDependencies?.[dependency] || !!packageJson?.dependencies?.[dependency]; - const dependencies = ['@sap-ux/preview-middleware', '@sap/ux-ui5-tooling']; - if (dependencies.some((dependency) => hasDependency(dependency))) { + if (dependencies.some((dependency) => hasDependency(packageJson, dependency))) { return; } diff --git a/packages/app-config-writer/src/preview-config/prerequisites.ts b/packages/app-config-writer/src/preview-config/prerequisites.ts index fd4ead2baa..1c42f8a04b 100644 --- a/packages/app-config-writer/src/preview-config/prerequisites.ts +++ b/packages/app-config-writer/src/preview-config/prerequisites.ts @@ -1,9 +1,24 @@ import { join } from 'path'; import type { Editor } from 'mem-fs-editor'; -import type { Package } from '@sap-ux/project-access'; +import { + type Package, + findCapProjectRoot, + FileName, + checkCdsUi5PluginEnabled, + hasDependency +} from '@sap-ux/project-access'; import type { ToolsLogger } from '@sap-ux/logger'; import { satisfies, valid } from 'semver'; +const packageName = { + WDIO_QUNIT_SERVICE: 'wdio-qunit-service', + KARMA_UI5: 'karma-ui5', + UI5_CLI: '@ui5/cli', + SAP_UX_UI5_TOOLING: '@sap/ux-ui5-tooling', + SAP_UX_UI5_MIDDLEWARE_FE_MOCKSERVER: '@sap-ux/ui5-middleware-fe-mockserver', + SAP_GRUNT_SAPUI5_BESTPRACTICE_BUILD: '@sap/grunt-sapui5-bestpractice-build' +} as const; + /** * Check if the version of the given package is lower than the minimal version. * @@ -35,6 +50,21 @@ function isLowerThanMinimalVersion( return !satisfies(minVersionInfo, versionInfo); } +/** + * Check if the project is a CAP project that uses 'cds-plugin-ui5'. + * + * @param basePath - base path of the app + * @param fs - file system reference + * @returns indicator if the project is a CAP project that uses 'cds-plugin-ui5' + */ +async function isUsingCdsPluginUi5(basePath: string, fs: Editor): Promise { + const capProjectRootPath = await findCapProjectRoot(basePath, false, fs); + if (!capProjectRootPath) { + return false; + } + return await checkCdsUi5PluginEnabled(capProjectRootPath, fs); +} + /** * Check if the prerequisites for the conversion are met. * - UI5 CLI version 3.0.0 or higher is being used. @@ -53,60 +83,52 @@ export async function checkPrerequisites( convertTests: boolean = false, logger?: ToolsLogger ): Promise { - const packageJsonPath = join(basePath, 'package.json'); + const packageJsonPath = join(basePath, FileName.Package); const packageJson = fs.readJSON(packageJsonPath) as Package | undefined; let prerequisitesMet = true; if (!packageJson) { - throw Error(`File 'package.json' not found at '${basePath}'`); + throw Error(`File '${FileName.Package}' not found at '${basePath}'`); } - const sapui5BestpracticeBuildExists = - !!packageJson?.devDependencies?.['@sap/grunt-sapui5-bestpractice-build'] || - !!packageJson?.dependencies?.['@sap/grunt-sapui5-bestpractice-build']; - if (sapui5BestpracticeBuildExists) { + if (hasDependency(packageJson, packageName.SAP_GRUNT_SAPUI5_BESTPRACTICE_BUILD)) { logger?.error( - "Conversion from '@sap/grunt-sapui5-bestpractice-build' is not supported. You must migrate to UI5 CLI version 3.0.0 or higher. For more information, see https://sap.github.io/ui5-tooling/v3/updates/migrate-v3." + `Conversion from '${packageName.SAP_GRUNT_SAPUI5_BESTPRACTICE_BUILD}' is not supported. You must migrate to UI5 CLI version 3.0.0 or higher. For more information, see https://sap.github.io/ui5-tooling/v3/updates/migrate-v3.` ); prerequisitesMet = false; } - if (isLowerThanMinimalVersion(packageJson, '@ui5/cli', '3.0.0')) { + if (isLowerThanMinimalVersion(packageJson, packageName.UI5_CLI, '3.0.0')) { logger?.error( 'UI5 CLI version 3.0.0 or higher is required to convert the preview to virtual files. For more information, see https://sap.github.io/ui5-tooling/v3/updates/migrate-v3.' ); prerequisitesMet = false; } - if (isLowerThanMinimalVersion(packageJson, '@sap/ux-ui5-tooling', '1.15.4', false)) { + if (isLowerThanMinimalVersion(packageJson, packageName.SAP_UX_UI5_TOOLING, '1.15.4', false)) { logger?.error( 'UX UI5 Tooling version 1.15.4 or higher is required to convert the preview to virtual files. For more information, see https://www.npmjs.com/package/@sap/ux-ui5-tooling.' ); prerequisitesMet = false; } - const ui5MiddlewareMockserverExists = - !!packageJson?.devDependencies?.['@sap-ux/ui5-middleware-fe-mockserver'] || - !!packageJson?.dependencies?.['@sap-ux/ui5-middleware-fe-mockserver']; - const cdsPluginUi5Exists = - !!packageJson?.devDependencies?.['cds-plugin-ui5'] || !!packageJson?.dependencies?.['cds-plugin-ui5']; - if (!ui5MiddlewareMockserverExists && !cdsPluginUi5Exists) { + if ( + !hasDependency(packageJson, packageName.SAP_UX_UI5_MIDDLEWARE_FE_MOCKSERVER) && + !(await isUsingCdsPluginUi5(basePath, fs)) + ) { logger?.error( - "Conversion from 'sap/ui/core/util/MockServer' is not supported. You must migrate from '@sap-ux/ui5-middleware-fe-mockserver'. For more information, see https://www.npmjs.com/package/@sap-ux/ui5-middleware-fe-mockserver." + `Conversion from 'sap/ui/core/util/MockServer' or '@sap/ux-ui5-fe-mockserver-middleware' is not supported. You must migrate to '${packageName.SAP_UX_UI5_MIDDLEWARE_FE_MOCKSERVER}' first. For more information, see https://www.npmjs.com/package/@sap-ux/ui5-middleware-fe-mockserver.` ); prerequisitesMet = false; } - if (convertTests && (packageJson?.devDependencies?.['karma-ui5'] ?? packageJson?.dependencies?.['karma-ui5'])) { + if (convertTests && hasDependency(packageJson, packageName.KARMA_UI5)) { logger?.warn( "This app seems to use Karma as a test runner. Please note that the converter does not convert any Karma configuration files. Please update your karma configuration ('ui5.configPath' and 'ui5.testpage') according to the new virtual endpoints after the conversion." ); } - if ( - convertTests && - (packageJson?.devDependencies?.['wdio-qunit-service'] ?? packageJson?.dependencies?.['wdio-qunit-service']) - ) { + if (convertTests && hasDependency(packageJson, packageName.WDIO_QUNIT_SERVICE)) { logger?.warn( 'This app seems to use the WebdriverIO QUnit Service as a test runner. Please note that the converter does not convert any WebdriverIO configuration files. Please update your WebdriverIO QUnit Service test paths according to the new virtual endpoints after the conversion.' ); diff --git a/packages/app-config-writer/test/unit/preview-config/prerequisites.test.ts b/packages/app-config-writer/test/unit/preview-config/prerequisites.test.ts index af51dd2692..0868f4c199 100644 --- a/packages/app-config-writer/test/unit/preview-config/prerequisites.test.ts +++ b/packages/app-config-writer/test/unit/preview-config/prerequisites.test.ts @@ -3,17 +3,20 @@ import { create } from 'mem-fs-editor'; import { create as createStorage } from 'mem-fs'; import { join } from 'path'; import { ToolsLogger } from '@sap-ux/logger'; +import * as ProjectAccess from '@sap-ux/project-access'; describe('prerequisites', () => { const logger = new ToolsLogger(); const errorLogMock = jest.spyOn(ToolsLogger.prototype, 'error').mockImplementation(() => {}); const warnLogMock = jest.spyOn(ToolsLogger.prototype, 'warn').mockImplementation(() => {}); const basePath = join(__dirname, '../../fixtures/preview-config'); + jest.spyOn(ProjectAccess, 'findCapProjectRoot').mockResolvedValue(basePath); const fs = create(createStorage()); beforeEach(() => { jest.clearAllMocks(); fs.delete(join(basePath, 'various-configs', 'package.json')); + jest.spyOn(ProjectAccess, 'checkCdsUi5PluginEnabled').mockResolvedValue(true); }); test('check prerequisites w/o package.json', async () => { @@ -42,19 +45,13 @@ describe('prerequisites', () => { }); test('check prerequisites with UI5 cli ^3 dependency', async () => { - fs.write( - join(basePath, 'package.json'), - JSON.stringify({ devDependencies: { '@ui5/cli': '^3', 'cds-plugin-ui5': '6.6.6' } }) - ); + fs.write(join(basePath, 'package.json'), JSON.stringify({ devDependencies: { '@ui5/cli': '^3' } })); expect(await checkPrerequisites(basePath, fs, false, logger)).toBeTruthy(); }); test('check prerequisites with UI5 cli ^2 dependency', async () => { - fs.write( - join(basePath, 'package.json'), - JSON.stringify({ devDependencies: { '@ui5/cli': '^2', 'cds-plugin-ui5': '6.6.6' } }) - ); + fs.write(join(basePath, 'package.json'), JSON.stringify({ devDependencies: { '@ui5/cli': '^2' } })); expect(await checkPrerequisites(basePath, fs, false, logger)).toBeFalsy(); expect(errorLogMock).toHaveBeenCalledWith( @@ -66,7 +63,7 @@ describe('prerequisites', () => { fs.write( join(basePath, 'package.json'), JSON.stringify({ - devDependencies: { '@sap/ux-ui5-tooling': '1.16.0', '@ui5/cli': '^3', 'cds-plugin-ui5': '6.6.6' } + devDependencies: { '@sap/ux-ui5-tooling': '1.16.0', '@ui5/cli': '^3' } }) ); @@ -77,7 +74,7 @@ describe('prerequisites', () => { fs.write( join(basePath, 'package.json'), JSON.stringify({ - devDependencies: { '@sap/ux-ui5-tooling': '1', '@ui5/cli': '^3', 'cds-plugin-ui5': '6.6.6' } + devDependencies: { '@sap/ux-ui5-tooling': '1', '@ui5/cli': '^3' } }) ); @@ -88,7 +85,7 @@ describe('prerequisites', () => { fs.write( join(basePath, 'package.json'), JSON.stringify({ - devDependencies: { '@sap/ux-ui5-tooling': 'latest', '@ui5/cli': '^3', 'cds-plugin-ui5': '6.6.6' } + devDependencies: { '@sap/ux-ui5-tooling': 'latest', '@ui5/cli': '^3' } }) ); @@ -108,19 +105,17 @@ describe('prerequisites', () => { }); test('check prerequisites w/o mockserver dependency', async () => { + jest.spyOn(ProjectAccess, 'checkCdsUi5PluginEnabled').mockResolvedValue(false); fs.write(join(basePath, 'package.json'), JSON.stringify({ devDependencies: { '@ui5/cli': '3.0.0' } })); expect(await checkPrerequisites(basePath, fs, false, logger)).toBeFalsy(); expect(errorLogMock).toHaveBeenCalledWith( - "Conversion from 'sap/ui/core/util/MockServer' is not supported. You must migrate from '@sap-ux/ui5-middleware-fe-mockserver'. For more information, see https://www.npmjs.com/package/@sap-ux/ui5-middleware-fe-mockserver." + "Conversion from 'sap/ui/core/util/MockServer' or '@sap/ux-ui5-fe-mockserver-middleware' is not supported. You must migrate to '@sap-ux/ui5-middleware-fe-mockserver' first. For more information, see https://www.npmjs.com/package/@sap-ux/ui5-middleware-fe-mockserver." ); }); test('check prerequisites w/o mockserver dependency but with cds-plugin-ui5 dependency', async () => { - fs.write( - join(basePath, 'package.json'), - JSON.stringify({ devDependencies: { '@ui5/cli': '3.0.0', 'cds-plugin-ui5': '6.6.6' } }) - ); + fs.write(join(basePath, 'package.json'), JSON.stringify({ devDependencies: { '@ui5/cli': '3.0.0' } })); expect(await checkPrerequisites(basePath, fs, false, logger)).toBeTruthy(); }); diff --git a/packages/cap-config-writer/src/cap-config/index.ts b/packages/cap-config-writer/src/cap-config/index.ts index cbd8f5ed6f..68241ab90e 100644 --- a/packages/cap-config-writer/src/cap-config/index.ts +++ b/packages/cap-config-writer/src/cap-config/index.ts @@ -2,19 +2,8 @@ import { join } from 'path'; import { create as createStorage } from 'mem-fs'; import { create } from 'mem-fs-editor'; import type { Editor } from 'mem-fs-editor'; -import type { Package, CdsVersionInfo } from '@sap-ux/project-access'; -import { - addCdsPluginUi5, - enableWorkspaces, - ensureMinCdsVersion, - getWorkspaceInfo, - hasCdsPluginUi5, - satisfiesMinCdsVersion, - minCdsVersion -} from './package-json'; -export { satisfiesMinCdsVersion } from './package-json'; -import type { CdsUi5PluginInfo } from './types'; -import { satisfies } from 'semver'; +import type { Package } from '@sap-ux/project-access'; +import { addCdsPluginUi5, enableWorkspaces, ensureMinCdsVersion } from './package-json'; /** * Enable workspace and cds-plugin-ui5 for given CAP project. @@ -37,86 +26,3 @@ export async function enableCdsUi5Plugin(basePath: string, fs?: Editor): Promise fs.writeJSON(packageJsonPath, packageJson); return fs; } - -/** - * Check if cds-plugin-ui5 is enabled on a CAP project. Checks also all prerequisites, like minimum @sap/cds version. - * Overloaded function that returns detailed CAP plugin info. - * - * @param basePath - root path of the CAP project, where package.json is located - * @param [fs] - optional: the memfs editor instance - * @returns true: cds-plugin-ui5 and all prerequisites are fulfilled; false: cds-plugin-ui5 is not enabled or not all prerequisites are fulfilled - */ -export async function checkCdsUi5PluginEnabled(basePath: string, fs?: Editor): Promise; - -/** - * 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 - * @returns false if package.json is not found at specified path or {@link CdsUi5PluginInfo} with additional info - */ -export async function checkCdsUi5PluginEnabled( - basePath: string, - fs?: Editor, - moreInfo?: boolean -): Promise; - -/** - * 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 - */ -export async function checkCdsUi5PluginEnabled( - basePath: string, - fs?: Editor, - moreInfo?: boolean, - cdsVersionInfo?: CdsVersionInfo -): Promise; - -/** - * 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: string, - fs?: Editor, - moreInfo?: boolean, - cdsVersionInfo?: CdsVersionInfo -): Promise { - if (!fs) { - fs = create(createStorage()); - } - const packageJsonPath = join(basePath, 'package.json'); - if (!fs.exists(packageJsonPath)) { - return false; - } - const packageJson = fs.readJSON(packageJsonPath) as Package; - const { workspaceEnabled } = await getWorkspaceInfo(basePath, packageJson); - const cdsInfo: CdsUi5PluginInfo = { - // 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: hasCdsPluginUi5(packageJson), - isCdsUi5PluginEnabled: false - }; - cdsInfo.isCdsUi5PluginEnabled = cdsInfo.hasMinCdsVersion && cdsInfo.isWorkspaceEnabled && cdsInfo.hasCdsUi5Plugin; - return moreInfo ? cdsInfo : cdsInfo.isCdsUi5PluginEnabled; -} - -export { minCdsVersion }; diff --git a/packages/cap-config-writer/src/cap-config/package-json.ts b/packages/cap-config-writer/src/cap-config/package-json.ts index 01f6d106a6..1fa099b336 100644 --- a/packages/cap-config-writer/src/cap-config/package-json.ts +++ b/packages/cap-config-writer/src/cap-config/package-json.ts @@ -1,8 +1,11 @@ -import { coerce, gte, satisfies } from 'semver'; -import { getCapCustomPaths } from '@sap-ux/project-access'; -import type { Package } from '@sap-ux/project-access'; +import { + type Package, + getWorkspaceInfo, + MinCdsVersionUi5Plugin, + hasMinCdsVersion, + hasDependency +} from '@sap-ux/project-access'; -export const minCdsVersion = '6.8.2'; const minCdsPluginUi5Version = '0.9.3'; /** @@ -13,7 +16,7 @@ const minCdsPluginUi5Version = '0.9.3'; export function ensureMinCdsVersion(packageJson: Package): void { if (!hasMinCdsVersion(packageJson)) { packageJson.dependencies ??= {}; - packageJson.dependencies['@sap/cds'] = `^${minCdsVersion}`; + packageJson.dependencies['@sap/cds'] = `^${MinCdsVersionUi5Plugin}`; } } @@ -24,12 +27,11 @@ export function ensureMinCdsVersion(packageJson: Package): void { * @param packageJson - the parsed package.json */ export async function enableWorkspaces(basePath: string, packageJson: Package): Promise { - const { appWorkspace, workspaceEnabled } = await getWorkspaceInfo(basePath, packageJson); + let { appWorkspace, workspaceEnabled, workspacePackages } = await getWorkspaceInfo(basePath, packageJson); if (workspaceEnabled) { return; } - let workspacePackages = getWorkspacePackages(packageJson); - if (!workspacePackages) { + if (workspacePackages.length === 0) { packageJson.workspaces ??= []; if (Array.isArray(packageJson.workspaces)) { workspacePackages = packageJson.workspaces; @@ -47,75 +49,8 @@ export async function enableWorkspaces(basePath: string, packageJson: Package): * @param packageJson - the parsed package.json */ export function addCdsPluginUi5(packageJson: Package): void { - if (!hasCdsPluginUi5(packageJson)) { + if (!hasDependency(packageJson, 'cds-plugin-ui5')) { packageJson.devDependencies ??= {}; packageJson.devDependencies['cds-plugin-ui5'] = `^${minCdsPluginUi5Version}`; } } - -/** - * 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: Package): boolean { - return gte(coerce(packageJson.dependencies?.['@sap/cds']) ?? '0.0.0', minCdsVersion); -} - -/** - * 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: Package): boolean { - return hasMinCdsVersion(packageJson) || satisfies(minCdsVersion, packageJson.dependencies?.['@sap/cds'] ?? '0.0.0'); -} - -/** - * 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: string, - packageJson: Package -): Promise<{ appWorkspace: string; workspaceEnabled: boolean }> { - 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 }; -} - -/** - * 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: Package): string[] | undefined { - let workspacePackages: string[] | undefined; - if (Array.isArray(packageJson.workspaces)) { - workspacePackages = packageJson.workspaces; - } else if (Array.isArray(packageJson.workspaces?.packages)) { - workspacePackages = packageJson.workspaces?.packages; - } - return workspacePackages; -} - -/** - * Check if devDependency to cds-plugin-ui5 is present in package.json. - * - * @param packageJson - the parsed package.json - * @returns true: devDependency to cds-plugin-ui5 exists; false: devDependency to cds-plugin-ui5 does not exist - */ -export function hasCdsPluginUi5(packageJson: Package): boolean { - return !!packageJson.devDependencies?.['cds-plugin-ui5']; -} diff --git a/packages/cap-config-writer/src/cap-config/types.ts b/packages/cap-config-writer/src/cap-config/types.ts index ce86eb5b23..be1360c6a7 100644 --- a/packages/cap-config-writer/src/cap-config/types.ts +++ b/packages/cap-config-writer/src/cap-config/types.ts @@ -1,23 +1,4 @@ -import type { CdsVersionInfo } from '@sap-ux/project-access'; - -export type CdsUi5PluginInfo = { - /** - * Convienience property. The CDS UI5 plugin is considered enabled if `hasCdsUi5Plugin`, `hasMinCdsVersion`, `isWorkspaceEnabled` are all true. - */ - isCdsUi5PluginEnabled: boolean; - /** - * True if the CDS version satisfies the minimum supported CDS version - */ - hasMinCdsVersion: boolean; - /** - * True if NPM workspaces are enabled at the root of a CAP project - */ - isWorkspaceEnabled: boolean; - /** - * True if the CDS ui5 plugin is specified as a dependency - */ - hasCdsUi5Plugin: boolean; -}; +import type { CdsVersionInfo, CdsUi5PluginInfo } from '@sap-ux/project-access'; export type CapRuntime = 'Node.js' | 'Java'; diff --git a/packages/cap-config-writer/src/cap-writer/package-json.ts b/packages/cap-config-writer/src/cap-writer/package-json.ts index 88a3d693ce..17daf746dd 100644 --- a/packages/cap-config-writer/src/cap-writer/package-json.ts +++ b/packages/cap-config-writer/src/cap-writer/package-json.ts @@ -1,16 +1,16 @@ import type { Package } from '@sap-ux/project-access'; -import { getCapCustomPaths } from '@sap-ux/project-access'; +import { getCapCustomPaths, checkCdsUi5PluginEnabled } from '@sap-ux/project-access'; import type { Editor } from 'mem-fs-editor'; import { dirname, join, normalize, posix } from 'path'; import type { CapServiceCdsInfo } from '../cap-config/types'; -import { enableCdsUi5Plugin, checkCdsUi5PluginEnabled } from '../cap-config'; +import { enableCdsUi5Plugin } from '../cap-config'; import type { Logger } from '@sap-ux/logger'; /** * Retrieves the CDS watch script for the CAP app. * * @param {string} projectName - The project's name, which is the module name. - * @param {string} appId - The application's ID, including its namespace and the module name. + * @param {string} appId - The application's ID, including its namespace and the module name. If appId is provided, it will be used to open the application instead of the project name. This option is available for use with npm workspaces. * @returns {{ [x: string]: string }} The CDS watch script for the CAP app. */ diff --git a/packages/cap-config-writer/src/index.ts b/packages/cap-config-writer/src/index.ts index f75d9e2948..91f73aadf6 100644 --- a/packages/cap-config-writer/src/index.ts +++ b/packages/cap-config-writer/src/index.ts @@ -1,4 +1,6 @@ -export { checkCdsUi5PluginEnabled, enableCdsUi5Plugin, satisfiesMinCdsVersion } from './cap-config'; +import { checkCdsUi5PluginEnabled } from '@sap-ux/project-access'; +export { checkCdsUi5PluginEnabled }; +export { enableCdsUi5Plugin } from './cap-config'; export type { CapService, CapRuntime } from './cap-config/types'; -export type { CdsUi5PluginInfo, CapServiceCdsInfo, CapProjectSettings } from './cap-config/types'; +export type { CapServiceCdsInfo, CapProjectSettings } from './cap-config/types'; export * from './cap-writer'; diff --git a/packages/cap-config-writer/test/unit/cap-config/index.test.ts b/packages/cap-config-writer/test/unit/cap-config/index.test.ts index b187fcb716..3b3a0a2534 100644 --- a/packages/cap-config-writer/test/unit/cap-config/index.test.ts +++ b/packages/cap-config-writer/test/unit/cap-config/index.test.ts @@ -2,8 +2,9 @@ import { promises } from 'fs'; import { join } from 'path'; import { create as createStorage } from 'mem-fs'; import { create } from 'mem-fs-editor'; -import * as projectAccessMock from '@sap-ux/project-access'; -import { checkCdsUi5PluginEnabled, enableCdsUi5Plugin } from '../../../src/cap-config'; +import type { Package } from '@sap-ux/project-access'; +import { enableCdsUi5Plugin } from '../../../src'; +import * as ProjectAccessMock from '@sap-ux/project-access'; const fixturesPath = join(__dirname, '../../fixture'); @@ -52,19 +53,20 @@ describe('Test enableCdsUi5Plugin()', () => { workspaces: ['app/*'] }); const fs = await enableCdsUi5Plugin(__dirname, memFs); - const packageJson = fs.readJSON(join(__dirname, 'package.json')) as projectAccessMock.Package; + const packageJson = fs.readJSON(join(__dirname, 'package.json')) as Package; expect(packageJson.devDependencies).toEqual({ 'cds-plugin-ui5': '^0.9.3' }); }); test('CAP with custom app path and mem-fs editor', async () => { - jest.spyOn(projectAccessMock, 'getCapCustomPaths').mockResolvedValueOnce({ - app: 'customAppPath', - db: 'db', - srv: 'srv' + jest.spyOn(ProjectAccessMock, 'hasMinCdsVersion').mockReturnValue(true); + jest.spyOn(ProjectAccessMock, 'getWorkspaceInfo').mockResolvedValueOnce({ + appWorkspace: 'customAppPath/*', + workspaceEnabled: false, + workspacePackages: [] }); const memFs = create(createStorage()); const fs = await enableCdsUi5Plugin(__dirname, memFs); - const packageJson = fs.readJSON(join(__dirname, 'package.json')) as projectAccessMock.Package; + const packageJson = fs.readJSON(join(__dirname, 'package.json')) as Package; expect(packageJson.workspaces).toEqual(['customAppPath/*']); }); @@ -72,99 +74,9 @@ describe('Test enableCdsUi5Plugin()', () => { const memFs = create(createStorage()); memFs.writeJSON(join(__dirname, 'package.json'), { workspaces: {} }); await enableCdsUi5Plugin(__dirname, memFs); - const packageJson = memFs.readJSON(join(__dirname, 'package.json')) as projectAccessMock.Package; + const packageJson = memFs.readJSON(join(__dirname, 'package.json')) as Package; expect(packageJson.workspaces).toEqual({ packages: ['app/*'] }); }); }); - -describe('Test checkCdsUi5PluginEnabled()', () => { - test('Empty project should return false', async () => { - expect(await checkCdsUi5PluginEnabled(__dirname)).toBe(false); - expect(await checkCdsUi5PluginEnabled(__dirname, undefined, true)).toBe(false); - }); - - test('CAP project with valid cds-plugin-ui', async () => { - expect(await checkCdsUi5PluginEnabled(join(fixturesPath, 'cap-valid-cds-plugin-ui'))).toBe(true); - expect(await checkCdsUi5PluginEnabled(join(fixturesPath, 'cap-valid-cds-plugin-ui'), undefined, true)).toEqual({ - hasCdsUi5Plugin: true, - hasMinCdsVersion: true, - isCdsUi5PluginEnabled: true, - isWorkspaceEnabled: true - }); - }); - - test('CAP project with missing apps folder in workspaces', async () => { - const memFs = create(createStorage()); - memFs.writeJSON(join(__dirname, 'package.json'), { - dependencies: { '@sap/cds': '6.8.2' }, - devDependencies: { 'cds-plugin-ui5': '0.0.1' }, - workspaces: [] - }); - expect(await checkCdsUi5PluginEnabled(__dirname, memFs)).toBe(false); - expect(await checkCdsUi5PluginEnabled(__dirname, memFs, true)).toEqual({ - hasCdsUi5Plugin: true, - hasMinCdsVersion: true, - isCdsUi5PluginEnabled: false, - isWorkspaceEnabled: false - }); - }); - - test('CAP project with workspaces config as object, but no apps folder', async () => { - const memFs = create(createStorage()); - memFs.writeJSON(join(__dirname, 'package.json'), { - dependencies: { '@sap/cds': '6.8.2' }, - devDependencies: { 'cds-plugin-ui5': '0.0.1' }, - workspaces: {} - }); - expect(await checkCdsUi5PluginEnabled(__dirname, memFs)).toBe(false); - expect(await checkCdsUi5PluginEnabled(__dirname, memFs, true)).toEqual({ - hasCdsUi5Plugin: true, - hasMinCdsVersion: true, - isCdsUi5PluginEnabled: false, - isWorkspaceEnabled: false - }); - }); - - test('CAP project with workspaces config as object, app folder in workspace', async () => { - const memFs = create(createStorage()); - memFs.writeJSON(join(__dirname, 'package.json'), { - dependencies: { '@sap/cds': '6.8.2' }, - devDependencies: { 'cds-plugin-ui5': '0.0.1' }, - workspaces: { - packages: ['app/*'] - } - }); - expect(await checkCdsUi5PluginEnabled(__dirname, memFs)).toBe(true); - expect(await checkCdsUi5PluginEnabled(__dirname, memFs, true)).toEqual({ - hasCdsUi5Plugin: true, - hasMinCdsVersion: true, - isCdsUi5PluginEnabled: true, - isWorkspaceEnabled: true - }); - }); - - test('CAP project with cds version info greater than minimum cds requirement', async () => { - const memFs = create(createStorage()); - memFs.writeJSON(join(__dirname, 'package.json'), { - dependencies: { '@sap/cds': '6.8.2' }, - devDependencies: { 'cds-plugin-ui5': '0.0.1' }, - workspaces: { - packages: ['app/*'] - } - }); - const cdsVersionInfo = { - home: '/path', - version: '7.7.2', - root: '/path/root' - }; - expect(await checkCdsUi5PluginEnabled(__dirname, memFs)).toBe(true); - expect(await checkCdsUi5PluginEnabled(__dirname, memFs, true, cdsVersionInfo)).toEqual({ - hasCdsUi5Plugin: true, - hasMinCdsVersion: true, - isCdsUi5PluginEnabled: true, - isWorkspaceEnabled: true - }); - }); -}); diff --git a/packages/cap-config-writer/test/unit/cap-config/package-json.test.ts b/packages/cap-config-writer/test/unit/cap-config/package-json.test.ts deleted file mode 100644 index 7873755f92..0000000000 --- a/packages/cap-config-writer/test/unit/cap-config/package-json.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { satisfiesMinCdsVersion } from '../../../src'; -import { hasMinCdsVersion } from '../../../src/cap-config/package-json'; - -describe('Test hasMinCdsVersion()', () => { - test('CAP project with valid @sap/cds version using caret(^)', async () => { - expect( - hasMinCdsVersion({ - dependencies: { '@sap/cds': '^6.7.0' } - }) - ).toBe(false); - }); - - test('CAP project with invalid @sap/cds version using caret(^)', async () => { - expect( - hasMinCdsVersion({ - dependencies: { '@sap/cds': '^4' } - }) - ).toBe(false); - }); - - test('CAP project with valid @sap/cds version using x-range', async () => { - expect( - hasMinCdsVersion({ - dependencies: { '@sap/cds': '6.x' } - }) - ).toBe(false); - }); - - test('CAP project with invalid @sap/cds version using x-range', async () => { - expect( - hasMinCdsVersion({ - dependencies: { '@sap/cds': '4.x' } - }) - ).toBe(false); - }); - - test('CAP project with valid @sap/cds version using greater than (>)', async () => { - expect( - hasMinCdsVersion({ - dependencies: { '@sap/cds': '>4.0.0' } - }) - ).toBe(false); - }); - - test('CAP project with invalid @sap/cds version containing semver with letters', async () => { - expect( - hasMinCdsVersion({ - dependencies: { '@sap/cds': 'a.b.c' } - }) - ).toBe(false); - }); - - test('CAP project with invalid @sap/cds version containing text', async () => { - expect( - hasMinCdsVersion({ - dependencies: { '@sap/cds': 'test' } - }) - ).toBe(false); - }); - - test('CAP project with valid @sap/cds version using higher version', async () => { - expect( - hasMinCdsVersion({ - dependencies: { '@sap/cds': '6.8.4' } - }) - ).toBe(true); - }); - - test('CAP project with valid @sap/cds version using higher version with caret (^)', async () => { - expect( - hasMinCdsVersion({ - dependencies: { '@sap/cds': '^7' } - }) - ).toBe(true); - }); -}); - -describe('Test satisfiesMinCdsVersion()', () => { - test('CAP project with valid @sap/cds version using caret(^)', async () => { - expect( - satisfiesMinCdsVersion({ - dependencies: { '@sap/cds': '^6.7.0' } - }) - ).toBe(true); - }); - - test('CAP project with invalid @sap/cds version using caret(^)', async () => { - expect( - satisfiesMinCdsVersion({ - dependencies: { '@sap/cds': '^4' } - }) - ).toBe(false); - }); - - test('CAP project with valid @sap/cds version using x-range', async () => { - expect( - satisfiesMinCdsVersion({ - dependencies: { '@sap/cds': '6.x' } - }) - ).toBe(true); - }); - - test('CAP project with invalid @sap/cds version using x-range', async () => { - expect( - satisfiesMinCdsVersion({ - dependencies: { '@sap/cds': '4.x' } - }) - ).toBe(false); - }); - - test('CAP project with valid @sap/cds version using greater than (>)', async () => { - expect( - satisfiesMinCdsVersion({ - dependencies: { '@sap/cds': '>4.0.0' } - }) - ).toBe(true); - }); - - test('CAP project with invalid @sap/cds version containing semver with letters', async () => { - expect( - satisfiesMinCdsVersion({ - dependencies: { '@sap/cds': 'a.b.c' } - }) - ).toBe(false); - }); - - test('CAP project with invalid @sap/cds version containing text', async () => { - expect( - satisfiesMinCdsVersion({ - dependencies: { '@sap/cds': 'test' } - }) - ).toBe(false); - }); - - test('CAP project with valid @sap/cds version using higher version', async () => { - expect( - satisfiesMinCdsVersion({ - dependencies: { '@sap/cds': '6.8.4' } - }) - ).toBe(true); - }); - - test('CAP project with valid @sap/cds version using higher version with caret (^)', async () => { - expect(satisfiesMinCdsVersion({ dependencies: { '@sap/cds': '^7' } })).toBe(true); - }); - - test('CAP project with missing @sap/cds', async () => { - expect(satisfiesMinCdsVersion({ dependencies: {} })).toBe(false); - }); - - test('CAP project with missing dependencies', async () => { - expect(satisfiesMinCdsVersion({})).toBe(false); - }); -}); diff --git a/packages/cap-config-writer/test/unit/cap-writer/package-json.test.ts b/packages/cap-config-writer/test/unit/cap-writer/package-json.test.ts index 4eb70c8af4..e0ec413115 100644 --- a/packages/cap-config-writer/test/unit/cap-writer/package-json.test.ts +++ b/packages/cap-config-writer/test/unit/cap-writer/package-json.test.ts @@ -1,23 +1,17 @@ -import type { CapRuntime } from '../../../src/cap-config/types'; -import { satisfiesMinCdsVersion } from '../../../src/cap-config/package-json'; +import type { CapRuntime, CapServiceCdsInfo } from '../../../src'; import memFs from 'mem-fs'; import { ToolsLogger } from '@sap-ux/logger'; import editor, { type Editor } from 'mem-fs-editor'; import { dirname, join } from 'path'; -import { minCdsVersion } from '../../../src/cap-config'; import { updateRootPackageJson, updateAppPackageJson } from '../../../src/cap-writer/package-json'; import type { Package } from '@sap-ux/project-access'; -import type { CapServiceCdsInfo } from '../../../src/index'; -import { dir } from 'console'; - -jest.mock('../../../src/cap-config/package-json', () => ({ - ...jest.requireActual('../../../src/cap-config/package-json'), - satisfiesMinCdsVersion: jest.fn() -})); +import * as ProjectAccessMock from '@sap-ux/project-access'; jest.mock('@sap-ux/project-access', () => ({ ...jest.requireActual('@sap-ux/project-access'), - getCdsVersionInfo: jest.fn() + getCdsVersionInfo: jest.fn(), + satisfiesMinCdsVersion: jest.fn().mockReturnValue(true), + checkCdsUi5PluginEnabled: jest.fn().mockReturnValue(false) })); describe('Writing/package json files', () => { @@ -60,7 +54,6 @@ describe('Writing/package json files', () => { const packageJsonPath = join(testInputPath, testProjectNameWithSapUx, 'package.json'); const isSapUxEnabled = true; capService.projectPath = join(testInputPath, testProjectNameWithSapUx); - (satisfiesMinCdsVersion as jest.Mock).mockReturnValue(true); await updateRootPackageJson(fs, testProjectNameNoSapUx, isSapUxEnabled, capService, 'test.app.project'); const packageJson = (fs.readJSON(packageJsonPath) ?? {}) as Package; const scripts = packageJson.scripts; @@ -95,6 +88,7 @@ describe('Writing/package json files', () => { ); }); test('should add watch script when workspace is NOT enabled', async () => { + jest.spyOn(ProjectAccessMock, 'checkCdsUi5PluginEnabled').mockResolvedValue(true); const isSapUxEnabled = true; const isNpmWorkspacesEnabled = false; const testProjectWSAlreadyEnabled = 'testprojectwsalreadyenabled'; @@ -118,7 +112,6 @@ describe('Writing/package json files', () => { isNpmWorkspacesEnabled ); const packageJson = (fs.readJSON(packageJsonPath) ?? {}) as Package; - const devDependencies = packageJson.devDependencies; const scripts = packageJson.scripts; expect(scripts?.['watch-testprojectwsalreadyenabled']).toBeDefined(); expect(scripts?.['watch-testprojectwsalreadyenabled']).toEqual( diff --git a/packages/cap-config-writer/test/unit/index.test.ts b/packages/cap-config-writer/test/unit/index.test.ts index 5dfb4f837f..b0a352d1e8 100644 --- a/packages/cap-config-writer/test/unit/index.test.ts +++ b/packages/cap-config-writer/test/unit/index.test.ts @@ -2,7 +2,5 @@ import * as capConfigWriter from '../../src'; test('Smoke test', () => { expect(capConfigWriter).toBeDefined(); - expect(typeof capConfigWriter.checkCdsUi5PluginEnabled).toBe('function'); expect(typeof capConfigWriter.enableCdsUi5Plugin).toBe('function'); - expect(typeof capConfigWriter.satisfiesMinCdsVersion).toBe('function'); }); diff --git a/packages/cards-editor-middleware/test/unit/index.test.ts b/packages/cards-editor-middleware/test/unit/index.test.ts index 06ae9b958b..2182093fa3 100644 --- a/packages/cards-editor-middleware/test/unit/index.test.ts +++ b/packages/cards-editor-middleware/test/unit/index.test.ts @@ -11,6 +11,7 @@ import path from 'path'; import os from 'os'; jest.mock('fs', () => ({ + ...jest.requireActual('fs'), promises: { ...jest.requireActual('fs').promises, writeFile: jest.fn(), diff --git a/packages/project-access/src/constants.ts b/packages/project-access/src/constants.ts index 7e9efebf4a..a4a93ca13b 100644 --- a/packages/project-access/src/constants.ts +++ b/packages/project-access/src/constants.ts @@ -64,3 +64,5 @@ export const fioriToolsDirectory = join(homedir(), FioriToolsSettings.dir); * Directory where modules are cached */ export const moduleCacheRoot = join(fioriToolsDirectory, DirName.ModuleCache); + +export const MinCdsVersionUi5Plugin = '6.8.2'; diff --git a/packages/project-access/src/index.ts b/packages/project-access/src/index.ts index 9b817ff251..ed09cff34d 100644 --- a/packages/project-access/src/index.ts +++ b/packages/project-access/src/index.ts @@ -1,4 +1,4 @@ -export { FileName, DirName, FioriToolsSettings } from './constants'; +export { FileName, DirName, FioriToolsSettings, MinCdsVersionUi5Plugin } from './constants'; export { getFilePaths } from './file'; export { addPackageDevDependency, @@ -47,8 +47,12 @@ export { readUi5Yaml, refreshSpecificationDistTags, toReferenceUri, - updatePackageScript + updatePackageScript, + getWorkspaceInfo, + hasMinCdsVersion, + checkCdsUi5PluginEnabled } from './project'; export { execNpmCommand } from './command/npm-command'; export * from './types'; export * from './library'; +export { hasDependency } from './project'; diff --git a/packages/project-access/src/project/cap.ts b/packages/project-access/src/project/cap.ts index 64f3addef5..3268fe966a 100644 --- a/packages/project-access/src/project/cap.ts +++ b/packages/project-access/src/project/cap.ts @@ -2,7 +2,7 @@ import { spawn } from 'child_process'; 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'; +import { FileName, MinCdsVersionUi5Plugin } from '../constants'; import type { CapCustomPaths, CapProjectType, @@ -12,7 +12,8 @@ import type { Package, ServiceDefinitions, ServiceInfo, - CdsVersionInfo + CdsVersionInfo, + CdsUi5PluginInfo } from '../types'; import { deleteDirectory, @@ -26,6 +27,10 @@ import { } from '../file'; import { loadModuleFromProject } from './module-loader'; import { findCapProjectRoot } from './search'; +import { coerce, gte, satisfies } from 'semver'; +import { create as createStorage } from 'mem-fs'; +import { create } from 'mem-fs-editor'; +import { hasDependency } from './dependencies'; interface CdsFacade { env: { for: (mode: string, path: string) => CdsEnvironment }; @@ -832,3 +837,144 @@ export async function deleteCapApp(appPath: string, memFs?: Editor, logger?: Log await deleteDirectory(dirname(appPath), memFs); } } + +/** + * Check if cds-plugin-ui5 is enabled on a CAP project. Checks also all prerequisites, like minimum @sap/cds version. + * Overloaded function that returns detailed CAP plugin info. + * + * @param basePath - root path of the CAP project, where package.json is located + * @param [fs] - optional: the memfs editor instance + * @returns true: cds-plugin-ui5 and all prerequisites are fulfilled; false: cds-plugin-ui5 is not enabled or not all prerequisites are fulfilled + */ +export async function checkCdsUi5PluginEnabled(basePath: string, fs?: Editor): Promise; + +/** + * 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 + * @returns false if package.json is not found at specified path or {@link CdsUi5PluginInfo} with additional info + */ +export async function checkCdsUi5PluginEnabled( + basePath: string, + fs?: Editor, + moreInfo?: boolean +): Promise; + +/** + * 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 + */ +export async function checkCdsUi5PluginEnabled( + basePath: string, + fs?: Editor, + moreInfo?: boolean, + cdsVersionInfo?: CdsVersionInfo +): Promise; + +/** + * 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: string, + fs?: Editor, + moreInfo?: boolean, + cdsVersionInfo?: CdsVersionInfo +): Promise { + if (!fs) { + fs = create(createStorage()); + } + const packageJsonPath = join(basePath, 'package.json'); + if (!fs.exists(packageJsonPath)) { + return false; + } + const packageJson = fs.readJSON(packageJsonPath) as Package; + const { workspaceEnabled } = await getWorkspaceInfo(basePath, packageJson); + const cdsInfo: CdsUi5PluginInfo = { + // 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, `>=${MinCdsVersionUi5Plugin}`) + : 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: string, + packageJson: Package +): Promise<{ appWorkspace: string; workspaceEnabled: boolean; workspacePackages: string[] }> { + 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: Package): string[] | undefined { + let workspacePackages: string[] | undefined; + 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: Package): boolean { + return ( + hasMinCdsVersion(packageJson) || + satisfies(MinCdsVersionUi5Plugin, 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: Package): boolean { + return gte(coerce(packageJson.dependencies?.['@sap/cds']) ?? '0.0.0', MinCdsVersionUi5Plugin); +} diff --git a/packages/project-access/src/project/index.ts b/packages/project-access/src/project/index.ts index b396d2ff58..db92ae9a51 100644 --- a/packages/project-access/src/project/index.ts +++ b/packages/project-access/src/project/index.ts @@ -13,10 +13,13 @@ export { isCapJavaProject, isCapNodeJsProject, readCapServiceMetadataEdmx, - toReferenceUri + toReferenceUri, + getWorkspaceInfo, + hasMinCdsVersion, + checkCdsUi5PluginEnabled } from './cap'; export { filterDataSourcesByType } from './service'; -export { addPackageDevDependency, getNodeModulesPath } from './dependencies'; +export { addPackageDevDependency, getNodeModulesPath, hasDependency } from './dependencies'; export { getCapI18nFolderNames, getI18nPropertiesPaths, getI18nBundles } from './i18n'; export { getAppProgrammingLanguage, diff --git a/packages/project-access/src/types/cap/index.ts b/packages/project-access/src/types/cap/index.ts index dc357c05c1..395907687c 100644 --- a/packages/project-access/src/types/cap/index.ts +++ b/packages/project-access/src/types/cap/index.ts @@ -235,3 +235,22 @@ export interface CdsVersionInfo { version: string; root: string; } + +export type CdsUi5PluginInfo = { + /** + * Convenience property. The CDS UI5 plugin is considered enabled if `hasCdsUi5Plugin`, `hasMinCdsVersion`, `isWorkspaceEnabled` are all true. + */ + isCdsUi5PluginEnabled: boolean; + /** + * True if the CDS version satisfies the minimum supported CDS version + */ + hasMinCdsVersion: boolean; + /** + * True if NPM workspaces are enabled at the root of a CAP project + */ + isWorkspaceEnabled: boolean; + /** + * True if the CDS ui5 plugin is specified as a dependency + */ + hasCdsUi5Plugin: boolean; +}; diff --git a/packages/project-access/test/fixture/cap-no-cds-plugin-ui/package.json b/packages/project-access/test/fixture/cap-no-cds-plugin-ui/package.json new file mode 100644 index 0000000000..b15b9a1948 --- /dev/null +++ b/packages/project-access/test/fixture/cap-no-cds-plugin-ui/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@sap/cds": "6.8.0" + } +} diff --git a/packages/project-access/test/fixture/cap-valid-cds-plugin-ui/package.json b/packages/project-access/test/fixture/cap-valid-cds-plugin-ui/package.json new file mode 100644 index 0000000000..3c6fb1785e --- /dev/null +++ b/packages/project-access/test/fixture/cap-valid-cds-plugin-ui/package.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "@sap/cds": "^6.8.2" + }, + "devDependencies": { + "cds-plugin-ui5": "0.9.3" + }, + "workspaces": [ + "app/*" + ] +} diff --git a/packages/project-access/test/project/cap.test.ts b/packages/project-access/test/project/cap.test.ts index 924f78a383..5071805244 100644 --- a/packages/project-access/test/project/cap.test.ts +++ b/packages/project-access/test/project/cap.test.ts @@ -5,7 +5,14 @@ import { create, type Editor } from 'mem-fs-editor'; import * as projectModuleMock from '../../src/project/module-loader'; import type { Package } from '../../src'; import { FileName } from '../../src/constants'; -import { clearCdsModuleCache, clearGlobalCdsModulePromiseCache, getCapServiceName } from '../../src/project/cap'; +import { + clearCdsModuleCache, + clearGlobalCdsModulePromiseCache, + getCapServiceName, + checkCdsUi5PluginEnabled, + satisfiesMinCdsVersion, + hasMinCdsVersion +} from '../../src/project/cap'; import { getCapCustomPaths, getCapEnvironment, @@ -1390,6 +1397,250 @@ describe('deleteCapApp', () => { }); }); +const fixturesPath = join(__dirname, '../fixture'); + +describe('Test checkCdsUi5PluginEnabled()', () => { + test('Empty project should return false', async () => { + expect(await checkCdsUi5PluginEnabled(__dirname)).toBe(false); + expect(await checkCdsUi5PluginEnabled(__dirname, undefined, true)).toBe(false); + }); + + test('CAP project with valid cds-plugin-ui', async () => { + expect(await checkCdsUi5PluginEnabled(join(fixturesPath, 'cap-valid-cds-plugin-ui'))).toBe(true); + expect(await checkCdsUi5PluginEnabled(join(fixturesPath, 'cap-valid-cds-plugin-ui'), undefined, true)).toEqual({ + hasCdsUi5Plugin: true, + hasMinCdsVersion: true, + isCdsUi5PluginEnabled: true, + isWorkspaceEnabled: true + }); + }); + + test('CAP project with missing apps folder in workspaces', async () => { + const memFs = create(createStorage()); + memFs.writeJSON(join(__dirname, 'package.json'), { + dependencies: { '@sap/cds': '6.8.2' }, + devDependencies: { 'cds-plugin-ui5': '0.0.1' }, + workspaces: [] + }); + expect(await checkCdsUi5PluginEnabled(__dirname, memFs)).toBe(false); + expect(await checkCdsUi5PluginEnabled(__dirname, memFs, true)).toEqual({ + hasCdsUi5Plugin: true, + hasMinCdsVersion: true, + isCdsUi5PluginEnabled: false, + isWorkspaceEnabled: false + }); + }); + + test('CAP project with workspaces config as object, but no apps folder', async () => { + const memFs = create(createStorage()); + memFs.writeJSON(join(__dirname, 'package.json'), { + dependencies: { '@sap/cds': '6.8.2' }, + devDependencies: { 'cds-plugin-ui5': '0.0.1' }, + workspaces: {} + }); + expect(await checkCdsUi5PluginEnabled(__dirname, memFs)).toBe(false); + expect(await checkCdsUi5PluginEnabled(__dirname, memFs, true)).toEqual({ + hasCdsUi5Plugin: true, + hasMinCdsVersion: true, + isCdsUi5PluginEnabled: false, + isWorkspaceEnabled: false + }); + }); + + test('CAP project with workspaces config as object, app folder in workspace', async () => { + const memFs = create(createStorage()); + memFs.writeJSON(join(__dirname, 'package.json'), { + dependencies: { '@sap/cds': '6.8.2' }, + devDependencies: { 'cds-plugin-ui5': '0.0.1' }, + workspaces: { + packages: ['app/*'] + } + }); + expect(await checkCdsUi5PluginEnabled(__dirname, memFs)).toBe(true); + expect(await checkCdsUi5PluginEnabled(__dirname, memFs, true)).toEqual({ + hasCdsUi5Plugin: true, + hasMinCdsVersion: true, + isCdsUi5PluginEnabled: true, + isWorkspaceEnabled: true + }); + }); + + test('CAP project with cds version info greater than minimum cds requirement', async () => { + const memFs = create(createStorage()); + memFs.writeJSON(join(__dirname, 'package.json'), { + dependencies: { '@sap/cds': '6.8.2' }, + devDependencies: { 'cds-plugin-ui5': '0.0.1' }, + workspaces: { + packages: ['app/*'] + } + }); + const cdsVersionInfo = { + home: '/path', + version: '7.7.2', + root: '/path/root' + }; + expect(await checkCdsUi5PluginEnabled(__dirname, memFs)).toBe(true); + expect(await checkCdsUi5PluginEnabled(__dirname, memFs, true, cdsVersionInfo)).toEqual({ + hasCdsUi5Plugin: true, + hasMinCdsVersion: true, + isCdsUi5PluginEnabled: true, + isWorkspaceEnabled: true + }); + }); +}); + +describe('Test satisfiesMinCdsVersion()', () => { + test('CAP project with valid @sap/cds version using caret(^)', async () => { + expect( + satisfiesMinCdsVersion({ + dependencies: { '@sap/cds': '^6.7.0' } + }) + ).toBe(true); + }); + + test('CAP project with invalid @sap/cds version using caret(^)', async () => { + expect( + satisfiesMinCdsVersion({ + dependencies: { '@sap/cds': '^4' } + }) + ).toBe(false); + }); + + test('CAP project with valid @sap/cds version using x-range', async () => { + expect( + satisfiesMinCdsVersion({ + dependencies: { '@sap/cds': '6.x' } + }) + ).toBe(true); + }); + + test('CAP project with invalid @sap/cds version using x-range', async () => { + expect( + satisfiesMinCdsVersion({ + dependencies: { '@sap/cds': '4.x' } + }) + ).toBe(false); + }); + + test('CAP project with valid @sap/cds version using greater than (>)', async () => { + expect( + satisfiesMinCdsVersion({ + dependencies: { '@sap/cds': '>4.0.0' } + }) + ).toBe(true); + }); + + test('CAP project with invalid @sap/cds version containing semver with letters', async () => { + expect( + satisfiesMinCdsVersion({ + dependencies: { '@sap/cds': 'a.b.c' } + }) + ).toBe(false); + }); + + test('CAP project with invalid @sap/cds version containing text', async () => { + expect( + satisfiesMinCdsVersion({ + dependencies: { '@sap/cds': 'test' } + }) + ).toBe(false); + }); + + test('CAP project with valid @sap/cds version using higher version', async () => { + expect( + satisfiesMinCdsVersion({ + dependencies: { '@sap/cds': '6.8.4' } + }) + ).toBe(true); + }); + + test('CAP project with valid @sap/cds version using higher version with caret (^)', async () => { + expect(satisfiesMinCdsVersion({ dependencies: { '@sap/cds': '^7' } })).toBe(true); + }); + + test('CAP project with missing @sap/cds', async () => { + expect(satisfiesMinCdsVersion({ dependencies: {} })).toBe(false); + }); + + test('CAP project with missing dependencies', async () => { + expect(satisfiesMinCdsVersion({})).toBe(false); + }); +}); + +describe('Test hasMinCdsVersion()', () => { + test('CAP project with valid @sap/cds version using caret(^)', async () => { + expect( + hasMinCdsVersion({ + dependencies: { '@sap/cds': '^6.7.0' } + }) + ).toBe(false); + }); + + test('CAP project with invalid @sap/cds version using caret(^)', async () => { + expect( + hasMinCdsVersion({ + dependencies: { '@sap/cds': '^4' } + }) + ).toBe(false); + }); + + test('CAP project with valid @sap/cds version using x-range', async () => { + expect( + hasMinCdsVersion({ + dependencies: { '@sap/cds': '6.x' } + }) + ).toBe(false); + }); + + test('CAP project with invalid @sap/cds version using x-range', async () => { + expect( + hasMinCdsVersion({ + dependencies: { '@sap/cds': '4.x' } + }) + ).toBe(false); + }); + + test('CAP project with valid @sap/cds version using greater than (>)', async () => { + expect( + hasMinCdsVersion({ + dependencies: { '@sap/cds': '>4.0.0' } + }) + ).toBe(false); + }); + + test('CAP project with invalid @sap/cds version containing semver with letters', async () => { + expect( + hasMinCdsVersion({ + dependencies: { '@sap/cds': 'a.b.c' } + }) + ).toBe(false); + }); + + test('CAP project with invalid @sap/cds version containing text', async () => { + expect( + hasMinCdsVersion({ + dependencies: { '@sap/cds': 'test' } + }) + ).toBe(false); + }); + + test('CAP project with valid @sap/cds version using higher version', async () => { + expect( + hasMinCdsVersion({ + dependencies: { '@sap/cds': '6.8.4' } + }) + ).toBe(true); + }); + + test('CAP project with valid @sap/cds version using higher version with caret (^)', async () => { + expect( + hasMinCdsVersion({ + dependencies: { '@sap/cds': '^7' } + }) + ).toBe(true); + }); +}); + function fail(message: string) { expect(message).toBeFalsy(); } diff --git a/packages/ui5-application-inquirer/src/index.ts b/packages/ui5-application-inquirer/src/index.ts index 266416241a..c3275cbba7 100644 --- a/packages/ui5-application-inquirer/src/index.ts +++ b/packages/ui5-application-inquirer/src/index.ts @@ -1,4 +1,4 @@ -import { type CdsUi5PluginInfo } from '@sap-ux/cap-config-writer'; +import { type CdsUi5PluginInfo } from '@sap-ux/project-access'; import type { InquirerAdapter, PromptDefaultValue } from '@sap-ux/inquirer-common'; import { getDefaultUI5Theme, getUI5Versions, type UI5VersionFilterOptions } from '@sap-ux/ui5-info'; import autocomplete from 'inquirer-autocomplete-prompt'; diff --git a/packages/ui5-application-inquirer/src/prompts/prompts.ts b/packages/ui5-application-inquirer/src/prompts/prompts.ts index 83671d14b6..0fae8ce360 100644 --- a/packages/ui5-application-inquirer/src/prompts/prompts.ts +++ b/packages/ui5-application-inquirer/src/prompts/prompts.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ // Nullish coalescing operator lint warnings disabled as its not appropriate in most cases where empty strings are not considered valid -import { type CdsUi5PluginInfo } from '@sap-ux/cap-config-writer'; +import { type CdsUi5PluginInfo } from '@sap-ux/project-access'; import { getUI5ThemesChoices, searchChoices, diff --git a/packages/ui5-application-inquirer/src/types.ts b/packages/ui5-application-inquirer/src/types.ts index b8a41ce426..3aa39f2cfd 100644 --- a/packages/ui5-application-inquirer/src/types.ts +++ b/packages/ui5-application-inquirer/src/types.ts @@ -1,4 +1,4 @@ -import type { CdsUi5PluginInfo } from '@sap-ux/cap-config-writer'; +import type { CdsUi5PluginInfo } from '@sap-ux/project-access'; import type { CommonPromptOptions, PromptDefaultValue, UI5VersionChoice, YUIQuestion } from '@sap-ux/inquirer-common'; import type { AutocompleteQuestionOptions } from 'inquirer-autocomplete-prompt';