Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9ac353f
feat: add middleware and adjust new project structure to support it
nikmace Oct 31, 2025
c1c7c01
feat: adjust preview-middleware
nikmace Oct 31, 2025
b0ee3e7
refactor: oauth middleware
nikmace Oct 31, 2025
1b76c47
chore: remove changelog
nikmace Oct 31, 2025
1a192ce
chore: improve readme
nikmace Oct 31, 2025
3777af5
refactor: improve code
nikmace Nov 3, 2025
c88b9a8
feat: write odata paths dynamically from xs-app.json
nikmace Nov 3, 2025
52f70e2
refactor: jsdocs
nikmace Nov 3, 2025
43194fe
Merge remote-tracking branch 'origin/main' into feat/3789/cf-adp-support
nikmace Nov 13, 2025
cb04ce0
refactor: cf interfaces
nikmace Nov 17, 2025
06f2ad9
feat: add proxy for odata
nikmace Nov 20, 2025
fd5a255
refactor: improve code, change structure
nikmace Nov 21, 2025
7b677b5
feat: add check for logged in cf
nikmace Nov 25, 2025
0a74145
fix: add async for is logged in cf
nikmace Nov 25, 2025
51fe719
refactor: rename middleware
nikmace Nov 25, 2025
f903d1a
refactor: delete artifact from renaming
nikmace Nov 25, 2025
5e509b3
refactor: rename middleware ui5 configuration
nikmace Nov 25, 2025
df3625d
test: fix existing tests
nikmace Nov 25, 2025
84c8e54
test: add middleware tests
nikmace Nov 26, 2025
2aafca4
fix: requested changes
nikmace Nov 26, 2025
f372af1
chore: add new package to sonar properties
nikmace Nov 26, 2025
4bcb65b
refactor: move initAdp inside flp sandbox class
nikmace Nov 26, 2025
be35366
fix: sonar errors
nikmace Nov 26, 2025
ac0ed36
test: add discovery tests
nikmace Nov 26, 2025
b510d2e
test: add missing test case
nikmace Nov 27, 2025
c0e432c
fix: sonar issue with replaceAll
nikmace Nov 27, 2025
b61e748
test: add missing test case
nikmace Nov 27, 2025
8a156d6
fix: review comments
nikmace Nov 27, 2025
70b2650
test: add missing test cases
nikmace Nov 27, 2025
53fd2d6
refactor: split methods
nikmace Nov 27, 2025
fa1acbf
refactor: move code and add missing test cases
nikmace Nov 28, 2025
587e21e
feat: prevent concurrent token fetches
nikmace Nov 28, 2025
f603964
Merge branch 'main' into feat/3789/cf-adp-support
voicis Nov 28, 2025
35d4e1a
chore: update lock file
nikmace Nov 28, 2025
7acf4b9
chore: add cset
nikmace Nov 28, 2025
80d5e38
chore: update preview-middleware readme with useLocal property
nikmace Nov 28, 2025
a05b3b4
Merge branch 'main' into feat/3789/cf-adp-support
nikmace Nov 28, 2025
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
8 changes: 8 additions & 0 deletions .changeset/slimy-toes-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sap-ux/backend-proxy-middleware-cf': patch
'@sap-ux/preview-middleware': patch
'@sap-ux/generator-adp': patch
'@sap-ux/adp-tooling': patch
---

feat: Enable Adaptation Editor for CF projects
56 changes: 54 additions & 2 deletions packages/adp-tooling/src/base/helper.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Editor } from 'mem-fs-editor';
import type { ReaderCollection } from '@ui5/fs';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join, isAbsolute, relative, basename, dirname } from 'node:path';

import type { UI5Config } from '@sap-ux/ui5-config';
import type { InboundContent, Inbound } from '@sap-ux/axios-extension';
import { getWebappPath, FileName, readUi5Yaml, type ManifestNamespace } from '@sap-ux/project-access';
import { getWebappPath, FileName, readUi5Yaml, type ManifestNamespace, type Manifest } from '@sap-ux/project-access';

