Skip to content
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
945d8ca
add support for ui5 project type 'component'
heimwege Dec 15, 2025
8e70432
Merge branch 'main' into feat/project-access/path-mappings-for-type-c…
heimwege Dec 16, 2025
c204fc0
add getWebappTestPath function and shortcut checking webapp directory
heimwege Jan 9, 2026
f93c92c
Merge branch 'main' of github.com:SAP/open-ux-tools into feat/project…
heimwege Jan 9, 2026
c263f20
export getWebappTestPath
heimwege Jan 9, 2026
7a15c06
remove unused code from eslint-plugin-fiori-tools
heimwege Jan 9, 2026
affb2f9
adjust virtual endpoints converter
heimwege Jan 9, 2026
9a54964
add todo for preview middleware
heimwege Jan 9, 2026
abbfe2f
adjust todo
heimwege Jan 9, 2026
3ebf770
adjust todos
heimwege Jan 9, 2026
7aabc18
make use of project type from ui5 server
heimwege Jan 19, 2026
3f16d92
refactoring
heimwege Jan 20, 2026
cfa9c09
Merge branch 'main' of github.com:SAP/open-ux-tools into feat/project…
heimwege Jan 20, 2026
eeeba31
Merge branch 'main' into feat/project-access/path-mappings-for-type-c…
heimwege Jan 20, 2026
a0b598e
fix unit tests
heimwege Jan 20, 2026
7bde53b
Merge branch 'main' into feat/project-access/path-mappings-for-type-c…
heimwege Jan 20, 2026
76e2aa4
add test project, adjust readme, refactoring and support editors
heimwege Jan 21, 2026
277c92c
delete unused code
heimwege Jan 21, 2026
3f41b86
refactoring, refactoring, refactoring
heimwege Jan 21, 2026
c5157ad
fix cards generator and add todo
heimwege Jan 21, 2026
93c77be
adjust reading manifest and manifest.appdescr_variant
heimwege Jan 21, 2026
0e8ea3f
fix reading manifest and manifest.appdescr_variant
heimwege Jan 21, 2026
a378724
more refactoring and fixes (sample app running with ui5 project type …
heimwege Jan 23, 2026
8e2b9a2
some renaming and refactoring
heimwege Jan 23, 2026
24d73e9
minor refactoring
heimwege Jan 26, 2026
6055b49
Merge branch 'main' of github.com:SAP/open-ux-tools into feat/project…
heimwege Jan 26, 2026
040b89f
Merge branch 'main' into feat/project-access/path-mappings-for-type-c…
heimwege Jan 26, 2026
4e872b7
adjust ui5 proxy to type component
heimwege Jan 27, 2026
845b16f
add unit tests for getWebappTestPath
heimwege Jan 27, 2026
c28db59
fix sonar issue
heimwege Jan 27, 2026
9995447
prettier
heimwege Jan 27, 2026
3b8212a
adjust jsdoc
heimwege Jan 27, 2026
da4a9f5
adjust proxy middleware UI5 spec version
heimwege Jan 27, 2026
cb338c4
add additional debug logging for routes
heimwege Jan 27, 2026
7d99e49
fix TestConfigDefaults
heimwege Jan 27, 2026
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"typescript-eslint": "8.49.0",
"update-ts-references": "3.6.2",
"yargs-parser": "21.1.1",
"yaml": "2.8.2"
"yaml": "2.8.2",
"@sap-ux/ui5-config": "workspace:*"
},
"scripts": {
"update-ts-references": "update-ts-references",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import * as manifestService from '../../../src/base/abap/manifest-service';
import type { AddXMLChange, AdpPreviewConfig, CommonChangeProperties } from '../../../src';
import { addXmlFragment, tryFixChange, addControllerExtension } from '../../../src/preview/change-handler';
import { addCustomSectionFragment } from '../../../src/preview/descriptor-change-handler';
// eslint-disable-next-line sonarjs/no-implicit-dependencies
import type { MiddlewareUtils } from '@ui5/server';

interface GetFragmentsResponse {
fragments: { fragmentName: string }[];
Expand Down Expand Up @@ -142,10 +144,13 @@ describe('AdaptationProject', () => {
},
getNamespace() {
return 'adp/project';
},
getType() {
return 'application';
}
};
}
};
} as unknown as MiddlewareUtils;

