Skip to content

Commit a73935c

Browse files
authored
Extract preview-middleware client code into separate module (#1261)
* create separate module for client files * converting init script to typescript * better documentation * hosting the client sources * initial unit tests * improved linting * use fetch instead of jquery * improving test coverage * faster testing * better test coverage * fix: win build error * added typecheck again * still win build issues * fix: unit tests * changeset * fix: build:client * use npm-run-all * fix: eslint
1 parent 512c88b commit a73935c

36 files changed

+2128
-704
lines changed

.changeset/curly-cycles-march.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-ux/preview-middleware': patch
3+
---
4+
5+
No change of functionality, just converted the init script to typescript

.changeset/weak-feet-sort.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-ux-private/preview-middleware-client': minor
3+
---
4+
5+
Initial version of the module containing a typescript version of of the flp init script.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"presets": [
3+
"@babel/preset-env",
4+
"transform-ui5",
5+
"@babel/preset-typescript"
6+
],
7+
"plugins": [
8+
[
9+
"transform-async-to-promises",
10+
{
11+
"inlineHelpers": true
12+
}
13+
]
14+
],
15+
"sourceMaps": true
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module.exports = {
2+
root: true,
3+
extends: ['plugin:@sap-ux/eslint-plugin-fiori-tools/defaultTS'],
4+
parserOptions: {
5+
project: './tsconfig.eslint.json',
6+
tsconfigRootDir: __dirname
7+
},
8+
rules: {
9+
'quotes': ['error', 'single'],
10+
'valid-jsdoc': ['error', {
11+
requireParamType: false,
12+
requireReturn: false,
13+
requireReturnType: false
14+
}]
15+
},
16+
overrides: [
17+
{
18+
files: ['types/*.*'],
19+
rules: {
20+
'@typescript-eslint/no-namespace': 'off',
21+
'jsdoc/require-jsdoc': 'off'
22+
}
23+
}
24+
]
25+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const config = require('../../jest.base');
2+
config.testEnvironment = 'jsdom';
3+
config.moduleNameMapper = {
4+
'^sap/(.+)$': `<rootDir>/test/__mock__/sap/$1.ts`
5+
};
6+
config.transform = {
7+
'^.+\\.ts$': [
8+
'ts-jest',
9+
{
10+
tsConfig: 'tsconfig.eslint.json'
11+
}
12+
]
13+
};
14+
module.exports = config;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@sap-ux-private/preview-middleware-client",
3+
"version": "0.0.1",
4+
"description": "Client-side coding hosted by the preview middleware",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://github.com/SAP/open-ux-tools.git",
8+
"directory": "packages/preview-middleware-client"
9+
},
10+
"license": "Apache-2.0",
11+
"private": true,
12+
"main": "dist/index.js",
13+
"scripts": {
14+
"build": "tsc && ui5 build --clean-dest --exclude-task=* --include-task=ui5-tooling-transpile-task",
15+
"clean": "rimraf --glob dist coverage *.tsbuildinfo",
16+
"format": "prettier --write '**/*.{js,json,ts,yaml,yml}' --ignore-path ../../.prettierignore",
17+
"lint": "eslint . --ext .ts",
18+
"lint:fix": "eslint . --ext .ts --fix",
19+
"test": "jest --ci --forceExit --detectOpenHandles --colors"
20+
},
21+
"files": [
22+
"dist"
23+
],
24+
"engines": {
25+
"pnpm": ">=6.26.1 < 7.0.0 || >=7.1.0",
26+
"node": ">= 14.16.0 < 15.0.0 || >=16.1.0 < 17.0.0 || >=18.0.0 < 19.0.0"
27+
},
28+
"dependencies": {},
29+
"devDependencies": {
30+
"@sap-ux/eslint-plugin-fiori-tools": "workspace:*",
31+
"@sapui5/types": "1.117.1",
32+
"@ui5/cli": "3.5.0",
33+
"ui5-tooling-transpile": "3.2.0"
34+
},
35+
"browserslist": "defaults"
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import Log from 'sap/base/Log';
2+
import type AppLifeCycle from 'sap/ushell/services/AppLifeCycle';
3+
import IconPool from 'sap/ui/core/IconPool';
4+
import ResourceBundle from 'sap/base/i18n/ResourceBundle';
5+
import UriParameters from 'sap/base/util/UriParameters';
6+
7+
/**
8+
* SAPUI5 delivered namespaces from https://ui5.sap.com/#/api/sap
9+
*/
10+
const UI5_LIBS = [
11+
'sap.apf',
12+
'sap.base',
13+
'sap.chart',
14+
'sap.collaboration',
15+
'sap.f',
16+
'sap.fe',
17+
'sap.fileviewer',
18+
'sap.gantt',
19+
'sap.landvisz',
20+
'sap.m',
21+
'sap.ndc',
22+
'sap.ovp',
23+
'sap.rules',
24+
'sap.suite',
25+
'sap.tnt',
26+
'sap.ui',
27+
'sap.uiext',
28+
'sap.ushell',
29+
'sap.uxap',
30+
'sap.viz',
31+
'sap.webanalytics',
32+
'sap.zen'
33+
];
34+
35+
/**
36+
* Check whether a specific dependency is a custom library, and if yes, add it to the map.
37+
*
38+
* @param dependency dependency from the manifest
39+
* @param customLibs map containing the required custom libraries
40+
*/
41+
function addKeys(dependency: Record<string, unknown>, customLibs: Record<string, true>): void {
42+
Object.keys(dependency).forEach(function (key) {
43+
// ignore libs or Components that start with SAPUI5 delivered namespaces
44+
if (
45+
!UI5_LIBS.some(function (substring) {
46+
return key === substring || key.startsWith(substring + '.');
47+
})
48+
) {
49+
customLibs[key] = true;
50+
}
51+
});
52+
}
53+
54+
/**
55+
* Fetch the manifest for all the given application urls and generate a string containing all required custom library ids.
56+
*
57+
* @param appUrls urls pointing to included applications
58+
* @returns Promise of a comma separated list of all required libraries.
59+
*/
60+
function getManifestLibs(appUrls: string[]): Promise<string> {
61+
const result = {} as Record<string, true>;
62+
const promises = [];
63+
for (const url of appUrls) {
64+
promises.push(
65+
fetch(`${url}/manifest.json`).then(async (resp) => {
66+
const manifest = await resp.json();
67+
if (manifest) {
68+
if (manifest['sap.ui5'] && manifest['sap.ui5'].dependencies) {
69+
if (manifest['sap.ui5'].dependencies.libs) {
70+
addKeys(manifest['sap.ui5'].dependencies.libs, result);
71+
}
72+
if (manifest['sap.ui5'].dependencies.components) {
73+
addKeys(manifest['sap.ui5'].dependencies.components, result);
74+
}
75+
}
76+
if (manifest['sap.ui5'] && manifest['sap.ui5'].componentUsages) {
77+
addKeys(manifest['sap.ui5'].componentUsages, result);
78+
}
79+
}
80+
})
81+
);
82+
}
83+
return Promise.all(promises).then(() => Object.keys(result).join(','));
84+
}
85+
86+
/**
87+
* Register the custom libraries and their url with the UI5 loader.
88+
*
89+
* @param dataFromAppIndex data returned from the app index service
90+
*/
91+
function registerModules(
92+
dataFromAppIndex: Record<
93+
string,
94+
{
95+
dependencies?: {
96+
url?: string;
97+
type?: string;
98+
componentId: string;
99+
}[];
100+
}
101+
>
102+
) {
103+
Object.keys(dataFromAppIndex).forEach(function (moduleDefinitionKey) {
104+
const moduleDefinition = dataFromAppIndex[moduleDefinitionKey];
105+
if (moduleDefinition && moduleDefinition.dependencies) {
106+
moduleDefinition.dependencies.forEach(function (dependency) {
107+
if (dependency.url && dependency.url.length > 0 && dependency.type === 'UI5LIB') {
108+
Log.info('Registering Library ' + dependency.componentId + ' from server ' + dependency.url);
109+
const compId = dependency.componentId.replace(/\./g, '/');
110+
const config = {
111+
paths: {} as Record<string, string>
112+
};
113+
config.paths[compId] = dependency.url;
114+
sap.ui.loader.config(config);
115+
}
116+
});
117+
}
118+
});
119+
}
120+
121+
/**
122+
* Fetch the manifest from the given application urls, then parse them for custom libs, and finally request their urls.
123+
*
124+
* @param appUrls application urls
125+
* @returns returns a promise when the registration is completed.
126+
*/
127+
export async function registerComponentDependencyPaths(appUrls: string[]): Promise<void> {
128+
const libs = await getManifestLibs(appUrls);
129+
if (libs && libs.length > 0) {
130+
let url = '/sap/bc/ui2/app_index/ui5_app_info?id=' + libs;
131+
const sapClient = UriParameters.fromQuery(window.location.search).get('sap-client');
132+
if (sapClient && sapClient.length === 3) {
133+
url = url + '&sap-client=' + sapClient;
134+
}
135+
const response = await fetch(url);
136+
registerModules(await response.json());
137+
}
138+
}
139+
140+
/**
141+
* Register SAP fonts that are also registered in a productive Fiori launchpad.
142+
*/
143+
export function registerSAPFonts() {
144+
//Fiori Theme font family and URI
145+
const fioriTheme = {
146+
fontFamily: 'SAP-icons-TNT',
147+
fontURI: sap.ui.require.toUrl('sap/tnt/themes/base/fonts/')
148+
};
149+
//Registering to the icon pool
150+
IconPool.registerFont(fioriTheme);
151+
//SAP Business Suite Theme font family and URI
152+
const bSuiteTheme = {
153+
fontFamily: 'BusinessSuiteInAppSymbols',
154+
fontURI: sap.ui.require.toUrl('sap/ushell/themes/base/fonts/')
155+
};
156+
//Registering to the icon pool
157+
IconPool.registerFont(bSuiteTheme);
158+
}
159+
160+
/**
161+
* Read the application title from the resource bundle and set it as document title.
162+
*/
163+
export function setI18nTitle() {
164+
const locale = sap.ui.getCore().getConfiguration().getLanguage();
165+
const resourceBundle = ResourceBundle.create({
166+
url: 'i18n/i18n.properties',
167+
locale
168+
}) as ResourceBundle;
169+
document.title = resourceBundle.getText('appTitle') ?? document.title;
170+
}
171+
172+
/**
173+
* Apply additional configuration.
174+
*
175+
* @param params init parameters read from the script tag
176+
* @param params.appUrls JSON containing a string array of application urls
177+
* @param params.flex JSON containing the flex configuration
178+
* @returns promise
179+
*/
180+
export function configure({ appUrls, flex }: { appUrls?: string | null; flex?: string | null }): Promise<void> {
181+
// Register RTA if configured
182+
if (flex) {
183+
sap.ushell.Container.attachRendererCreatedEvent(async function () {
184+
const serviceInstance = await sap.ushell.Container.getServiceAsync<AppLifeCycle>('AppLifeCycle');
185+
serviceInstance.attachAppLoaded(event => {
186+
const oView = event.getParameter('componentInstance');
187+
sap.ui.require(['sap/ui/rta/api/startAdaptation'], function (startAdaptation: (opts: object) => void) {
188+
const options = {
189+
rootControl: oView,
190+
validateAppVersion: false,
191+
flexSettings: JSON.parse(flex)
192+
};
193+
startAdaptation(options);
194+
});
195+
});
196+
});
197+
}
198+
199+
// Load custom library paths if configured
200+
if (appUrls) {
201+
return registerComponentDependencyPaths(JSON.parse(appUrls));
202+
} else {
203+
return Promise.resolve();
204+
}
205+
}
206+
207+
/**
208+
* Initialize the FLP sandbox.
209+
*/
210+
export function init() {
211+
setI18nTitle();
212+
registerSAPFonts();
213+
sap.ushell.Container.createRenderer().placeAt('content');
214+
}
215+
216+
const bootstrapConfig = document.getElementById('sap-ui-bootstrap');
217+
if (bootstrapConfig) {
218+
configure({
219+
appUrls: bootstrapConfig.getAttribute('data-open-ux-preview-libs-manifests'),
220+
flex: bootstrapConfig.getAttribute('data-open-ux-preview-flex-settings')
221+
})
222+
.then(init)
223+
.catch(() => Log.error('Sandbox initialization failed.'));
224+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"sap.app": {
3+
"id": "open.ux.preview.client"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default {
2+
debug: jest.fn(),
3+
info: jest.fn()
4+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const mockBundle = {
2+
getText: jest.fn()
3+
};
4+
5+
export default {
6+
create: jest.fn().mockReturnValue(mockBundle)
7+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
fromQuery: jest.fn().mockReturnValue({
3+
get: jest.fn()
4+
})
5+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
registerFont: jest.fn()
3+
};

0 commit comments

Comments
 (0)