import type { DescriptorVariant, AdpPreviewConfig } from '../types';
import type { DescriptorVariant, AdpPreviewConfig, UI5YamlCustomTaskConfiguration } from '../types';

/**
* Get the app descriptor variant.
Expand Down Expand Up @@ -86,6 +87,57 @@ export function extractAdpConfig(ui5Conf: UI5Config): AdpPreviewConfig | undefin
return customMiddleware?.configuration?.adp;
}

/**
* Extracts the CF build task from the UI5 configuration.
*
* @param {UI5Config} ui5Conf - The UI5 configuration.
* @returns {UI5YamlCustomTaskConfiguration} The CF build task.
*/
export function extractCfBuildTask(ui5Conf: UI5Config): UI5YamlCustomTaskConfiguration {
const buildTask =
ui5Conf.findCustomTask<UI5YamlCustomTaskConfiguration>('app-variant-bundler-build')?.configuration;

if (!buildTask) {
throw new Error('No CF ADP project found');
}

return buildTask;
}

/**
* Read the manifest from the local dist folder.
*
* @param {string} useLocal - The path to the dist folder.
* @returns {Manifest} The manifest.
*/
export function readLocalManifest(useLocal: string): Manifest {
const distPath = join(process.cwd(), useLocal);
const manifestPath = join(distPath, 'manifest.json');
return JSON.parse(readFileSync(manifestPath, 'utf-8')) as Manifest;
}

/**
* Load and parse the app variant descriptor.
*
* @param {ReaderCollection} rootProject - The root project.
* @returns {Promise<DescriptorVariant>} The parsed descriptor variant.
*/
export async function loadAppVariant(rootProject: ReaderCollection): Promise<DescriptorVariant> {
const appVariant = await rootProject.byPath('/manifest.appdescr_variant');
if (!appVariant) {
throw new Error('ADP configured but no manifest.appdescr_variant found.');
}
try {
const content = await appVariant.getString();
if (!content || content.trim() === '') {
throw new Error('ADP configured but manifest.appdescr_variant file is empty.');
}
return JSON.parse(content) as DescriptorVariant;
} catch (e) {
throw new Error(`Failed to parse manifest.appdescr_variant: ${e.message}`);
}
}

/**
* Convenience wrapper that reads the ui5.yaml and directly returns the ADP preview configuration.
* Throws if the configuration cannot be found.
Expand Down
72 changes: 71 additions & 1 deletion packages/adp-tooling/src/cf/app/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type AdmZip from 'adm-zip';

import type { ToolsLogger } from '@sap-ux/logger';

import { t } from '../../i18n';
import { extractXSApp } from '../utils';
import { getFDCApps } from '../services/api';
import type { CfConfig, CFApp, ServiceKeys } from '../../types';
import type { CfConfig, CFApp, ServiceKeys, XsApp } from '../../types';

/**
* Get the app host ids.
Expand All @@ -25,6 +28,73 @@ export function getAppHostIds(serviceKeys: ServiceKeys[]): string[] {
return [...new Set(appHostIds)];
}

/**
* Extracts the backend URL from service key credentials. Iterates through all endpoint keys to find the first endpoint with a URL.
*
* @param {ServiceKeys[]} serviceKeys - The credentials from service keys.
* @returns {string | undefined} The backend URL or undefined if not found.
*/
export function getBackendUrlFromServiceKeys(serviceKeys: ServiceKeys[]): string | undefined {
if (!serviceKeys || serviceKeys.length === 0) {
return undefined;
}

const endpoints = serviceKeys[0]?.credentials?.endpoints as Record<string, { url?: string }> | undefined;
if (endpoints && typeof endpoints === 'object' && endpoints !== null) {
for (const key in endpoints) {
if (Object.hasOwn(endpoints, key)) {
const endpoint = endpoints[key] as { url?: string } | undefined;
if (endpoint && typeof endpoint === 'object' && endpoint.url && typeof endpoint.url === 'string') {
return endpoint.url;
}
}
}
}

return undefined;
}

