diff --git a/.changeset/light-moles-search.md b/.changeset/light-moles-search.md
new file mode 100644
index 0000000000..b8808711d9
--- /dev/null
+++ b/.changeset/light-moles-search.md
@@ -0,0 +1,6 @@
+---
+"@sap-ux/adp-tooling": patch
+"@sap-ux/preview-middleware": patch
+---
+
+fix: refresh descriptor after manifest changes
diff --git a/packages/adp-tooling/src/preview/adp-preview.ts b/packages/adp-tooling/src/preview/adp-preview.ts
index f16549cad3..9713ccf755 100644
--- a/packages/adp-tooling/src/preview/adp-preview.ts
+++ b/packages/adp-tooling/src/preview/adp-preview.ts
@@ -12,7 +12,6 @@ import RoutesHandler from './routes-handler';
import type { AdpPreviewConfig, CommonChangeProperties, DescriptorVariant, OperationType } from '../types';
import type { Editor } from 'mem-fs-editor';
import { addXmlFragment, isAddXMLChange, moduleNameContentMap, tryFixChange } from './change-handler';
-
declare global {
// false positive, const can't be used here https://github.com/eslint/eslint/issues/15896
// eslint-disable-next-line no-var
@@ -119,9 +118,12 @@ export class AdpPreview {
/**
* Synchronize local changes with the backend.
- *
+ * The descriptor is refreshed only if the global flag is set to true.
*/
async sync(): Promise {
+ if (!global.__SAP_UX_MANIFEST_SYNC_REQUIRED__ && this.mergedDescriptor) {
+ return;
+ }
if (!this.lrep || !this.descriptorVariantId) {
throw new Error('Not initialized');
}
@@ -133,6 +135,7 @@ export class AdpPreview {
const buffer = zip.toBuffer();
this.mergedDescriptor = (await this.lrep.mergeAppDescriptorVariant(buffer, '//'))[this.descriptorVariantId];
+ global.__SAP_UX_MANIFEST_SYNC_REQUIRED__ = false;
}
/**
@@ -144,10 +147,7 @@ export class AdpPreview {
*/
async proxy(req: Request, res: Response, next: NextFunction): Promise {
if (req.path === '/manifest.json') {
- if (global.__SAP_UX_MANIFEST_SYNC_REQUIRED__) {
- await this.sync();
- global.__SAP_UX_MANIFEST_SYNC_REQUIRED__ = false;
- }
+ await this.sync();
res.status(200);
res.send(JSON.stringify(this.descriptor.manifest, undefined, 2));
} else if (req.path === '/Component-preload.js') {
diff --git a/packages/adp-tooling/test/unit/preview/adp-preview.test.ts b/packages/adp-tooling/test/unit/preview/adp-preview.test.ts
index 485f4671c3..282f3e7316 100644
--- a/packages/adp-tooling/test/unit/preview/adp-preview.test.ts
+++ b/packages/adp-tooling/test/unit/preview/adp-preview.test.ts
@@ -120,21 +120,23 @@ describe('AdaptationProject', () => {
}
};
- beforeAll(() => {
- nock(backend)
- .get((path) => path.startsWith('/sap/bc/lrep/actions/getcsrftoken/'))
- .reply(200)
- .persist(true);
- nock(backend)
- .put('/sap/bc/lrep/appdescr_variant_preview/?workspacePath=//')
- .reply(200, {
- 'my.adaptation': mockMergedDescriptor
- })
- .persist(true);
- });
-
const logger = new ToolsLogger();
describe('init', () => {
+ beforeAll(() => {
+ nock(backend)
+ .get((path) => path.startsWith('/sap/bc/lrep/actions/getcsrftoken/'))
+ .reply(200)
+ .persist(true);
+ nock(backend)
+ .put('/sap/bc/lrep/appdescr_variant_preview/?workspacePath=//')
+ .reply(200, {
+ 'my.adaptation': mockMergedDescriptor
+ })
+ .persist(true);
+ });
+ afterAll(() => {
+ nock.cleanAll();
+ });
test('default (no) config', async () => {
const adp = new AdpPreview(
{
@@ -180,7 +182,37 @@ describe('AdaptationProject', () => {
});
});
describe('sync', () => {
+ let secondCall: boolean = false;
+ beforeAll(() => {
+ nock(backend)
+ .get((path) => path.startsWith('/sap/bc/lrep/actions/getcsrftoken/'))
+ .reply(200)
+ .persist(true);
+ nock(backend)
+ .put('/sap/bc/lrep/appdescr_variant_preview/?workspacePath=//')
+ .reply(200, () => {
+ if (secondCall) {
+ return {
+ 'my.adaptation': 'testDescriptor'
+ };
+ }
+ return {
+ 'my.adaptation': mockMergedDescriptor
+ };
+ })
+ .persist(true);
+ });
+
+ afterAll(() => {
+ nock.cleanAll();
+ });
+
+ afterEach(() => {
+ global.__SAP_UX_MANIFEST_SYNC_REQUIRED__ = false;
+ });
+
test('updates merged descriptor', async () => {
+ global.__SAP_UX_MANIFEST_SYNC_REQUIRED__ = true;
const adp = new AdpPreview(
{
target: {
@@ -203,11 +235,102 @@ describe('AdaptationProject', () => {
await adp.sync();
expect(adp.descriptor).toBeDefined();
});
+
+ test('skip updating the merge descriptor if no manifest changes and descriptor was already fetched', async () => {
+ const adp = new AdpPreview(
+ {
+ target: {
+ url: backend
+ }
+ },
+ mockProject as unknown as ReaderCollection,
+ middlewareUtil,
+ logger
+ );
+
+ mockProject.byGlob.mockResolvedValueOnce([
+ {
+ getPath: () => '/manifest.appdescr_variant',
+ getBuffer: () => Buffer.from(descriptorVariant)
+ }
+ ]);
+ await adp.init(JSON.parse(descriptorVariant));
+ (adp as any).mergedDescriptor = undefined;
+ await adp.sync();
+ expect(adp.descriptor).toEqual(mockMergedDescriptor);
+ secondCall = true;
+ await adp.sync();
+ secondCall = false;
+ expect(adp.descriptor).not.toEqual('testDescriptor');
+ });
+
+ test('update descriptor if no manifest changes, but this is first descriptor fetch', async () => {
+ const adp = new AdpPreview(
+ {
+ target: {
+ url: backend
+ }
+ },
+ mockProject as unknown as ReaderCollection,
+ middlewareUtil,
+ logger
+ );
+
+ mockProject.byGlob.mockResolvedValueOnce([
+ {
+ getPath: () => '/manifest.appdescr_variant',
+ getBuffer: () => Buffer.from(descriptorVariant)
+ }
+ ]);
+ await adp.init(JSON.parse(descriptorVariant));
+ (adp as any).mergedDescriptor = undefined;
+ await adp.sync();
+ expect(adp.descriptor).toEqual(mockMergedDescriptor);
+ });
+
+ test('update descriptor if descriptor was already fetched, but there are manifest changes', async () => {
+ const adp = new AdpPreview(
+ {
+ target: {
+ url: backend
+ }
+ },
+ mockProject as unknown as ReaderCollection,
+ middlewareUtil,
+ logger
+ );
+
+ mockProject.byGlob.mockResolvedValueOnce([
+ {
+ getPath: () => '/manifest.appdescr_variant',
+ getBuffer: () => Buffer.from(descriptorVariant)
+ }
+ ]);
+ await adp.init(JSON.parse(descriptorVariant));
+ (adp as any).mergedDescriptor = undefined;
+ await adp.sync();
+ expect(adp.descriptor).toEqual(mockMergedDescriptor);
+ secondCall = true;
+ global.__SAP_UX_MANIFEST_SYNC_REQUIRED__ = true;
+ await adp.sync();
+ secondCall = false;
+ expect(adp.descriptor).toEqual('testDescriptor');
+ });
});
describe('proxy', () => {
let server: SuperTest;
const next = jest.fn().mockImplementation((_req, res) => res.status(200).send());
beforeAll(async () => {
+ nock(backend)
+ .get((path) => path.startsWith('/sap/bc/lrep/actions/getcsrftoken/'))
+ .reply(200)
+ .persist(true);
+ nock(backend)
+ .put('/sap/bc/lrep/appdescr_variant_preview/?workspacePath=//')
+ .reply(200, {
+ 'my.adaptation': mockMergedDescriptor
+ })
+ .persist(true);
const adp = new AdpPreview(
{
target: {
@@ -235,6 +358,10 @@ describe('AdaptationProject', () => {
server = supertest(app);
});
+ afterAll(() => {
+ nock.cleanAll();
+ });
+
afterEach(() => {
global.__SAP_UX_MANIFEST_SYNC_REQUIRED__ = false;
});
@@ -321,6 +448,16 @@ describe('AdaptationProject', () => {
describe('addApis', () => {
let server: SuperTest;
beforeAll(async () => {
+ nock(backend)
+ .get((path) => path.startsWith('/sap/bc/lrep/actions/getcsrftoken/'))
+ .reply(200)
+ .persist(true);
+ nock(backend)
+ .put('/sap/bc/lrep/appdescr_variant_preview/?workspacePath=//')
+ .reply(200, {
+ 'my.adaptation': mockMergedDescriptor
+ })
+ .persist(true);
const adp = new AdpPreview(
{
target: {
diff --git a/packages/preview-middleware/src/base/config.ts b/packages/preview-middleware/src/base/config.ts
index 3d41e79a06..77c6ffb74f 100644
--- a/packages/preview-middleware/src/base/config.ts
+++ b/packages/preview-middleware/src/base/config.ts
@@ -206,23 +206,11 @@ function getFlexSettings(): TemplateConfig['ui5']['flex'] {
* @param manifest manifest of the additional target app
* @param app configuration for the preview
* @param logger logger instance
- * @param descriptor descriptor of the additional target app
*/
-export async function addApp(
- templateConfig: TemplateConfig,
- manifest: Partial,
- app: App,
- logger: Logger,
- descriptor?: MergedAppDescriptor
-) {
+export async function addApp(templateConfig: TemplateConfig, manifest: Partial, app: App, logger: Logger) {
const id = manifest['sap.app']?.id ?? '';
- app.intent ??= {
- object: id.replace(/\./g, ''),
- action: 'preview'
- };
-
- const appName = `${app.intent?.object}-${app.intent?.action}`;
+ const appName = getAppName(manifest, app.intent);
templateConfig.ui5.resources[id] = app.target;
templateConfig.apps[appName] = {
title: (await getI18nTextFromProperty(app.local, manifest['sap.app']?.title, logger)) ?? id,
@@ -231,9 +219,24 @@ export async function addApp(
applicationType: 'URL',
url: app.target
};
- if (descriptor) {
- templateConfig.apps[appName].applicationDependencies = descriptor;
- }
+}
+
+/**
+ * Get the application name based on the manifest and app configuration.
+ *
+ * @param manifest - The application manifest.
+ * @param intent - The app configuration.
+ * @returns The application name.
+ */
+export function getAppName(manifest: Partial, intent?: Intent): string {
+ const id = manifest['sap.app']?.id ?? '';
+
+ intent ??= {
+ object: id.replace(/\./g, ''),
+ action: 'preview'
+ };
+
+ return `${intent?.object}-${intent?.action}`;
}
/**
diff --git a/packages/preview-middleware/src/base/flp.ts b/packages/preview-middleware/src/base/flp.ts
index d9e3ab7e48..83fc884ec8 100644
--- a/packages/preview-middleware/src/base/flp.ts
+++ b/packages/preview-middleware/src/base/flp.ts
@@ -18,7 +18,6 @@ import {
type OperationType
} from '@sap-ux/adp-tooling';
import { isAppStudio, exposePort } from '@sap-ux/btp-utils';
-import type { MergedAppDescriptor } from '@sap-ux/axios-extension';
import { FeatureToggleAccess } from '@sap-ux/feature-toggle';
import { deleteChange, readChanges, writeChange } from './flex';
@@ -30,7 +29,8 @@ import {
PREVIEW_URL,
type TemplateConfig,
createTestTemplateConfig,
- addApp
+ addApp,
+ getAppName
} from './config';
const DEVELOPER_MODE_CONFIG = new Map([
@@ -63,6 +63,8 @@ type OnChangeRequestHandler = (
* Class handling preview of a sandbox FLP.
*/
export class FlpSandbox {
+ private adp?: AdpPreview;
+ private manifest: Manifest;
protected onChangeRequest: OnChangeRequestHandler | undefined;
protected templateConfig: TemplateConfig;
public readonly config: FlpConfig;
@@ -106,18 +108,20 @@ export class FlpSandbox {
* @param manifest application manifest
* @param componentId optional componentId e.g. for adaptation projects
* @param resources optional additional resource mappings
- * @param descriptor optional additional descriptor mappings
+ * @param adp optional reference to the ADP tooling
*/
async init(
manifest: Manifest,
componentId?: string,
resources: Record = {},
- descriptor?: MergedAppDescriptor
+ adp?: AdpPreview
): Promise {
this.createFlexHandler();
this.config.libs ??= await this.hasLocateReuseLibsScript();
- const id = manifest['sap.app'].id;
+ const id = manifest['sap.app']?.id ?? '';
this.templateConfig = createFlpTemplateConfig(this.config, manifest, resources);
+ this.adp = adp;
+ this.manifest = manifest;
await addApp(
this.templateConfig,
@@ -128,8 +132,7 @@ export class FlpSandbox {
local: '.',
intent: this.config.intent
},
- this.logger,
- descriptor
+ this.logger
);
this.addStandardRoutes();
if (this.rta) {
@@ -158,10 +161,12 @@ export class FlpSandbox {
* @param editor editor configuration
* @returns FLP sandbox html
*/
- private generateSandboxForEditor(rta: RtaConfig, editor: Editor): string {
+ private async generateSandboxForEditor(rta: RtaConfig, editor: Editor): Promise {
const defaultGenerator = editor.developerMode
? '@sap-ux/control-property-editor'
: '@sap-ux/preview-middleware';
+
+ await this.setApplicationDependencies();
const config = { ...this.templateConfig };
/* sap.ui.rta needs to be added to the list of preload libs for variants management and adaptation projects */
if (!config.ui5.libs.includes('sap.ui.rta')) {
@@ -185,6 +190,20 @@ export class FlpSandbox {
return render(template, config);
}
+ /**
+ * Sets application dependencies in the template configuration.
+ * The descriptor is refreshed if the global flag is set.
+ *
+ * @returns Promise that resolves when the application dependencies are set
+ */
+ private async setApplicationDependencies(): Promise {
+ if (this.adp) {
+ await this.adp.sync();
+ const appName = getAppName(this.manifest, this.config.intent);
+ this.templateConfig.apps[appName].applicationDependencies = this.adp.descriptor;
+ }
+ }
+
/**
* Add additional routes for configured editors.
*
@@ -227,7 +246,7 @@ export class FlpSandbox {
this.router.use(`${path}editor`, serveStatic(cpe));
}
- this.router.get(previewUrl, (req: Request, res: Response) => {
+ this.router.get(previewUrl, async (req: Request, res: Response) => {
if (!req.query['fiori-tools-rta-mode']) {
// Redirect to the same URL but add the necessary parameter
const params = JSON.parse(JSON.stringify(req.query));
@@ -237,7 +256,7 @@ export class FlpSandbox {
res.redirect(302, `${previewUrl}?${new URLSearchParams(params)}`);
return;
}
- const html = this.generateSandboxForEditor(rta, editor).replace(
+ const html = (await this.generateSandboxForEditor(rta, editor)).replace(
'
+',
`\n`
);
@@ -554,7 +573,7 @@ export async function initAdp(
const descriptor = adp.descriptor;
const { name, manifest } = descriptor;
- await flp.init(manifest, name, adp.resources, descriptor);
+ await flp.init(manifest, name, adp.resources, adp);
flp.router.use(adp.descriptor.url, adp.proxy.bind(adp) as RequestHandler);
flp.addOnChangeRequestHandler(adp.onChangeRequest.bind(adp));
flp.router.use(json());
diff --git a/packages/preview-middleware/test/unit/base/__snapshots__/flp.test.ts.snap b/packages/preview-middleware/test/unit/base/__snapshots__/flp.test.ts.snap
index 3d80758fb7..91caff7e98 100644
--- a/packages/preview-middleware/test/unit/base/__snapshots__/flp.test.ts.snap
+++ b/packages/preview-middleware/test/unit/base/__snapshots__/flp.test.ts.snap
@@ -343,74 +343,6 @@ Object {
}
`;
-exports[`FlpSandbox init with passed descriptor 1`] = `
-Object {
- "apps": Object {
- "app-preview": Object {
- "additionalInformation": "SAPUI5.Component=myComponent",
- "applicationDependencies": Object {
- "asyncHints": Object {
- "requests": Array [
- Object {
- "url": "myRequestUrl",
- },
- ],
- },
- "components": Array [
- Object {
- "name": "myComponent",
- "url": "myComponentUrl",
- },
- ],
- "libs": Array [
- Object {
- "name": "myLib",
- "url": "myLibUrl",
- },
- ],
- },
- "applicationType": "URL",
- "description": "",
- "title": "my.id",
- "url": "..",
- },
- },
- "basePath": "..",
- "init": undefined,
- "locateReuseLibsScript": false,
- "ui5": Object {
- "bootstrapOptions": "",
- "flex": Array [
- Object {
- "connector": "LrepConnector",
- "layers": Array [],
- "url": "/sap/bc/lrep",
- },
- Object {
- "applyConnector": "custom.connectors.WorkspaceConnector",
- "custom": true,
- "writeConnector": "custom.connectors.WorkspaceConnector",
- },
- Object {
- "connector": "LocalStorageConnector",
- "layers": Array [
- "CUSTOMER",
- "USER",
- ],
- },
- ],
- "libs": "sap.m,sap.ui.core,sap.ushell",
- "resources": Object {
- "my.id": "..",
- "myResources1": "myResourcesUrl1",
- "myResources2": "myResourcesUrl2",
- "open.ux.preview.client": "../preview/client",
- },
- "theme": "sap_horizon",
- },
-}
-`;
-
exports[`FlpSandbox router GET /preview/api/changes 1`] = `"{\\"sap.ui.fl.myid\\":{\\"id\\":\\"myId\\"}}"`;
exports[`FlpSandbox router custom opa5 path test/integration/opaTests.qunit.html 1`] = `
@@ -584,6 +516,156 @@ exports[`FlpSandbox router editor with config 1`] = `
exports[`FlpSandbox router rta 1`] = `"Found. Redirecting to /my/rta.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true"`;
+exports[`FlpSandbox router rta with adp instance 1`] = `
+Object {
+ "apps": Object {
+ "app-preview": Object {
+ "additionalInformation": "SAPUI5.Component=myComponent",
+ "applicationType": "URL",
+ "description": "",
+ "title": "my.id",
+ "url": "..",
+ },
+ "testfev2other-preview": Object {
+ "additionalInformation": "SAPUI5.Component=test.fe.v2.other",
+ "applicationType": "URL",
+ "description": "This is a very simple application.",
+ "title": "My Other App",
+ "url": "/yet/another/app",
+ },
+ },
+ "basePath": "..",
+ "init": undefined,
+ "locateReuseLibsScript": false,
+ "ui5": Object {
+ "bootstrapOptions": "",
+ "flex": Array [
+ Object {
+ "connector": "LrepConnector",
+ "layers": Array [],
+ "url": "/sap/bc/lrep",
+ },
+ Object {
+ "applyConnector": "custom.connectors.WorkspaceConnector",
+ "custom": true,
+ "writeConnector": "custom.connectors.WorkspaceConnector",
+ },
+ Object {
+ "connector": "LocalStorageConnector",
+ "layers": Array [
+ "CUSTOMER",
+ "USER",
+ ],
+ },
+ ],
+ "libs": "sap.m,sap.ui.core,sap.ushell",
+ "resources": Object {
+ "my.id": "..",
+ "myResources1": "myResourcesUrl1",
+ "myResources2": "myResourcesUrl2",
+ "open.ux.preview.client": "../preview/client",
+ "test.fe.v2.other": "/yet/another/app",
+ },
+ "theme": "sap_horizon",
+ },
+}
+`;
+
+exports[`FlpSandbox router rta with adp instance 2`] = `
+"
+
+
+
+
+
+
+
+
Local FLP Sandbox
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+