const logger = new ToolsLogger();
describe('init', () => {
Expand Down
10 changes: 6 additions & 4 deletions packages/app-config-writer/src/preview-config/preview-files.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { join } from 'node:path';
import { getWebappPath } from '@sap-ux/project-access';
import { join, basename } from 'node:path';
import { getWebappTestPath } from '@sap-ux/project-access';
import type { Editor } from 'mem-fs-editor';
import type { ToolsLogger } from '@sap-ux/logger';
import { TEST_CONFIG_DEFAULTS } from '../common/ui5-yaml';
Expand All @@ -22,7 +22,9 @@ const renameMessage = (filePath: string): string =>
* @param logger logger to report info to the user
*/
export async function renameSandbox(fs: Editor, basePath: string, path: string, logger?: ToolsLogger): Promise<void> {
const filePath = join(await getWebappPath(basePath), path);
const webappTestPath = await getWebappTestPath(basePath);
const fileName = basename(path);
const filePath = join(webappTestPath, fileName);
if (fs.exists(filePath)) {
fs.move(filePath, filePath.replace('.html', '_old.html'));
logger?.info(renameMessage(path));
Expand Down Expand Up @@ -86,7 +88,7 @@ export async function deleteNoLongerUsedFiles(
convertTests: boolean,
logger?: ToolsLogger
): Promise<void> {
const webappTestPath = join(await getWebappPath(basePath, fs), 'test');
const webappTestPath = await getWebappTestPath(basePath, fs);
const files = [
join(webappTestPath, 'locate-reuse-libs.js'),
join(webappTestPath, 'changes_loader.js'),
Expand Down
60 changes: 30 additions & 30 deletions packages/preview-middleware/README.md

Large diffs are not rendered by default.

59 changes: 40 additions & 19 deletions packages/preview-middleware/src/base/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@ import type {
} from '../types';
import { render } from 'ejs';
import { resolve, join, posix } from 'node:path';
import { createProjectAccess, getWebappPath, type Manifest, type UI5FlexLayer } from '@sap-ux/project-access';
import {
createProjectAccess,
getWebappPath,
getWebappTestPath,
type Manifest,
type UI5FlexLayer
} from '@sap-ux/project-access';
import { extractDoubleCurlyBracketsKey } from '@sap-ux/i18n';
import { readFileSync } from 'node:fs';
import { mergeTestConfigDefaults } from './test';
import { type Editor, create } from 'mem-fs-editor';
import { create as createStorage } from 'mem-fs';
import type { MergedAppDescriptor } from '@sap-ux/axios-extension';
// eslint-disable-next-line sonarjs/no-implicit-dependencies
import type { MiddlewareUtils } from '@ui5/server';

export interface CustomConnector {
applyConnector: string;
Expand Down Expand Up @@ -182,22 +190,27 @@ function getUI5Libs(manifest: Partial<Manifest>): string {
* Default configuration for the FLP.
*
* @param config partial configuration
* @param utils middleware utils
* @returns a full configuration with default values
*/
export function getFlpConfigWithDefaults(config: Partial<FlpConfig> = {}): FlpConfig {
const flpConfig = {
path: config.path ?? DEFAULT_PATH,
export function getFlpConfigWithDefaults(config: Partial<FlpConfig> = {}, utils?: MiddlewareUtils): FlpConfig {
const sandboxPathPrefix =
typeof utils === 'object' && utils.getProject?.()?.getType?.() === 'component'
? posix.join('/test-resources', utils.getProject().getNamespace())
: undefined;

// remove leading /test from default if sandboxPathPrefix is set
const defaultPath = sandboxPathPrefix ? DEFAULT_PATH.replace(/^\/test/, '') : DEFAULT_PATH;

return {
path: posix.join(sandboxPathPrefix ?? '/', config.path ?? defaultPath),
intent: config.intent ?? DEFAULT_INTENT,
apps: config.apps ?? [],
libs: config.libs,
theme: config.theme,
init: config.init,
enhancedHomePage: config.enhancedHomePage === true
} satisfies FlpConfig;
if (!flpConfig.path.startsWith('/')) {
flpConfig.path = `/${flpConfig.path}`;
}
return flpConfig;
}

/**
Expand Down Expand Up @@ -423,15 +436,20 @@ export function createTestTemplateConfig(config: CompleteTestConfig, id: string,
* Returns the preview paths.
*
* @param config configuration from the ui5.yaml
* @param utils middleware utils
* @param logger logger instance
* @returns an array of preview paths
*/
export function getPreviewPaths(config: MiddlewareConfig, logger: ToolsLogger = new ToolsLogger()): PreviewUrls[] {
export function getPreviewPaths(
config: MiddlewareConfig,
utils: MiddlewareUtils,
logger: ToolsLogger = new ToolsLogger()
): PreviewUrls[] {
const urls: PreviewUrls[] = [];
// remove incorrect configurations
sanitizeConfig(config, logger);
// add flp preview url
const flpConfig = getFlpConfigWithDefaults(config.flp);
const flpConfig = getFlpConfigWithDefaults(config.flp, utils);
urls.push({ path: `${flpConfig.path}#${flpConfig.intent.object}-${flpConfig.intent.action}`, type: 'preview' });
// add editor urls
if (config.editors?.rta) {
Expand All @@ -442,7 +460,7 @@ export function getPreviewPaths(config: MiddlewareConfig, logger: ToolsLogger =
// add test urls if configured
if (config.test) {
config.test.forEach((test) => {
const testConfig = mergeTestConfigDefaults(test);
const testConfig = mergeTestConfigDefaults(test, utils);
urls.push({ path: testConfig.path, type: 'test' });
});
}
Expand All @@ -457,16 +475,20 @@ const TEMPLATE_PATH = join(__dirname, '../../templates');
* @param configs array of test configurations
* @param manifest application manifest
* @param fs mem fs editor instance
* @param webappPath webapp path
* @param webappTestPath webapp test path
* @param flpTemplConfig FLP configuration
*/
function generateTestRunners(
configs: TestConfig[] | undefined,
manifest: Manifest,
fs: Editor,
webappPath: string,
webappTestPath: string,
flpTemplConfig: TemplateConfig
): void {
// Strip trailing 'test' or '/test' from webappTestPath to avoid duplication
// since testConfig.path typically starts with '/test/'
const basePath = webappTestPath.replace(/[/\\]test$/, '');

for (const test of configs ?? []) {
const testConfig = mergeTestConfigDefaults(test);
if (['QUnit', 'OPA5'].includes(test.framework)) {
Expand All @@ -476,14 +498,14 @@ function generateTestRunners(
manifest['sap.app'].id,
flpTemplConfig.ui5.theme
);
fs.write(join(webappPath, testConfig.path), render(testTemplate, testTemplateConfig));
fs.write(join(basePath, testConfig.path), render(testTemplate, testTemplateConfig));
} else if (test.framework === 'Testsuite') {
const testTemplate = readFileSync(join(TEMPLATE_PATH, 'test/testsuite.qunit.ejs'), 'utf-8');
const testTemplateConfig = {
basePath: flpTemplConfig.basePath,
initPath: testConfig.init
};
fs.write(join(webappPath, testConfig.path), render(testTemplate, testTemplateConfig));
fs.write(join(basePath, testConfig.path), render(testTemplate, testTemplateConfig));
}
}
}
Expand All @@ -507,15 +529,14 @@ export async function generatePreviewFiles(
sanitizeConfig(config, logger);

// create file system if not provided
if (!fs) {
fs = create(createStorage());
}
fs ??= create(createStorage());

// generate FLP configuration
const flpTemplate = readFileSync(join(TEMPLATE_PATH, 'flp/sandbox.ejs'), 'utf-8');
const flpConfig = getFlpConfigWithDefaults(config.flp);

const webappPath = await getWebappPath(basePath, fs);
const webappTestPath = await getWebappTestPath(basePath, fs);
let manifest: Manifest | undefined;
if (fs.exists(join(webappPath, 'manifest.json'))) {
manifest = (await fs.readJSON(join(webappPath, 'manifest.json'))) as unknown as Manifest;
Expand All @@ -536,7 +557,7 @@ export async function generatePreviewFiles(
},
logger
);
generateTestRunners(config.test, manifest, fs, webappPath, flpTemplConfig);
generateTestRunners(config.test, manifest, fs, webappTestPath, flpTemplConfig);
} else {
flpTemplConfig = createFlpTemplateConfig(flpConfig, {});
flpPath = join(basePath, flpConfig.path);
Expand Down
39 changes: 26 additions & 13 deletions packages/preview-middleware/src/base/flp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class FlpSandbox {
this.logger = logger;
this.project = project;
this.utils = utils;
this.flpConfig = getFlpConfigWithDefaults(config.flp);
this.flpConfig = getFlpConfigWithDefaults(config.flp, this.utils);
this.test = config.test;
this.rta = config.editors?.rta ?? sanitizeRtaConfig(config.rta, logger); //NOSONAR
logger.debug(`Config: ${JSON.stringify({ flp: this.flpConfig, rta: this.rta, test: this.test })}`);
Expand Down Expand Up @@ -182,10 +182,6 @@ export class FlpSandbox {
this.addStandardRoutes();

if (this.cardGenerator?.path) {
this.cardGenerator.path = this.cardGenerator.path.startsWith('/')
? this.cardGenerator.path
: `/${this.cardGenerator.path}`;

await this.addCardGeneratorMiddlewareRoute();
await this.addStoreCardManifestRoute();
await this.addStoreI18nKeysRoute();
Expand Down Expand Up @@ -421,14 +417,21 @@ export class FlpSandbox {
*/
private addEditorRoutes(rta: RtaConfig): void {
const cpe = dirname(require.resolve('@sap-ux/control-property-editor-sources'));
const sandboxPathPrefix =
typeof this.utils === 'object' && this.utils.getProject?.()?.getType?.() === 'component'
? posix.join('/test-resources', this.utils.getProject().getNamespace())
: undefined;
for (const editor of rta.endpoints) {
let previewUrl = editor.path.startsWith('/') ? editor.path : `/${editor.path}`;
let previewUrl = posix.join(sandboxPathPrefix ?? '/', editor.path);
if (editor.developerMode) {
previewUrl = `${previewUrl}.inner.html`;
editor.pluginScript ??= 'open/ux/preview/client/cpe/init';
this.router.get(editor.path, async (_req: Request, res: Response) => {
await this.editorGetHandlerDeveloperMode(res, rta, previewUrl);
});
this.router.get(
posix.join(sandboxPathPrefix ?? '/', editor.path),
async (_req: Request, res: Response) => {
await this.editorGetHandlerDeveloperMode(res, rta, previewUrl);
}
);
let path = dirname(editor.path);
if (!path.endsWith('/')) {
path = `${path}/`;
Expand Down Expand Up @@ -517,7 +520,17 @@ export class FlpSandbox {
* @private
*/
private async addCardGeneratorMiddlewareRoute(): Promise<void> {
const previewGeneratorPath = this.cardGenerator?.path ?? CARD_GENERATOR_DEFAULT.previewGeneratorSandbox;
const sandboxPathPrefix =
typeof this.utils === 'object' && this.utils.getProject?.()?.getType?.() === 'component'
? posix.join('/test-resources', this.utils.getProject().getNamespace())
: undefined;
const previewGeneratorSandbox = sandboxPathPrefix
? CARD_GENERATOR_DEFAULT.previewGeneratorSandbox.replace(/^\/test/, '')
: CARD_GENERATOR_DEFAULT.previewGeneratorSandbox;
const previewGeneratorPath = posix.join(
sandboxPathPrefix ?? '/',
this.cardGenerator?.path ?? previewGeneratorSandbox
);
this.logger.debug(`Add route for ${previewGeneratorPath}`);
this.router.get(
previewGeneratorPath,
Expand Down Expand Up @@ -828,7 +841,7 @@ export class FlpSandbox {
return;
}
const testsuite = readFileSync(join(__dirname, '../../templates/test/testsuite.qunit.ejs'), 'utf-8');
const config = mergeTestConfigDefaults(testsuiteConfig);
const config = mergeTestConfigDefaults(testsuiteConfig, this.utils);
this.logger.debug(`Add route for ${config.path}`);
this.router.get(
config.path,
Expand All @@ -849,7 +862,7 @@ export class FlpSandbox {
if (testConfig.framework === 'Testsuite') {
continue;
}
const mergedConfig = mergeTestConfigDefaults(testConfig);
const mergedConfig = mergeTestConfigDefaults(testConfig, this.utils);
testPaths.push(posix.relative(posix.dirname(config.path), mergedConfig.path));
}

Expand Down Expand Up @@ -955,7 +968,7 @@ export class FlpSandbox {
const ns = id.replace(/\./g, '/');
const htmlTemplate = readFileSync(join(__dirname, '../../templates/test/qunit.ejs'), 'utf-8');
for (const testConfig of configs) {
const config = mergeTestConfigDefaults(testConfig);
const config = mergeTestConfigDefaults(testConfig, this.utils);
this.logger.debug(`Add route for ${config.path}`);
// add route for the *.qunit.html
this.router.get(
Expand Down
48 changes: 42 additions & 6 deletions packages/preview-middleware/src/base/test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// eslint-disable-next-line sonarjs/no-implicit-dependencies
import type { Resource } from '@ui5/fs';
import type { CompleteTestConfig, TestConfig, TestConfigDefaults } from '../types';
// eslint-disable-next-line sonarjs/no-implicit-dependencies
import type { MiddlewareUtils } from '@ui5/server';
import { posix } from 'node:path';

const DEFAULTS: Record<string, Readonly<CompleteTestConfig>> = {
qunit: {
Expand All @@ -23,21 +26,54 @@
}
} satisfies TestConfigDefaults;

/**
* Get the test path prefix for component projects.
*
* @param utils middleware utils
* @returns test path prefix or undefined
*/
function getTestPathPrefix(utils?: MiddlewareUtils): string | undefined {
if (typeof utils !== 'object') {
return undefined;
}
return utils.getProject?.()?.getType?.() === 'component'
? posix.join('/test-resources', utils.getProject().getNamespace())
: undefined;
}

/**
* Merge the given test configuration with the default values.
*
* @param config test configuration
* @param utils middleware utils
* @returns merged test configuration
*/
export function mergeTestConfigDefaults(config: TestConfig): CompleteTestConfig {
const defaults = DEFAULTS[config.framework.toLowerCase()] ?? {};
export function mergeTestConfigDefaults(config: TestConfig, utils?: MiddlewareUtils): CompleteTestConfig {
const testPathPrefix = getTestPathPrefix(utils);
const defaults: CompleteTestConfig = DEFAULTS[config.framework.toLowerCase()] ?? {};

if (testPathPrefix) {
// remove leading /test from defaults if sandboxPathPrefix is set
defaults.pattern = defaults.pattern.replace(/^\/test/, '');
for (const prop of ['path', 'init'] as const) {
defaults[prop] = defaults[prop].replace(/^\/test/, '');
}
}

const merged: CompleteTestConfig = { ...defaults, ...config };
if (!merged.path.startsWith('/')) {
merged.path = `/${merged.path}`;

if (testPathPrefix) {
// Prepend testPathPrefix
for (const prop of ['path', 'init'] as const) {
merged[prop] = posix.join(testPathPrefix, merged[prop]);
}
}
if (!merged.init.startsWith('/')) {
merged.init = `/${config.init}`;

// Ensure path and init start with leading slash when no prefix exists
for (const prop of ['path', 'init'] as const) {
merged[prop] = posix.join('/', merged[prop]);
}

return merged;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/preview-middleware/src/ui5/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async function createRouter(
}

// add exposed endpoints for cds-plugin-ui5
flp.router.getAppPages = (): string[] => getPreviewPaths(config).map(({ path }) => path);
flp.router.getAppPages = (): string[] => getPreviewPaths(config, middlewareUtil).map(({ path }) => path);
return flp.router;
}

Expand Down
Loading
Loading