Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2821/add generate cap mta #2822

Merged
merged 13 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/soft-carrots-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ux/cf-deploy-config-writer': minor
---

append new logic to create CAP mta
73 changes: 66 additions & 7 deletions packages/cf-deploy-config-writer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ Yarn
Pnpm
`pnpm add @sap-ux/cf-deploy-config-writer`

## Usage
Calling the `MtaConfig` library to add different types of modules, for example HTML5 resources, routing module and mta extension configurations to an existing MTA configuration file. Dependent on the [MTA Tool](https://www.npmjs.com/package/mta) for exploring and validating the changes being made.
## Example: Reading and writing MTA Configurations
Generate an `MtaConfig` instance to read an `mta.yaml` configuration to allow you append different resources and modules. For example HTML5 resources, routing modules or append mta extension configurations.

Dependent on the [MTA Tool](https://www.npmjs.com/package/mta) being installed globally on your dev space.
- For VSCode, run the command `npm i -g mta`
- For Business Application Studio, the binary is already installed

```Typescript
import { MtaConfig } from '@sap-ux/cf-deploy-config-writer';
// Create a new instance of MtaConfig
Expand All @@ -38,17 +43,41 @@ await mtaConfig.addMtaExtensionConfig('mynewdestination', 'https://my-service-ur
await mtaConfig.save();
```

## Example: Appending a Managed Approuter Configuration to an SAP Fiori UI5 Application

Calling the `generateAppConfig` function to append Cloud Foundry configuration to a HTML5 application, assumes `manifest.json` and `ui5.yaml` configurations are present otherwise the process will exit with an error;
```Typescript
import { generateAppConfig, DefaultMTADestination } from '@sap-ux/cf-deploy-config-writer'
import { join } from 'path';

const exampleWriter = async () => {
const appPath = join(__dirname, 'testapp');
const ui5AppPath = join(__dirname, 'testapp');
// Option 1. Append managed approuter configuration, toggle `addManagedAppRouter` to false to ommit the managed approuter configuration being appended to the mta.yaml
const fs = await generateAppConfig({appPath, addManagedAppRouter: true, destinationName: 'SAPBTPDestination'});
const fs = await generateAppConfig({appPath: ui5AppPath, addManagedAppRouter: true, destinationName: 'SAPBTPDestination'});
return new Promise((resolve) => {
fs.commit(resolve); // When using with Yeoman it handle the fs commit.
});
}
// Calling the function
await exampleWriter();
```

## Example: Generate or Append a `Managed` Approuter Configuration to a SAP Fiori UI5 Application

Calling the `generateAppConfig` function to append Cloud Foundry configuration to a HTML5 application;
- Assumes `manifest.json` and `ui5.yaml` configurations are present in the target folder
- Supports `CAP` projects where an existing `mta.yaml` is already present and you are adding a SAP Fiori UI5 app to it

```Typescript
import { generateAppConfig, DefaultMTADestination } from '@sap-ux/cf-deploy-config-writer'
import { join } from 'path';

const exampleWriter = async () => {
const ui5AppPath = join(__dirname, 'testapp');
// Option 1. Append managed approuter configuration, toggle `addManagedAppRouter` to false to ommit the managed approuter configuration being appended to the mta.yaml
const fs = await generateAppConfig({appPath: ui5AppPath, addManagedAppRouter: true, destinationName: 'SAPBTPDestination'});
// Option 2. For CAP flows, set the destination to DefaultMTADestination to create a destination instance between the HTML5 app and CAP Project
const fs = await generateAppConfig({appPath, addManagedAppRouter: true, destinationName: DefaultMTADestination});
const fs = await generateAppConfig({appPath: ui5AppPath, addManagedAppRouter: true, destinationName: DefaultMTADestination});
return new Promise((resolve) => {
fs.commit(resolve); // When using with Yeoman it handle the fs commit.
});
Expand All @@ -57,15 +86,43 @@ const exampleWriter = async () => {
await exampleWriter();
```

## Example: Generate a Base `Managed` | `Standalone` Approuter Configuration

Calling the `generateBaseConfig` function to generate a `new` Cloud Foundry configuration, supporting `managed` | `standalone` configurations;
- Creates a new `mta.yaml` into the specified `mtaPath`, it will fail if an existing `mta.yaml` is found
- Optional parameters include adding a `connectivity` service if the SAP BTP destination is using an `OnPremise` configuration
- New configuration will include a destination instance to expose a `UI5` endpoint, consumed by SAP Fiori applications when deployed to Cloud Foundry
```Typescript
import { generateBaseConfig, RouterModuleType } from '@sap-ux/cf-deploy-config-writer'
import { join } from 'path';

const exampleWriter = async () => {
const mtaPath = join(__dirname, 'testapp');
const mtaPath = join(__dirname, 'testproject');
// If your SAPUI5 application will be consuming an SAB BTP OnPremise destination, Connectivity serivce is required; Refer to https://discovery-center.cloud.sap/serviceCatalog/connectivity-service?region=all
const addConnectivityService = true;
// Generate a managed approuter configuration, toggle the routerType to RouterModuleType.Standard for a standalone configuration
const fs = await generateBaseConfig({ mtaId: 'myapp', routerType: RouterModuleType.Managed, mtaPath });
const fs = await generateBaseConfig({ mtaId: 'mymtaproject', routerType: RouterModuleType.Managed, mtaPath, addConnectivityService });
return new Promise((resolve) => {
fs.commit(resolve); // When using with Yeoman it handle the fs commit.
});
}
// Calling the function
await exampleWriter();
```

## Example: Generate a CAP `Managed` | `Standalone` Approuter Configuration
Calling the `generateCAPConfig` function to generate a `new` Cloud Foundry configuration, supporting `managed` | `standalone` configurations;
- Generate a CAP `mta.yaml` with `destination`, `HTML5-Repo` and `XSUAA` services added by default
- New configuration will include destination instances to expose `UI5` and `CAP` endpoints, consumed by SAP Fiori applications when deployed to Cloud Foundry

```Typescript
import { generateCAPConfig, RouterModuleType } from '@sap-ux/cf-deploy-config-writer'
import { join } from 'path';

const exampleWriter = async () => {
const mtaPath = join(__dirname, 'testcapproject');
// Generate a managed approuter configuration, toggle the routerType to RouterModuleType.Standard or RouterModuleType.Managed
const fs = await generateCAPConfig({ mtaId: 'mymtaproject', routerType: RouterModuleType.Managed, mtaPath });
return new Promise((resolve) => {
fs.commit(resolve); // When using with Yeoman it handle the fs commit.
});
Expand All @@ -81,4 +138,6 @@ SAP Deployment
Cloud Foundry
MTA
Multi-Target Application
CAP
CDS

129 changes: 58 additions & 71 deletions packages/cf-deploy-config-writer/src/cf-writer/app-config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { dirname, join, relative } from 'path';
import { spawnSync } from 'child_process';
import { create as createStorage } from 'mem-fs';
import { create, type Editor } from 'mem-fs-editor';
import { sync } from 'hasbin';
import { type FioriToolsProxyConfig, type UI5Config, UI5Config as UI5ConfigInstance } from '@sap-ux/ui5-config';
import {
addPackageDevDependency,
Expand All @@ -16,39 +14,35 @@ import {
import { Authentication } from '@sap-ux/btp-utils';
import {
appDeployMTAScript,
CDSAddMtaParams,
CDSBinNotFound,
CDSExecutable,
DefaultMTADestination,
EmptyDestination,
MbtPackage,
MbtPackageVersion,
MTABinNotFound,
MTABuildScript,
MTAExecutable,
MTAFileExtension,
ResourceMTADestination,
Rimraf,
RimrafVersion,
rootDeployMTAScript,
UI5DeployBuildScript,
undeployMTAScript,
XSAppFile,
XSSecurityFile
WelcomeFile,
XSAppFile
} from '../constants';
import {
addCommonPackageDependencies,
addGitIgnore,
addRootPackage,
addXSSecurityConfig,
generateSupportingConfig,
getDestinationProperties,
getTemplatePath,
readManifest,
toPosixPath
} from '../utils';
import {
addMtaDeployParameters,
createCAPMTA,
createMTA,
doesCDSBinaryExist,
doesMTABinaryExist,
getMtaConfig,
getMtaId,
type MtaConfig,
Expand All @@ -57,7 +51,7 @@ import {
import LoggerHelper from '../logger-helper';
import { t } from '../i18n';
import { type Logger } from '@sap-ux/logger';
import { ApiHubType, type CFAppConfig, type CFConfig, type MTABaseConfig } from '../types';
import { type XSAppDocument, ApiHubType, type CFAppConfig, type CFConfig, type MTABaseConfig } from '../types';

/**
* Add a managed approuter configuration to an existing HTML5 application.
Expand All @@ -74,22 +68,11 @@ export async function generateAppConfig(cfAppConfig: CFAppConfig, fs?: Editor, l
if (logger) {
LoggerHelper.logger = logger;
}
validateMtaConfig();
doesMTABinaryExist();
await generateDeployConfig(cfAppConfig, fs);
return fs;
}

/**
* Validate the conditions to allow deployment configuration to be added.
*
*/
function validateMtaConfig(): void {
// CF writer is dependent on the mta-lib library, which in turn relies on the mta executable being installed and available in the path
if (!sync(MTAExecutable)) {
throw new Error(MTABinNotFound);
}
}

/**
* Returns the updated configuration for the given HTML5 app, after reading all the required files.
*
Expand All @@ -105,6 +88,11 @@ async function getUpdatedConfig(cfAppConfig: CFAppConfig, fs: Editor): Promise<C
fs
);
const { servicePath, firstServicePathSegment, appId } = await processManifest(cfAppConfig.appPath, fs);

if (!appId) {
throw new Error('No SAP Fiori UI5 application found.');
}

const { destinationIsFullUrl, destinationAuthentication } = await getDestinationProperties(
cfAppConfig.destinationName ?? destination
);
Expand All @@ -130,7 +118,6 @@ async function getUpdatedConfig(cfAppConfig: CFAppConfig, fs: Editor): Promise<C
appId,
capRoot
} as CFConfig;
LoggerHelper.logger?.debug(`CF Config loaded: ${JSON.stringify(config, null, 2)}`);
return config;
}

Expand Down Expand Up @@ -160,10 +147,7 @@ async function getProjectProperties(config: CFAppConfig): Promise<{
const hasRoot = foundMtaPath?.hasRoot ?? false;
const capRoot = (await findCapProjectRoot(config.appPath)) ?? undefined;
if (capRoot) {
// CDS executable is required for CAP projects as the mta.yaml file is generated by the cds deploy command
if (!sync(CDSExecutable)) {
throw new Error(CDSBinNotFound);
}
doesCDSBinaryExist();
isCap = true;
rootPath = capRoot;
} else {
Expand Down Expand Up @@ -232,34 +216,32 @@ async function processManifest(
* @param fs reference to a mem-fs editor
*/
async function generateDeployConfig(cfAppConfig: CFAppConfig, fs: Editor): Promise<void> {
const cfConfig = await getUpdatedConfig(cfAppConfig, fs);
// Generate MTA Config, LCAP will generate the mta.yaml on the fly so we dont care about it!
if (!cfConfig.lcapMode) {
createMTAConfig(cfConfig);
await generateSupportingConfig(cfConfig, fs);
await updateMtaConfig(cfConfig);
const config = await getUpdatedConfig(cfAppConfig, fs);

LoggerHelper?.logger?.debug(`Generate app configuration using: \n ${JSON.stringify(config)}`);

// Generate MTA Config, LCAP will generate the mta.yaml on the fly so we don't care about it!
if (!config.lcapMode) {
generateMTAFile(config);
await generateSupportingConfig(config, fs);
await updateMtaConfig(config, fs);
}
// Generate HTML5 config
await appendCloudFoundryConfigurations(cfConfig, fs);
await updateManifest(cfConfig, fs);
await updateHTML5AppPackage(cfConfig, fs);
await updateRootPackage(cfConfig, fs);
await appendCloudFoundryConfigurations(config, fs);
await updateManifest(config, fs);
await updateHTML5AppPackage(config, fs);
await updateRootPackage(config, fs);
}

/**
* Creates the MTA configuration file.
*
* @param cfConfig writer configuration
*/
function createMTAConfig(cfConfig: CFConfig): void {
export function generateMTAFile(cfConfig: CFConfig): void {
if (!cfConfig.mtaId) {
if (cfConfig.isCap) {
const result = spawnSync(CDSExecutable, CDSAddMtaParams, {
cwd: cfConfig.rootPath
});
if (result.error) {
throw new Error(CDSBinNotFound);
}
createCAPMTA(cfConfig.rootPath);
} else {
createMTA({ mtaId: cfConfig.appId, mtaPath: cfConfig.mtaPath ?? cfConfig.rootPath } as MTABaseConfig);
}
Expand All @@ -268,34 +250,13 @@ function createMTAConfig(cfConfig: CFConfig): void {
}
}

/**
* Generate CF specific configurations to support deployment and undeployment.
*
* @param config writer configuration
* @param fs reference to a mem-fs editor
*/
async function generateSupportingConfig(config: CFConfig, fs: Editor): Promise<void> {
const mtaId: string | undefined = config.mtaId ?? (await getMtaId(config.rootPath));
// Add specific MTA ID configurations
const mtaConfig = { mtaId: mtaId ?? config.appId, mtaPath: config.rootPath } as MTABaseConfig;
if (mtaId && !fs.exists(join(config.rootPath, 'package.json'))) {
addRootPackage(mtaConfig, fs);
}
if (config.addManagedAppRouter && !fs.exists(join(config.rootPath, XSSecurityFile))) {
addXSSecurityConfig(mtaConfig, fs);
}
// Be a good developer and add a .gitignore if missing from the existing project root
if (!fs.exists(join(config.rootPath, '.gitignore'))) {
addGitIgnore(config.rootPath, fs);
}
}

/**
* Updates the MTA configuration file.
*
* @param cfConfig writer configuration
* @param fs reference to a mem-fs editor
*/
async function updateMtaConfig(cfConfig: CFConfig): Promise<void> {
async function updateMtaConfig(cfConfig: CFConfig, fs: Editor): Promise<void> {
const mtaInstance = await getMtaConfig(cfConfig.rootPath);
if (mtaInstance) {
await mtaInstance.addRoutingModules({ isManagedApp: cfConfig.addManagedAppRouter });
Expand All @@ -315,11 +276,37 @@ async function updateMtaConfig(cfConfig: CFConfig): Promise<void> {
cfConfig.destinationAuthentication = Authentication.NO_AUTHENTICATION;
}
}
cleanupStandaloneRoutes(cfConfig, mtaInstance, fs);
await saveMta(cfConfig, mtaInstance);
cfConfig.cloudServiceName = mtaInstance.cloudServiceName;
}
}

/**
*
* @param root0
* @param root0.rootPath
* @param root0.appId
* @param mtaInstance
* @param fs
*/
function cleanupStandaloneRoutes({ rootPath, appId }: CFConfig, mtaInstance: MtaConfig, fs: Editor): void {
// Cleanup standalone xs-app.json to reflect new application
const appRouterPath = mtaInstance.standaloneRouterPath;
if (appRouterPath) {
try {
const xsAppPath = join(appRouterPath, XSAppFile);
const appRouterXsAppObj = fs.readJSON(join(rootPath, xsAppPath)) as unknown as XSAppDocument;
if ((appRouterXsAppObj && !appRouterXsAppObj?.[WelcomeFile]) || appRouterXsAppObj?.[WelcomeFile] === '/') {
appRouterXsAppObj[WelcomeFile] = `/${appId}`;
fs.writeJSON(join(rootPath, xsAppPath), appRouterXsAppObj);
}
} catch (error) {
LoggerHelper.logger?.error(t('error.cannotUpdateRouterXSApp', { error }));
}
}
}

/**
* Apply changes to mta.yaml.
*
Expand Down Expand Up @@ -420,7 +407,7 @@ async function updateHTML5AppPackage(cfConfig: CFConfig, fs: Editor): Promise<vo
* @param fs reference to a mem-fs editor
*/
async function updateRootPackage(cfConfig: CFConfig, fs: Editor): Promise<void> {
const packageExists = fs.exists(join(cfConfig.rootPath, 'package.json'));
const packageExists = fs.exists(join(cfConfig.rootPath, FileName.Package));
// Append mta scripts only if mta.yaml is at a different level to the HTML5 app
if (cfConfig.isMtaRoot && packageExists) {
await addPackageDevDependency(cfConfig.rootPath, Rimraf, RimrafVersion, fs);
Expand Down
Loading
Loading