/**
* Extracts OAuth paths from xs-app.json routes that have a source property.
* These paths should receive OAuth Bearer tokens in the middleware.
*
* @param {AdmZip.IZipEntry[]} zipEntries - The zip entries containing xs-app.json.
* @returns {string[]} Array of path patterns (from route.source) that have a source property.
*/
export function getOAuthPathsFromXsApp(zipEntries: AdmZip.IZipEntry[]): string[] {
const xsApp: XsApp | undefined = extractXSApp(zipEntries);
if (!xsApp?.routes) {
return [];
}

const pathsSet = new Set<string>();
for (const route of xsApp.routes) {
if (route.service === 'html5-apps-repo-rt' || !route.source) {
continue;
}

let path = route.source;
// Remove leading ^ and trailing $
path = path.replace(/^\^/, '').replace(/\$$/, '');
// Remove capture groups like (.*) or $1
path = path.replace(/\([^)]*\)/g, '');
// Remove regex quantifiers
path = path.replace(/\$\d+/g, '');
// Clean up any remaining regex characters at the end
path = path.replace(/\/?\*$/, '');
// Normalize multiple consecutive slashes to single slash
while (path.includes('//')) {
path = path.replaceAll('//', '/');
}

if (path) {
pathsSet.add(path);
}
}

return Array.from(pathsSet);
}

