generated from SAP/repository-template
-
Notifications
You must be signed in to change notification settings - Fork 35
/
utils.ts
337 lines (309 loc) · 11.2 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';
import type { ToolsLogger } from '@sap-ux/logger';
import { getMinimumUI5Version, type Manifest } from '@sap-ux/project-access';
import { UI5Config } from '@sap-ux/ui5-config';
import type { NextFunction, Request, Response } from 'express';
import type { ProxyConfig } from './types';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { BOOTSTRAP_LINK, BOOTSTRAP_REPLACE_REGEX, SANDBOX_LINK, SANDBOX_REPLACE_REGEX } from './constants';
import type { Url } from 'url';
import { t } from '../i18n';
/**
* Handler for the proxy response event.
* Sets an Etag which will be used for re-validation of the cached UI5 sources.
*
* @param proxyRes - proxy response object
* @param etag - ETag for the cached sources, normally the UI5 version
*/
export const proxyResponseHandler = (proxyRes: IncomingMessage, etag: string): void => {
proxyRes.headers['Etag'] = etag;
proxyRes.headers['cache-control'] = 'no-cache';
};
/**
* Handler for the proxy request event.
* Re-validates the cached UI5 sources based on the ETag.
* Logs the requests made by the proxy.
*
* @param proxyReq - proxy request object
* @param res - server response object
* @param etag - Etag of the cached UI5 sources, normally the UI5 version
* @param logger - Logger for loging the requests
*/
export const proxyRequestHandler = (
proxyReq: ClientRequest,
res: ServerResponse,
etag: string,
logger: ToolsLogger
): void => {
logger.debug(proxyReq.path);
if (proxyReq.getHeader('if-none-match') === etag) {
res.statusCode = 304;
res.end();
}
};
/**
* Get user's proxy configuration.
*
* @param yamlProxyServer - proxy server config from yaml file
* @returns User's proxy configuration or undefined
*/
export const getCorporateProxyServer = (yamlProxyServer: string | undefined): string | undefined => {
let proxyFromArgs: string | undefined;
process.argv.forEach((arg) => {
if (arg.match(/proxy=/g)) {
proxyFromArgs = arg.split('=')[1];
}
});
return (
proxyFromArgs ||
process.env.FIORI_TOOLS_PROXY ||
process.env.npm_config_proxy ||
process.env.npm_config_https_proxy ||
process.env.http_proxy ||
process.env.HTTP_PROXY ||
process.env.https_proxy ||
process.env.HTTPS_PROXY ||
yamlProxyServer
);
};
/**
* Hides the proxy credentials for displaying the proxy configuration in the console.
*
* @param proxy - user's proxy server
* @returns proxy with hidden credentials for displaying in the console
*/
export const hideProxyCredentials = (proxy: string | undefined): string | undefined => {
if (proxy) {
const forwardSlashIndex = proxy.indexOf('//');
const atIndex = proxy.indexOf('@');
if (forwardSlashIndex !== -1 && atIndex !== -1) {
proxy = proxy.replace(proxy.slice(forwardSlashIndex + 2, atIndex), '***:***');
}
}
return proxy;
};
/**
* Updates the proxy configuration with values from runtime args (highest priority), environment variables or given config value.
*
* @param proxyFromConfig - optional proxy string from configuration
*/
export function updateProxyEnv(proxyFromConfig?: string): void {
let proxyFromArgs: string | undefined;
process.argv.forEach((arg) => {
if (arg.match(/proxy=/g)) {
proxyFromArgs = arg.split('=')[1];
}
});
if (proxyFromArgs || process.env.FIORI_TOOLS_PROXY) {
process.env['npm_config_proxy'] = proxyFromArgs || process.env.FIORI_TOOLS_PROXY;
process.env['npm_config_https_proxy'] = proxyFromArgs || process.env.FIORI_TOOLS_PROXY;
} else {
const proxyFromEnv =
process.env.npm_config_proxy ||
process.env.npm_config_https_proxy ||
process.env.http_proxy ||
process.env.HTTP_PROXY ||
process.env.https_proxy ||
process.env.HTTPS_PROXY;
if (!proxyFromEnv && proxyFromConfig) {
process.env['npm_config_proxy'] = proxyFromConfig;
process.env['npm_config_https_proxy'] = proxyFromConfig;
}
}
}
/**
* Returns the name of html file, which is used to preview the application, from the URL.
*
* @param url - html request url
* @returns Name of the html file
*/
export const getHtmlFile = (url: string): string => {
let html = url;
if (html.indexOf('?') !== -1) {
html = html.split('?')[0].replace(/["']/g, '');
} else if (html.indexOf('#') !== -1) {
html = html.split('#')[0].replace(/["']/g, '');
} else {
html = html.replace(/["']/g, '');
}
return html;
};
/**
* Returns the name of the yaml file, which is used to for the server configuration, from the runtime arguments.
*
* @param args - runtime arguments
* @returns Name of the YAML file
*/
export const getYamlFile = (args: string[]): string => {
let yaml = 'ui5.yaml';
const index = args.indexOf('--config') !== -1 ? args.indexOf('--config') : args.indexOf('-c');
if (index !== -1) {
yaml = args[index + 1];
}
return yaml;
};
/**
* Gets the path to the webapp folder from the YAML file.
*
* @param ui5YamlPath - path to the yaml file
* @returns Path to the webapp folder
*/
export const getWebAppFolderFromYaml = async (ui5YamlPath: string): Promise<string> => {
if (existsSync(ui5YamlPath)) {
const ui5Config = await UI5Config.newInstance(readFileSync(ui5YamlPath, { encoding: 'utf8' }));
return ui5Config.getConfiguration().paths?.webapp ?? 'webapp';
} else {
return 'webapp';
}
};
/**
* Sends HTML content as a response.
*
* @param res - The http response object
* @param html - The HTML content
*/
export const setHtmlResponse = (res: any, html: string): void => {
if (res['_livereload']) {
res.write(html);
res.end();
} else {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.write(html);
res.end();
}
};
/**
* Determines which UI5 version to use when previewing the application.
*
* @param version ui5 version as defined in the yaml or via cli argument
* @param log logger for outputing information from where ui5 version config is coming
* @param manifest optional already loaded manifest.json
* @returns The UI5 version with which the application will be started
*/
export async function resolveUI5Version(version?: string, log?: ToolsLogger, manifest?: Manifest): Promise<string> {
let ui5Version: string;
let ui5VersionInfo: string;
let ui5VersionLocation: string;
if (process.env.FIORI_TOOLS_UI5_VERSION || process.env.FIORI_TOOLS_UI5_VERSION === '') {
ui5Version = process.env.FIORI_TOOLS_UI5_VERSION;
ui5VersionLocation = 'CLI arguments / Run configuration';
} else if (version !== undefined) {
ui5Version = version ? version : '';
ui5VersionLocation = getYamlFile(process.argv);
} else {
ui5Version = (manifest && getMinimumUI5Version(manifest)) || '';
ui5VersionLocation = 'manifest.json';
}
if (log) {
ui5VersionInfo = ui5Version ? ui5Version : 'latest';
log.info(t('info.ui5VersionSource', { version: ui5VersionInfo, source: ui5VersionLocation }));
}
return ui5Version;
}
/**
* Injects the absolute UI5 urls into the html file, which is used to preview the application.
*
* @param htmlFilePath - path to the html file which is used for previwing the application
* @param ui5Configs - the configuration of the ui5-proxy-middleware
* @returns The modified html file content
*/
export function injectUI5Url(htmlFilePath: string, ui5Configs: ProxyConfig[]): string | undefined {
if (existsSync(htmlFilePath)) {
let html = readFileSync(htmlFilePath, { encoding: 'utf8' });
for (const ui5Config of ui5Configs) {
const ui5Host = ui5Config.url.replace(/\/$/, '');
const ui5Url = ui5Config.version ? `${ui5Host}/${ui5Config.version}` : ui5Host;
if (ui5Config.path === '/resources') {
const resourcesUrl = `src="${ui5Url}/${BOOTSTRAP_LINK}"`;
html = html.replace(BOOTSTRAP_REPLACE_REGEX, resourcesUrl);
}
if (ui5Config.path === '/test-resources') {
const testResourcesUrl = `src="${ui5Url}/${SANDBOX_LINK}"`;
html = html.replace(SANDBOX_REPLACE_REGEX, testResourcesUrl);
}
}
return html;
} else {
return undefined;
}
}
/**
* Injects scripts into the html file, which is used to preview the application.
*
* @param req - the http request object
* @param res - the http response object
* @param next - the next function, used to forward the request to the next available handler
* @param ui5Configs - the UI5 configuration of the ui5-proxy-middleware
*/
export const injectScripts = async (
req: Request,
res: Response,
next: NextFunction,
ui5Configs: ProxyConfig[]
): Promise<void> => {
try {
const projectRoot = process.cwd();
const args = process.argv;
const htmlFileName = getHtmlFile(req.baseUrl);
const yamlFileName = getYamlFile(args);
const ui5YamlPath = join(projectRoot, yamlFileName);
const webAppFolder = await getWebAppFolderFromYaml(ui5YamlPath);
const htmlFilePath = join(projectRoot, webAppFolder, htmlFileName);
const html = injectUI5Url(htmlFilePath, ui5Configs);
if (html) {
setHtmlResponse(res, html);
} else {
next();
}
} catch (error) {
next(error);
}
};
/**
* Filters comressed html files from UI5 CDN.
* Avoid ERR_CONTENT_DECODING_FAILED on http request for gzip'd html files.
* e.g. /test-resources/sap/ui/qunit/testrunner.html?testpage=%2Ftest%2Ftestsuite.qunit.html&autostart=true.
*
* @param _pathname - the request path
* @param req - the http request object
* @returns True, indicating that the request should be proxied
*/
export const filterCompressedHtmlFiles = (_pathname: string, req: IncomingMessage): boolean => {
const acceptHeader = req.headers['accept'] || '';
if (
req.headers['accept-encoding'] &&
(acceptHeader.includes('text/html') || acceptHeader.includes('application/xhtml+xml'))
) {
delete req.headers['accept-encoding']; // Don't accept compressed html files from ui5 CDN
}
return true;
};
/**
* Specifically handling errors due to undefined and empty errors.
*
* @param err the error thrown when proxying the request or processing the response
* @param req request causing the error
* @param logger logger instance
* @param _res (not used)
* @param _target (not used)
*/
export function proxyErrorHandler(
err: Error & { code?: string },
req: IncomingMessage & { next?: Function; originalUrl?: string },
logger: ToolsLogger,
_res?: ServerResponse,
_target?: string | Partial<Url>
): void {
if (err && err.stack?.toLowerCase() !== 'error') {
if (typeof req.next === 'function') {
req.next(err);
} else {
throw err;
}
} else {
logger.debug(t('error.noCodeError', { error: JSON.stringify(err, null, 2), request: req.originalUrl }));
}
}