@sap-ux/ui5-proxy-middleware 1.5.12 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -11
- package/dist/base/proxy.d.ts +4 -1
- package/dist/base/proxy.js +23 -20
- package/dist/base/types.d.ts +16 -0
- package/dist/base/utils.d.ts +14 -4
- package/dist/base/utils.js +21 -5
- package/dist/ui5/middleware.js +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
The `@sap-ux/ui5-proxy-middleware` is a [Custom UI5 Server Middleware](https://sap.github.io/ui5-tooling/pages/extensibility/CustomServerMiddleware) for loading the UI5 sources in your application. It can be used either with the `ui5 serve` or the `fiori run` commands.
|
|
4
4
|
|
|
5
5
|
## Configuration Options
|
|
6
|
-
| Option | Default Value | Description |
|
|
7
|
-
| ------------ | ------------- | ----------- |
|
|
8
|
-
| `ui5` |
|
|
9
|
-
| `
|
|
10
|
-
| `
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
6
|
+
| Option | Value Type | Requirement Type | Default Value | Description |
|
|
7
|
+
| ------------ | ------------- |------------- | ----------- |----------- |
|
|
8
|
+
| `ui5` | object | optional | --- | Configuration object for the UI5 proxy middleware |
|
|
9
|
+
| `ui5.path` | string | optional | `/resources`, `/test-resources` | Path that is to be proxied |
|
|
10
|
+
| `ui5.url` | string | optional |`https://ui5.sap.com` | URL pointing to the resources |
|
|
11
|
+
| `ui5.pathReplace`| string | optional | `undefined` | If provided then the path will be replaced with this value before forwarding |
|
|
12
|
+
| `ui5.version` | string | optional |`undefined` | The UI5 version. If this property is not defined, then the `minUI5Version` from the `manifest.json` will be used |
|
|
13
|
+
| `secure` | boolean | optional | true | Defines if SSL certs should be verified |
|
|
14
|
+
| `debug` | boolean | optional | false | Enables debug output |
|
|
15
|
+
| `proxy` | string | optional | `undefined` | Use for adding corporate proxy configuration |
|
|
16
|
+
| `directLoad` | boolean | optional | false | Defines whether the UI5 sources should be loaded directly from UI5 CDN |
|
|
14
17
|
|
|
15
18
|
## Usage
|
|
16
19
|
In order to use the middleware this is the minimal configuration that you need to provide in the `ui5.yaml` of your application. All requests to `/resources` and `/test-resources` will be proxied to the latest UI5 version at https://ui5.sap.com.
|
|
@@ -40,7 +43,7 @@ server:
|
|
|
40
43
|
url: https://ui5.sap.com
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
Alternatively you can use the following syntax if all paths should be proxied to the same
|
|
46
|
+
Alternatively you can use the following syntax if all paths should be proxied to the same URL.
|
|
44
47
|
|
|
45
48
|
```Yaml
|
|
46
49
|
server:
|
|
@@ -54,10 +57,11 @@ server:
|
|
|
54
57
|
- /test-resources
|
|
55
58
|
url: https://ui5.sap.com
|
|
56
59
|
```
|
|
57
|
-
|
|
60
|
+
|
|
61
|
+
**NOTE: You can't mix the syntaxes!**
|
|
58
62
|
|
|
59
63
|
### Loading a specific UI5 version
|
|
60
|
-
To load a specific
|
|
64
|
+
To load a specific UI5 version in your application you can use the `version` parameter, e.g.
|
|
61
65
|
|
|
62
66
|
```Yaml
|
|
63
67
|
server:
|
|
@@ -92,6 +96,23 @@ server:
|
|
|
92
96
|
directLoad: true
|
|
93
97
|
```
|
|
94
98
|
|
|
99
|
+
### Loading UI5 sources from a different Host
|
|
100
|
+
If you want to load UI5 sources from a different host, then you can set the property `pathReplace` to point to the desired resources. If provided then the `path` will be replaced with this value before forwarding.
|
|
101
|
+
|
|
102
|
+
**NOTE: using `pathReplace` will not consider a specified UI5 version. If a specific UI5 version is needed, then it needs to be part of the `pathReplace`.**
|
|
103
|
+
|
|
104
|
+
```Yaml
|
|
105
|
+
server:
|
|
106
|
+
customMiddleware:
|
|
107
|
+
- name: ui5-proxy-middleware
|
|
108
|
+
afterMiddleware: compression
|
|
109
|
+
configuration:
|
|
110
|
+
ui5:
|
|
111
|
+
- path: /resources
|
|
112
|
+
url: https://my.backend.example:1234
|
|
113
|
+
pathReplace: /sap/public/ui5/resources
|
|
114
|
+
```
|
|
115
|
+
|
|
95
116
|
### Adding corporate proxy configuration
|
|
96
117
|
By default the `ui5-proxy-middleware` will read the proxy configuration from the OS environment variables `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` or from the Node.js environment variables `proxy`, `https-proxy`, and `noproxy`. If those variables are not set, then you can also provide the proxy configuration in the `ui5.yaml` file.
|
|
97
118
|
|
package/dist/base/proxy.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { Filter, Options } from 'http-proxy-middleware';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
3
|
import type { ProxyConfig } from './types';
|
|
4
|
+
import { ToolsLogger } from '@sap-ux/logger';
|
|
3
5
|
/**
|
|
4
6
|
* Function for proxying UI5 sources.
|
|
5
7
|
*
|
|
6
8
|
* @param config - proxy configuration
|
|
7
9
|
* @param options - additional configuration options
|
|
8
10
|
* @param filter - custom filter function which will be applied to all requests
|
|
11
|
+
* @param logger - optional logger instance
|
|
9
12
|
* @returns Proxy function to use
|
|
10
13
|
*/
|
|
11
|
-
export declare const ui5Proxy: (config: ProxyConfig, options?: Options, filter?: Filter) => import("http-proxy-middleware").RequestHandler
|
|
14
|
+
export declare const ui5Proxy: (config: ProxyConfig, options?: Options, filter?: Filter, logger?: ToolsLogger) => import("http-proxy-middleware").RequestHandler<IncomingMessage, ServerResponse<IncomingMessage>, (err?: any) => void>;
|
|
12
15
|
//# sourceMappingURL=proxy.d.ts.map
|
package/dist/base/proxy.js
CHANGED
|
@@ -12,28 +12,36 @@ const https_proxy_agent_1 = require("https-proxy-agent");
|
|
|
12
12
|
* @param config - proxy configuration
|
|
13
13
|
* @param options - additional configuration options
|
|
14
14
|
* @param filter - custom filter function which will be applied to all requests
|
|
15
|
+
* @param logger - optional logger instance
|
|
15
16
|
* @returns Proxy function to use
|
|
16
17
|
*/
|
|
17
|
-
const ui5Proxy = (config, options, filter
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
18
|
+
const ui5Proxy = (config, options, filter, logger = new logger_1.ToolsLogger({
|
|
19
|
+
transports: [new logger_1.UI5ToolingTransport({ moduleName: 'ui5-proxy-middleware' })]
|
|
20
|
+
})) => {
|
|
21
21
|
const today = new Date();
|
|
22
22
|
const etag = `W/"${config.version || 'ui5-latest-' + today.getDate() + today.getMonth() + today.getFullYear()}"`;
|
|
23
23
|
const ui5Ver = config.version ? `/${config.version}` : '';
|
|
24
|
+
let proxyFilter = utils_1.filterCompressedHtmlFiles;
|
|
25
|
+
if (filter) {
|
|
26
|
+
proxyFilter = filter;
|
|
27
|
+
}
|
|
24
28
|
const proxyConfig = {
|
|
29
|
+
on: {
|
|
30
|
+
proxyReq: (proxyReq, _req, res) => {
|
|
31
|
+
(0, utils_1.proxyRequestHandler)(proxyReq, res, etag);
|
|
32
|
+
},
|
|
33
|
+
proxyRes: (proxyRes) => {
|
|
34
|
+
(0, utils_1.proxyResponseHandler)(proxyRes, etag);
|
|
35
|
+
},
|
|
36
|
+
error: (err, req, res, target) => {
|
|
37
|
+
(0, utils_1.proxyErrorHandler)(err, req, logger, res, target);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
25
40
|
target: config.url,
|
|
26
41
|
changeOrigin: true,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
pathRewrite: { [config.path]: ui5Ver + config.path },
|
|
31
|
-
onProxyRes: (proxyRes) => {
|
|
32
|
-
(0, utils_1.proxyResponseHandler)(proxyRes, etag);
|
|
33
|
-
},
|
|
34
|
-
onError: (err, req, res, target) => {
|
|
35
|
-
(0, utils_1.proxyErrorHandler)(err, req, logger, res, target);
|
|
36
|
-
}
|
|
42
|
+
pathRewrite: (0, utils_1.getPathRewrite)(config, ui5Ver),
|
|
43
|
+
pathFilter: proxyFilter,
|
|
44
|
+
...options
|
|
37
45
|
};
|
|
38
46
|
// update proxy config with values coming from args or ui5.yaml
|
|
39
47
|
(0, utils_1.updateProxyEnv)(config.proxy);
|
|
@@ -41,12 +49,7 @@ const ui5Proxy = (config, options, filter) => {
|
|
|
41
49
|
if (corporateProxy) {
|
|
42
50
|
proxyConfig.agent = new https_proxy_agent_1.HttpsProxyAgent(corporateProxy);
|
|
43
51
|
}
|
|
44
|
-
|
|
45
|
-
let proxyFilter = utils_1.filterCompressedHtmlFiles;
|
|
46
|
-
if (filter) {
|
|
47
|
-
proxyFilter = filter;
|
|
48
|
-
}
|
|
49
|
-
return (0, http_proxy_middleware_1.createProxyMiddleware)(proxyFilter, proxyConfig);
|
|
52
|
+
return (0, http_proxy_middleware_1.createProxyMiddleware)(proxyConfig);
|
|
50
53
|
};
|
|
51
54
|
exports.ui5Proxy = ui5Proxy;
|
|
52
55
|
//# sourceMappingURL=proxy.js.map
|
package/dist/base/types.d.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import type { NextFunction } from 'express';
|
|
2
2
|
import type { IncomingMessage } from 'http';
|
|
3
3
|
export interface ProxyConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Path that is to be proxied.
|
|
6
|
+
*/
|
|
4
7
|
path: string;
|
|
8
|
+
/**
|
|
9
|
+
* If provided then the path will be replaced with this value before forwarding.
|
|
10
|
+
*/
|
|
11
|
+
pathReplace?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The target URL to proxy the request to.
|
|
14
|
+
*/
|
|
5
15
|
url: string;
|
|
16
|
+
/**
|
|
17
|
+
* If provided then the proxy will try to load the specified version of UI5 resources.
|
|
18
|
+
*/
|
|
6
19
|
version?: string;
|
|
20
|
+
/**
|
|
21
|
+
* If set then it will override the proxy settings from node.
|
|
22
|
+
*/
|
|
7
23
|
proxy?: string;
|
|
8
24
|
}
|
|
9
25
|
export interface UI5ProxyRequest {
|
package/dist/base/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import type { Options } from 'http-proxy-middleware';
|
|
2
3
|
import type { ToolsLogger } from '@sap-ux/logger';
|
|
3
4
|
import { type Manifest } from '@sap-ux/project-access';
|
|
4
5
|
import type { RequestHandler, NextFunction, Request, Response } from 'express';
|
|
@@ -6,6 +7,7 @@ import type http from 'http';
|
|
|
6
7
|
import type { ProxyConfig } from './types';
|
|
7
8
|
import type { Url } from 'url';
|
|
8
9
|
import type { ReaderCollection } from '@ui5/fs';
|
|
10
|
+
import type { Socket } from 'node:net';
|
|
9
11
|
/**
|
|
10
12
|
* Handler for the proxy response event.
|
|
11
13
|
* Sets an Etag which will be used for re-validation of the cached UI5 sources.
|
|
@@ -22,9 +24,8 @@ export declare const proxyResponseHandler: (proxyRes: IncomingMessage, etag: str
|
|
|
22
24
|
* @param proxyReq - proxy request object
|
|
23
25
|
* @param res - server response object
|
|
24
26
|
* @param etag - Etag of the cached UI5 sources, normally the UI5 version
|
|
25
|
-
* @param logger - Logger for loging the requests
|
|
26
27
|
*/
|
|
27
|
-
export declare const proxyRequestHandler: (proxyReq: ClientRequest, res: ServerResponse, etag: string
|
|
28
|
+
export declare const proxyRequestHandler: (proxyReq: ClientRequest, res: ServerResponse, etag: string) => void;
|
|
28
29
|
/**
|
|
29
30
|
* Get user's proxy configuration.
|
|
30
31
|
*
|
|
@@ -94,8 +95,9 @@ export declare function injectUI5Url(originalHtml: string, ui5Configs: ProxyConf
|
|
|
94
95
|
* @param next - the next function, used to forward the request to the next available handler
|
|
95
96
|
* @param ui5Configs - the UI5 configuration of the ui5-proxy-middleware
|
|
96
97
|
* @param rootProject - the root project
|
|
98
|
+
* @param logger - logger to be used
|
|
97
99
|
*/
|
|
98
|
-
export declare const injectScripts: (req: Request, res: Response, next: NextFunction, ui5Configs: ProxyConfig[], rootProject: ReaderCollection) => Promise<void>;
|
|
100
|
+
export declare const injectScripts: (req: Request, res: Response, next: NextFunction, ui5Configs: ProxyConfig[], rootProject: ReaderCollection, logger?: ToolsLogger) => Promise<void>;
|
|
99
101
|
/**
|
|
100
102
|
* Filters comressed html files from UI5 CDN.
|
|
101
103
|
* Avoid ERR_CONTENT_DECODING_FAILED on http request for gzip'd html files.
|
|
@@ -120,7 +122,7 @@ export declare function proxyErrorHandler(err: Error & {
|
|
|
120
122
|
}, req: IncomingMessage & {
|
|
121
123
|
next?: Function;
|
|
122
124
|
originalUrl?: string;
|
|
123
|
-
}, logger: ToolsLogger, _res?: ServerResponse, _target?: string | Partial<Url>): void;
|
|
125
|
+
}, logger: ToolsLogger, _res?: ServerResponse | Socket, _target?: string | Partial<Url>): void;
|
|
124
126
|
/**
|
|
125
127
|
* Adjust UI5 bootstrap URLs to load directly from UI5 CDN.
|
|
126
128
|
*
|
|
@@ -130,4 +132,12 @@ export declare function proxyErrorHandler(err: Error & {
|
|
|
130
132
|
* @returns RequestHandler to adjust bootstraps
|
|
131
133
|
*/
|
|
132
134
|
export declare function directLoadProxy(ui5Configs: ProxyConfig[], rootProject: ReaderCollection, logger: ToolsLogger): RequestHandler;
|
|
135
|
+
/**
|
|
136
|
+
* Create a rewrite based on the provided configuration.
|
|
137
|
+
*
|
|
138
|
+
* @param config proxy configuration
|
|
139
|
+
* @param ui5Ver UI5 version string
|
|
140
|
+
* @returns a path rewrite
|
|
141
|
+
*/
|
|
142
|
+
export declare function getPathRewrite(config: ProxyConfig, ui5Ver: string): Options['pathRewrite'];
|
|
133
143
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/base/utils.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.resolveUI5Version = resolveUI5Version;
|
|
|
6
6
|
exports.injectUI5Url = injectUI5Url;
|
|
7
7
|
exports.proxyErrorHandler = proxyErrorHandler;
|
|
8
8
|
exports.directLoadProxy = directLoadProxy;
|
|
9
|
+
exports.getPathRewrite = getPathRewrite;
|
|
9
10
|
const project_access_1 = require("@sap-ux/project-access");
|
|
10
11
|
const constants_1 = require("./constants");
|
|
11
12
|
const i18n_1 = require("../i18n");
|
|
@@ -29,10 +30,8 @@ exports.proxyResponseHandler = proxyResponseHandler;
|
|
|
29
30
|
* @param proxyReq - proxy request object
|
|
30
31
|
* @param res - server response object
|
|
31
32
|
* @param etag - Etag of the cached UI5 sources, normally the UI5 version
|
|
32
|
-
* @param logger - Logger for loging the requests
|
|
33
33
|
*/
|
|
34
|
-
const proxyRequestHandler = (proxyReq, res, etag
|
|
35
|
-
logger.debug(proxyReq.path);
|
|
34
|
+
const proxyRequestHandler = (proxyReq, res, etag) => {
|
|
36
35
|
if (proxyReq.getHeader('if-none-match') === etag) {
|
|
37
36
|
res.statusCode = 304;
|
|
38
37
|
res.end();
|
|
@@ -232,12 +231,14 @@ function injectUI5Url(originalHtml, ui5Configs) {
|
|
|
232
231
|
* @param next - the next function, used to forward the request to the next available handler
|
|
233
232
|
* @param ui5Configs - the UI5 configuration of the ui5-proxy-middleware
|
|
234
233
|
* @param rootProject - the root project
|
|
234
|
+
* @param logger - logger to be used
|
|
235
235
|
*/
|
|
236
|
-
const injectScripts = async (req, res, next, ui5Configs, rootProject) => {
|
|
236
|
+
const injectScripts = async (req, res, next, ui5Configs, rootProject, logger) => {
|
|
237
237
|
try {
|
|
238
238
|
const htmlFileName = (0, exports.getHtmlFile)(req.url);
|
|
239
239
|
const files = await rootProject.byGlob(`**/${htmlFileName}`);
|
|
240
240
|
if (files.length === 0) {
|
|
241
|
+
logger?.warn('No HTML file found for direct load injection.');
|
|
241
242
|
next();
|
|
242
243
|
}
|
|
243
244
|
else {
|
|
@@ -302,7 +303,7 @@ function proxyErrorHandler(err, req, logger, _res, _target) {
|
|
|
302
303
|
function directLoadProxy(ui5Configs, rootProject, logger) {
|
|
303
304
|
return async (req, res, next) => {
|
|
304
305
|
try {
|
|
305
|
-
await (0, exports.injectScripts)(req, res, next, ui5Configs, rootProject);
|
|
306
|
+
await (0, exports.injectScripts)(req, res, next, ui5Configs, rootProject, logger);
|
|
306
307
|
}
|
|
307
308
|
catch (error) {
|
|
308
309
|
logger.error(error);
|
|
@@ -310,4 +311,19 @@ function directLoadProxy(ui5Configs, rootProject, logger) {
|
|
|
310
311
|
}
|
|
311
312
|
};
|
|
312
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Create a rewrite based on the provided configuration.
|
|
316
|
+
*
|
|
317
|
+
* @param config proxy configuration
|
|
318
|
+
* @param ui5Ver UI5 version string
|
|
319
|
+
* @returns a path rewrite
|
|
320
|
+
*/
|
|
321
|
+
function getPathRewrite(config, ui5Ver) {
|
|
322
|
+
if (config.pathReplace) {
|
|
323
|
+
// Remove trailing slash from pathReplace if present
|
|
324
|
+
const sanitizedPathReplace = config.pathReplace?.replace(/\/$/, '');
|
|
325
|
+
return (path) => path.replace(config.path, sanitizedPathReplace);
|
|
326
|
+
}
|
|
327
|
+
return (path) => (path.startsWith(config.path) ? ui5Ver + path : ui5Ver + config.path + path);
|
|
328
|
+
}
|
|
313
329
|
//# sourceMappingURL=utils.js.map
|
package/dist/ui5/middleware.js
CHANGED
|
@@ -17,8 +17,7 @@ const dotenv_1 = __importDefault(require("dotenv"));
|
|
|
17
17
|
function createProxyOptions(logger, config) {
|
|
18
18
|
return {
|
|
19
19
|
secure: config.secure !== undefined ? !!config.secure : true,
|
|
20
|
-
|
|
21
|
-
logProvider: () => logger
|
|
20
|
+
logger: config.debug ? logger : undefined
|
|
22
21
|
};
|
|
23
22
|
}
|
|
24
23
|
/**
|
|
@@ -51,6 +50,7 @@ async function loadManifest(rootProject) {
|
|
|
51
50
|
}
|
|
52
51
|
module.exports = async ({ resources, options }) => {
|
|
53
52
|
const logger = new logger_1.ToolsLogger({
|
|
53
|
+
logLevel: options.configuration?.debug ? logger_1.LogLevel.Debug : logger_1.LogLevel.Info,
|
|
54
54
|
transports: [new logger_1.UI5ToolingTransport({ moduleName: 'ui5-proxy-middleware' })]
|
|
55
55
|
});
|
|
56
56
|
dotenv_1.default.config();
|
|
@@ -76,7 +76,7 @@ module.exports = async ({ resources, options }) => {
|
|
|
76
76
|
// hide user and pass from proxy configuration for displaying it in the terminal
|
|
77
77
|
const proxyInfo = (0, base_1.hideProxyCredentials)(corporateProxyServer);
|
|
78
78
|
const proxyOptions = createProxyOptions(logger, config);
|
|
79
|
-
logger.info(`Starting ui5-proxy-middleware using following configuration:\nproxy: '${proxyInfo}'\nsecure: '${proxyOptions.secure}'\nlog: '${
|
|
79
|
+
logger.info(`Starting ui5-proxy-middleware using following configuration:\nproxy: '${proxyInfo}'\nsecure: '${proxyOptions.secure}'\nlog: '${config.debug ? 'debug' : 'info'}' \ndirectLoad: '${directLoad}'`);
|
|
80
80
|
const configs = Array.isArray(config.ui5) ? config.ui5 : [config.ui5];
|
|
81
81
|
const ui5Configs = [];
|
|
82
82
|
const routes = [];
|
|
@@ -89,7 +89,7 @@ module.exports = async ({ resources, options }) => {
|
|
|
89
89
|
version: ui5Version,
|
|
90
90
|
proxy: config.proxy
|
|
91
91
|
};
|
|
92
|
-
routes.push({ route: ui5Config.path, handler: (0, base_1.ui5Proxy)(ui5Config, proxyOptions) });
|
|
92
|
+
routes.push({ route: ui5Config.path, handler: (0, base_1.ui5Proxy)(ui5Config, proxyOptions, undefined, logger) });
|
|
93
93
|
ui5Configs.push(ui5Config);
|
|
94
94
|
}
|
|
95
95
|
}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"bugs": {
|
|
10
10
|
"url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Aui5-proxy-middleware"
|
|
11
11
|
},
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.6.1",
|
|
13
13
|
"license": "Apache-2.0",
|
|
14
14
|
"author": "@SAP/ux-tools-team",
|
|
15
15
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"dotenv": "16.3.1",
|
|
25
|
-
"http-proxy-middleware": "
|
|
25
|
+
"http-proxy-middleware": "3.0.5",
|
|
26
26
|
"https-proxy-agent": "5.0.1",
|
|
27
27
|
"i18next": "25.3.0",
|
|
28
28
|
"proxy-from-env": "1.1.0",
|