/**
* Discover apps from FDC API based on credentials.
*
Expand Down
5 changes: 4 additions & 1 deletion packages/adp-tooling/src/cf/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,10 @@ async function getServiceInstance(params: GetServiceInstanceParams): Promise<Ser
* @param {ToolsLogger} logger - The logger.
* @returns {Promise<ServiceKeys[]>} The service instance keys.
*/
async function getOrCreateServiceKeys(serviceInstance: ServiceInstance, logger: ToolsLogger): Promise<ServiceKeys[]> {
export async function getOrCreateServiceKeys(
serviceInstance: ServiceInstance,
logger: ToolsLogger
): Promise<ServiceKeys[]> {
const serviceInstanceName = serviceInstance.name;
try {
const credentials = await getServiceKeys(serviceInstance.guid);
Expand Down
24 changes: 22 additions & 2 deletions packages/adp-tooling/src/preview/adp-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,14 @@ export class AdpPreview {
/**
* Fetch all required configurations from the backend and initialize all configurations.
*
* @param descriptorVariant descriptor variant from the project
* @returns the UI5 flex layer for which editing is enabled
* @param {DescriptorVariant} descriptorVariant - Descriptor variant from the project.
* @returns {Promise<UI5FlexLayer>} The UI5 flex layer for which editing is enabled.
*/
async init(descriptorVariant: DescriptorVariant): Promise<UI5FlexLayer> {
if (this.config.useLocal) {
return this.initLocal(descriptorVariant);
}

this.descriptorVariantId = descriptorVariant.id;
this.provider = await createAbapServiceProvider(
this.config.target,
Expand All @@ -152,11 +156,27 @@ export class AdpPreview {
return descriptorVariant.layer;
}

/**
* Initialize the preview for a local CF ADP project.
*
* @param descriptorVariant descriptor variant from the project
* @returns the UI5 flex layer for which editing is enabled
*/
private async initLocal(descriptorVariant: DescriptorVariant): Promise<UI5FlexLayer> {
this.descriptorVariantId = descriptorVariant.id;
this.isCloud = false;
this.routesHandler = new RoutesHandler(this.project, this.util, {} as AbapServiceProvider, this.logger);
return descriptorVariant.layer;
}

/**
* Synchronize local changes with the backend.
* The descriptor is refreshed only if the global flag is set to true.
*/
async sync(): Promise<void> {
if (this.config.useLocal) {
return;
}
if (!global.__SAP_UX_MANIFEST_SYNC_REQUIRED__ && this.mergedDescriptor) {
return;
}
Expand Down
19 changes: 18 additions & 1 deletion packages/adp-tooling/src/preview/routes-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ type ControllerInfo = { controllerName: string };
* @description Handles API Routes
*/
export default class RoutesHandler {
/**
* Whether this is running in local mode (CF ADP without backend).
*/
private readonly isLocalMode: boolean;

/**
* Constructor taking project as input.
*
Expand All @@ -60,7 +65,9 @@ export default class RoutesHandler {
private readonly util: MiddlewareUtils,
private readonly provider: AbapServiceProvider,
private readonly logger: ToolsLogger
) {}
) {
this.isLocalMode = !provider || typeof provider.getLayeredRepository !== 'function';
}

/**
* Reads files from workspace by given search pattern.
Expand Down Expand Up @@ -288,6 +295,16 @@ export default class RoutesHandler {
try {
const isRunningInBAS = isAppStudio();

if (this.isLocalMode) {
// In local mode (CF ADP), skip ManifestService as it requires ABAP backend
const apiResponse: AnnotationDataSourceResponse = {
isRunningInBAS,
annotationDataSourceMap: {}
};
this.sendFilesResponse(res, apiResponse);
return;
}

const manifestService = await this.getManifestService();
const dataSources = manifestService.getManifestDataSources();
const apiResponse: AnnotationDataSourceResponse = {
Expand Down
22 changes: 21 additions & 1 deletion packages/adp-tooling/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export interface AdpPreviewConfig {
* If set to true then certification validation errors are ignored.
*/
ignoreCertErrors?: boolean;
/**
* For CF ADP projects: when set to 'dist', serve resources from local dist instead of backend merge.
*/
useLocal?: string;
}

export interface OnpremApp {
Expand Down Expand Up @@ -884,6 +888,8 @@ export interface UI5YamlCustomTaskConfiguration {
space: string;
html5RepoRuntime: string;
sapCloudService: string;
serviceInstanceName: string;
serviceInstanceGuid: string;
}

export interface UI5YamlCustomTask {
Expand Down Expand Up @@ -1039,6 +1045,18 @@ export interface CfAdpWriterConfig {
approuter: AppRouterType;
businessService: string;
businessSolutionName?: string;
/**
* GUID of the business service instance.
*/
serviceInstanceGuid?: string;
/**
* Backend URL from service instance keys.
*/
backendUrl?: string;
/**
* OAuth paths extracted from xs-app.json routes that have a source property.
*/
oauthPaths?: string[];
};
project: {
name: string;
Expand Down Expand Up @@ -1068,6 +1086,9 @@ export interface CreateCfConfigParams {
layer: FlexLayer;
manifest: Manifest;
html5RepoRuntimeGuid: string;
serviceInstanceGuid?: string;
backendUrl?: string;
oauthPaths?: string[];
projectPath: string;
addStandaloneApprouter?: boolean;
publicVersions: UI5Version;
Expand Down Expand Up @@ -1137,7 +1158,6 @@ export interface CFApp {
messages?: string[];
serviceInstanceGuid?: string;
}

/**
* CF services (application sources) prompts
*/
Expand Down
3 changes: 1 addition & 2 deletions packages/adp-tooling/src/writer/cf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { adjustMtaYaml } from '../cf';
import { getApplicationType } from '../source';
import { fillDescriptorContent } from './manifest';
import type { CfAdpWriterConfig, Content } from '../types';
import { getCfVariant, writeCfTemplates, writeCfUI5Yaml, writeCfUI5BuildYaml } from './project-utils';
import { getCfVariant, writeCfTemplates, writeCfUI5Yaml } from './project-utils';
import { getI18nDescription, getI18nModels, writeI18nModels } from './i18n';

/**
Expand Down Expand Up @@ -54,7 +54,6 @@ export async function generateCf(

await writeCfTemplates(basePath, variant, fullConfig, fs);
await writeCfUI5Yaml(fullConfig.project.folder, fullConfig, fs);
await writeCfUI5BuildYaml(fullConfig.project.folder, fullConfig, fs);

return fs;
}
Expand Down
Loading
Loading