@appium/base-driver 10.2.0 → 10.2.2
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/LICENSE +201 -0
- package/build/lib/basedriver/capabilities.js +7 -7
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/event.d.ts +1 -1
- package/build/lib/basedriver/commands/event.d.ts.map +1 -1
- package/build/lib/basedriver/commands/execute.d.ts +1 -1
- package/build/lib/basedriver/commands/execute.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.d.ts +1 -1
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/mixin.d.ts +1 -1
- package/build/lib/basedriver/commands/mixin.d.ts.map +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts +14 -23
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js +11 -26
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts +36 -57
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +148 -239
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/logger.d.ts +1 -2
- package/build/lib/basedriver/logger.d.ts.map +1 -1
- package/build/lib/basedriver/logger.js +2 -2
- package/build/lib/basedriver/logger.js.map +1 -1
- package/build/lib/basedriver/validation.d.ts.map +1 -1
- package/build/lib/basedriver/validation.js +3 -3
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/constants.d.ts +1 -1
- package/build/lib/constants.d.ts.map +1 -1
- package/build/lib/express/crash.d.ts +8 -2
- package/build/lib/express/crash.d.ts.map +1 -1
- package/build/lib/express/crash.js +6 -0
- package/build/lib/express/crash.js.map +1 -1
- package/build/lib/express/express-logging.d.ts +12 -2
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +34 -26
- package/build/lib/express/express-logging.js.map +1 -1
- package/build/lib/express/idempotency.d.ts +4 -10
- package/build/lib/express/idempotency.d.ts.map +1 -1
- package/build/lib/express/idempotency.js +69 -73
- package/build/lib/express/idempotency.js.map +1 -1
- package/build/lib/express/logger.d.ts +1 -2
- package/build/lib/express/logger.d.ts.map +1 -1
- package/build/lib/express/logger.js +2 -2
- package/build/lib/express/logger.js.map +1 -1
- package/build/lib/express/middleware.d.ts +37 -41
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +48 -60
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts +57 -101
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +51 -128
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/static.d.ts +10 -5
- package/build/lib/express/static.d.ts.map +1 -1
- package/build/lib/express/static.js +32 -42
- package/build/lib/express/static.js.map +1 -1
- package/build/lib/express/websocket.d.ts +22 -6
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +10 -15
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts +4 -16
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +36 -48
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts +42 -78
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +87 -139
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +2 -2
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/jsonwp-status/status.d.ts +113 -158
- package/build/lib/jsonwp-status/status.d.ts.map +1 -1
- package/build/lib/jsonwp-status/status.js +10 -14
- package/build/lib/jsonwp-status/status.js.map +1 -1
- package/build/lib/protocol/bidi-commands.d.ts +31 -36
- package/build/lib/protocol/bidi-commands.d.ts.map +1 -1
- package/build/lib/protocol/bidi-commands.js +5 -5
- package/build/lib/protocol/bidi-commands.js.map +1 -1
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/helpers.d.ts +7 -11
- package/build/lib/protocol/helpers.d.ts.map +1 -1
- package/build/lib/protocol/helpers.js +5 -9
- package/build/lib/protocol/helpers.js.map +1 -1
- package/build/lib/protocol/index.d.ts +4 -21
- package/build/lib/protocol/index.d.ts.map +1 -1
- package/build/lib/protocol/index.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +15 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +50 -20
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts +8 -15
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +18 -33
- package/build/lib/protocol/routes.js.map +1 -1
- package/lib/basedriver/capabilities.ts +1 -1
- package/lib/basedriver/commands/event.ts +2 -2
- package/lib/basedriver/commands/execute.ts +2 -2
- package/lib/basedriver/commands/find.ts +2 -2
- package/lib/basedriver/commands/mixin.ts +1 -1
- package/lib/basedriver/commands/timeout.ts +2 -2
- package/lib/basedriver/{device-settings.js → device-settings.ts} +24 -35
- package/lib/basedriver/{helpers.js → helpers.ts} +208 -266
- package/lib/basedriver/logger.ts +3 -0
- package/lib/basedriver/validation.ts +2 -2
- package/lib/constants.ts +1 -1
- package/lib/express/crash.ts +15 -0
- package/lib/express/express-logging.ts +84 -0
- package/lib/express/{idempotency.js → idempotency.ts} +105 -89
- package/lib/express/logger.ts +3 -0
- package/lib/express/middleware.ts +187 -0
- package/lib/express/{server.js → server.ts} +175 -167
- package/lib/express/static.ts +77 -0
- package/lib/express/websocket.ts +81 -0
- package/lib/helpers/capabilities.ts +83 -0
- package/lib/jsonwp-proxy/protocol-converter.ts +284 -0
- package/lib/jsonwp-proxy/proxy.js +1 -1
- package/lib/jsonwp-status/{status.js → status.ts} +12 -15
- package/lib/protocol/{bidi-commands.js → bidi-commands.ts} +7 -5
- package/lib/protocol/errors.ts +1 -1
- package/lib/protocol/{helpers.js → helpers.ts} +8 -11
- package/lib/protocol/protocol.ts +57 -26
- package/lib/protocol/{routes.js → routes.ts} +29 -40
- package/package.json +11 -11
- package/tsconfig.json +3 -1
- package/lib/basedriver/logger.js +0 -4
- package/lib/express/crash.js +0 -11
- package/lib/express/express-logging.js +0 -60
- package/lib/express/logger.js +0 -4
- package/lib/express/middleware.js +0 -171
- package/lib/express/static.js +0 -76
- package/lib/express/websocket.js +0 -79
- package/lib/helpers/capabilities.js +0 -93
- package/lib/jsonwp-proxy/protocol-converter.js +0 -317
- /package/lib/protocol/{index.js → index.ts} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../lib/basedriver/helpers.
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../lib/basedriver/helpers.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,mBAAmB,EAKpB,MAAM,eAAe,CAAC;AAKvB,eAAO,MAAgB,cAAc,QAAqC,CAAC;AAkD3E;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAM,GAAG,MAAM,EAAE,GAAG,mBAA+C,GAC3E,OAAO,CAAC,MAAM,CAAC,CAyLjB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC,CAuBjF;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,CAqBpE;AAED;;;;;;GAMG;AAEH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAM9F;;;;;;;;AAkND,wBAM0B"}
|
|
@@ -11,7 +11,7 @@ exports.parseCapsArray = parseCapsArray;
|
|
|
11
11
|
exports.generateDriverLogPrefix = generateDriverLogPrefix;
|
|
12
12
|
const lodash_1 = __importDefault(require("lodash"));
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
-
const logger_1 =
|
|
14
|
+
const logger_1 = require("./logger");
|
|
15
15
|
const support_1 = require("@appium/support");
|
|
16
16
|
const lru_cache_1 = require("lru-cache");
|
|
17
17
|
const async_lock_1 = __importDefault(require("async-lock"));
|
|
@@ -26,13 +26,12 @@ const DEFAULT_REQ_HEADERS = Object.freeze({
|
|
|
26
26
|
'user-agent': `Appium (BaseDriver v${exports.BASEDRIVER_VER})`,
|
|
27
27
|
});
|
|
28
28
|
const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
|
|
29
|
-
/** @type {LRUCache<string, import('@appium/types').CachedAppInfo>} */
|
|
30
29
|
const APPLICATIONS_CACHE = new lru_cache_1.LRUCache({
|
|
31
30
|
max: MAX_CACHED_APPS,
|
|
32
31
|
ttl: CACHED_APPS_MAX_AGE_MS, // expire after 24 hours
|
|
33
32
|
updateAgeOnGet: true,
|
|
34
33
|
dispose: ({ fullPath }, app) => {
|
|
35
|
-
logger_1.
|
|
34
|
+
logger_1.log.info(`The application '${app}' cached at '${fullPath}' has ` +
|
|
36
35
|
`expired after ${CACHED_APPS_MAX_AGE_MS}ms`);
|
|
37
36
|
if (fullPath) {
|
|
38
37
|
support_1.fs.rimraf(fullPath);
|
|
@@ -49,40 +48,40 @@ process.on('exit', () => {
|
|
|
49
48
|
return;
|
|
50
49
|
}
|
|
51
50
|
const appPaths = [...APPLICATIONS_CACHE.values()].map(({ fullPath }) => fullPath);
|
|
52
|
-
logger_1.
|
|
53
|
-
support_1.util.pluralize('application', appPaths.length));
|
|
51
|
+
logger_1.log.debug(`Performing cleanup of ${support_1.util.pluralize('cached application', appPaths.length, true)}`);
|
|
54
52
|
for (const appPath of appPaths) {
|
|
53
|
+
if (!appPath) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
55
56
|
try {
|
|
56
|
-
// @ts-ignore it's defined
|
|
57
57
|
support_1.fs.rimrafSync(appPath);
|
|
58
58
|
}
|
|
59
59
|
catch (e) {
|
|
60
|
-
logger_1.
|
|
60
|
+
logger_1.log.warn(e.message);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* - Manages caching logic
|
|
69
|
-
* - Downloads the app from a remote URL to the local filesystem
|
|
70
|
-
* - Determines package name
|
|
71
|
-
* - Checks basic requirements on the application package
|
|
65
|
+
* Performs initial application package configuration so the app is ready for driver use.
|
|
66
|
+
* Resolves local paths, downloads remote apps (http/https) with optional caching, and
|
|
67
|
+
* runs optional post-process or custom download hooks.
|
|
72
68
|
*
|
|
73
|
-
* @param
|
|
74
|
-
* @param
|
|
75
|
-
* @
|
|
69
|
+
* @param app - Path to a local app or URL of a downloadable app (http/https).
|
|
70
|
+
* @param options - Supported extensions and optional hooks. Either a single extension
|
|
71
|
+
* string, an array of extension strings, or {@link ConfigureAppOptions} (e.g.
|
|
72
|
+
* `supportedExtensions`, `onPostProcess`, `onDownload`).
|
|
73
|
+
* @returns Resolved path to the application (local path or path to downloaded/cached app).
|
|
74
|
+
* @throws {Error} If supported extensions are missing, the app path/URL is invalid, or download fails.
|
|
76
75
|
*/
|
|
77
|
-
async function configureApp(app, options =
|
|
76
|
+
async function configureApp(app, options = {}) {
|
|
78
77
|
if (!lodash_1.default.isString(app)) {
|
|
79
78
|
// immediately shortcircuit if not given an app
|
|
80
79
|
return '';
|
|
81
80
|
}
|
|
82
|
-
/** @type {string[]} */
|
|
83
81
|
let supportedAppExtensions;
|
|
84
|
-
const
|
|
85
|
-
const
|
|
82
|
+
const opts = !lodash_1.default.isString(options) && !lodash_1.default.isArray(options) ? options : undefined;
|
|
83
|
+
const onPostProcess = opts?.onPostProcess;
|
|
84
|
+
const onDownload = opts?.onDownload;
|
|
86
85
|
if (lodash_1.default.isString(options)) {
|
|
87
86
|
supportedAppExtensions = [options];
|
|
88
87
|
}
|
|
@@ -90,18 +89,18 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
90
89
|
supportedAppExtensions = options;
|
|
91
90
|
}
|
|
92
91
|
else if (lodash_1.default.isPlainObject(options)) {
|
|
93
|
-
supportedAppExtensions = options.supportedExtensions;
|
|
92
|
+
supportedAppExtensions = options.supportedExtensions ?? [];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
supportedAppExtensions = [];
|
|
94
96
|
}
|
|
95
|
-
// @ts-ignore this is OK
|
|
96
97
|
if (lodash_1.default.isEmpty(supportedAppExtensions)) {
|
|
97
98
|
throw new Error(`One or more supported app extensions must be provided`);
|
|
98
99
|
}
|
|
99
100
|
let newApp = app;
|
|
100
101
|
const originalAppLink = app;
|
|
101
102
|
let packageHash = null;
|
|
102
|
-
/** @type {import('axios').AxiosResponse['headers']|undefined} */
|
|
103
103
|
let headers;
|
|
104
|
-
/** @type {RemoteAppProps} */
|
|
105
104
|
const remoteAppProps = {
|
|
106
105
|
lastModified: null,
|
|
107
106
|
immutable: false,
|
|
@@ -112,7 +111,7 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
112
111
|
const isUrl = isSupportedUrl(app);
|
|
113
112
|
if (!isUrl && !node_path_1.default.isAbsolute(newApp)) {
|
|
114
113
|
newApp = node_path_1.default.resolve(process.cwd(), newApp);
|
|
115
|
-
logger_1.
|
|
114
|
+
logger_1.log.warn(`The current application path '${app}' is not absolute ` +
|
|
116
115
|
`and has been rewritten to '${newApp}'. Consider using absolute paths rather than relative`);
|
|
117
116
|
app = newApp;
|
|
118
117
|
}
|
|
@@ -120,64 +119,68 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
120
119
|
return await APPLICATIONS_CACHE_GUARD.acquire(appCacheKey, async () => {
|
|
121
120
|
const cachedAppInfo = APPLICATIONS_CACHE.get(appCacheKey);
|
|
122
121
|
if (cachedAppInfo) {
|
|
123
|
-
logger_1.
|
|
122
|
+
logger_1.log.debug(`Cached app data: ${JSON.stringify(cachedAppInfo, null, 2)}`);
|
|
124
123
|
}
|
|
125
124
|
if (isUrl) {
|
|
126
125
|
// Use the app from remote URL
|
|
127
|
-
logger_1.
|
|
128
|
-
const reqHeaders = {
|
|
129
|
-
...DEFAULT_REQ_HEADERS,
|
|
130
|
-
};
|
|
126
|
+
logger_1.log.info(`Using downloadable app '${newApp}'`);
|
|
127
|
+
const reqHeaders = { ...DEFAULT_REQ_HEADERS };
|
|
131
128
|
if (cachedAppInfo?.etag) {
|
|
132
129
|
reqHeaders['if-none-match'] = cachedAppInfo.etag;
|
|
133
130
|
}
|
|
134
131
|
else if (cachedAppInfo?.lastModified) {
|
|
135
132
|
reqHeaders['if-modified-since'] = cachedAppInfo.lastModified.toUTCString();
|
|
136
133
|
}
|
|
137
|
-
logger_1.
|
|
138
|
-
let
|
|
139
|
-
|
|
134
|
+
logger_1.log.debug(`Request headers: ${JSON.stringify(reqHeaders)}`);
|
|
135
|
+
let result = await queryAppLink(newApp, reqHeaders);
|
|
136
|
+
headers = result.headers;
|
|
137
|
+
let { stream, status } = result;
|
|
138
|
+
logger_1.log.debug(`Response status: ${status}`);
|
|
140
139
|
try {
|
|
141
140
|
if (!lodash_1.default.isEmpty(headers)) {
|
|
142
141
|
if (headers.etag) {
|
|
143
|
-
logger_1.
|
|
142
|
+
logger_1.log.debug(`Etag: ${headers.etag}`);
|
|
144
143
|
remoteAppProps.etag = headers.etag;
|
|
145
144
|
}
|
|
146
145
|
if (headers['last-modified']) {
|
|
147
|
-
logger_1.
|
|
146
|
+
logger_1.log.debug(`Last-Modified: ${headers['last-modified']}`);
|
|
148
147
|
remoteAppProps.lastModified = new Date(headers['last-modified']);
|
|
149
148
|
}
|
|
150
149
|
if (headers['cache-control']) {
|
|
151
|
-
logger_1.
|
|
152
|
-
remoteAppProps.immutable = /\bimmutable\b/i.test(headers['cache-control']);
|
|
153
|
-
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(headers['cache-control']);
|
|
150
|
+
logger_1.log.debug(`Cache-Control: ${headers['cache-control']}`);
|
|
151
|
+
remoteAppProps.immutable = /\bimmutable\b/i.test(String(headers['cache-control']));
|
|
152
|
+
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(String(headers['cache-control']));
|
|
154
153
|
if (maxAgeMatch) {
|
|
155
154
|
remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
|
|
156
155
|
}
|
|
157
156
|
}
|
|
158
157
|
}
|
|
159
158
|
if (cachedAppInfo && status === HTTP_STATUS_NOT_MODIFIED) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
const cachedPath = cachedAppInfo.fullPath ?? '';
|
|
160
|
+
if (cachedPath && (await isAppIntegrityOk(cachedPath, cachedAppInfo.integrity))) {
|
|
161
|
+
logger_1.log.info(`Reusing previously downloaded application at '${cachedPath}'`);
|
|
162
|
+
return verifyAppExtension(cachedPath, supportedAppExtensions);
|
|
163
163
|
}
|
|
164
|
-
logger_1.
|
|
164
|
+
logger_1.log.info(`The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
|
|
165
165
|
`or its integrity has been damaged. Deleting it from the internal cache`);
|
|
166
166
|
APPLICATIONS_CACHE.delete(appCacheKey);
|
|
167
167
|
if (!stream.closed) {
|
|
168
168
|
stream.destroy();
|
|
169
169
|
}
|
|
170
|
-
|
|
170
|
+
result = await queryAppLink(newApp, { ...DEFAULT_REQ_HEADERS });
|
|
171
|
+
stream = result.stream;
|
|
172
|
+
headers = result.headers;
|
|
173
|
+
status = result.status;
|
|
171
174
|
}
|
|
172
175
|
if (onDownload) {
|
|
173
176
|
newApp = await onDownload({
|
|
174
177
|
url: originalAppLink,
|
|
175
|
-
headers:
|
|
178
|
+
headers: lodash_1.default.clone(headers),
|
|
176
179
|
stream,
|
|
177
180
|
});
|
|
178
181
|
}
|
|
179
182
|
else {
|
|
180
|
-
const fileName = determineFilename(headers, pathname, supportedAppExtensions);
|
|
183
|
+
const fileName = determineFilename(headers, pathname ?? '', supportedAppExtensions);
|
|
181
184
|
newApp = await fetchApp(stream, await support_1.tempDir.path({
|
|
182
185
|
prefix: fileName,
|
|
183
186
|
suffix: '',
|
|
@@ -192,7 +195,7 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
192
195
|
}
|
|
193
196
|
else if (await support_1.fs.exists(newApp)) {
|
|
194
197
|
// Use the local app
|
|
195
|
-
logger_1.
|
|
198
|
+
logger_1.log.info(`Using local app '${newApp}'`);
|
|
196
199
|
}
|
|
197
200
|
else {
|
|
198
201
|
let errorMessage = `The application at '${newApp}' does not exist or is not accessible`;
|
|
@@ -208,7 +211,6 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
208
211
|
if (isPackageAFile) {
|
|
209
212
|
packageHash = await calculateFileIntegrity(newApp);
|
|
210
213
|
}
|
|
211
|
-
/** @type {(appPathToCache: string) => Promise<string>} */
|
|
212
214
|
const storeAppInCache = async (appPathToCache) => {
|
|
213
215
|
const cachedFullPath = cachedAppInfo?.fullPath;
|
|
214
216
|
if (cachedFullPath && cachedFullPath !== appPathToCache) {
|
|
@@ -231,14 +233,14 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
231
233
|
return appPathToCache;
|
|
232
234
|
};
|
|
233
235
|
if (lodash_1.default.isFunction(onPostProcess)) {
|
|
234
|
-
const
|
|
235
|
-
/** @type {import('@appium/types').PostProcessOptions<import('axios').AxiosResponseHeaders>} */ ({
|
|
236
|
+
const postProcessArg = {
|
|
236
237
|
cachedAppInfo: lodash_1.default.clone(cachedAppInfo),
|
|
237
238
|
isUrl,
|
|
238
239
|
originalAppLink,
|
|
239
240
|
headers: lodash_1.default.clone(headers),
|
|
240
241
|
appPath: newApp,
|
|
241
|
-
}
|
|
242
|
+
};
|
|
243
|
+
const result = await onPostProcess(postProcessArg);
|
|
242
244
|
return !result?.appPath || app === result?.appPath || !(await support_1.fs.exists(result?.appPath))
|
|
243
245
|
? newApp
|
|
244
246
|
: await storeAppInCache(result.appPath);
|
|
@@ -250,21 +252,23 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
250
252
|
});
|
|
251
253
|
}
|
|
252
254
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
+
* Returns whether the given string looks like a package or bundle identifier
|
|
256
|
+
* (e.g. `com.example.app` or `org.company.AnotherApp`).
|
|
257
|
+
*
|
|
258
|
+
* @param app - Value to check (e.g. app path or bundle id).
|
|
259
|
+
* @returns `true` if the value matches a dot-separated identifier pattern.
|
|
255
260
|
*/
|
|
256
261
|
function isPackageOrBundle(app) {
|
|
257
262
|
return /^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/.test(app);
|
|
258
263
|
}
|
|
259
264
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
265
|
+
* Recursively ensures both keys exist with the same value in objects and arrays.
|
|
266
|
+
* For each object, if `firstKey` exists its value is also set at `secondKey`, and vice versa.
|
|
262
267
|
*
|
|
263
|
-
*
|
|
264
|
-
|
|
265
|
-
* @param
|
|
266
|
-
* @
|
|
267
|
-
* @param {String} secondKey The second key to duplicate
|
|
268
|
+
* @param input - Object, array, or primitive to process (arrays/objects traversed recursively).
|
|
269
|
+
* @param firstKey - First key name to mirror.
|
|
270
|
+
* @param secondKey - Second key name to mirror.
|
|
271
|
+
* @returns A deep copy of `input` with both keys present where objects had either key.
|
|
268
272
|
*/
|
|
269
273
|
function duplicateKeys(input, firstKey, secondKey) {
|
|
270
274
|
// If array provided, recursively call on all elements
|
|
@@ -274,7 +278,7 @@ function duplicateKeys(input, firstKey, secondKey) {
|
|
|
274
278
|
// If object, create duplicates for keys and then recursively call on values
|
|
275
279
|
if (lodash_1.default.isPlainObject(input)) {
|
|
276
280
|
const resultObj = {};
|
|
277
|
-
for (
|
|
281
|
+
for (const [key, value] of lodash_1.default.toPairs(input)) {
|
|
278
282
|
const recursivelyCalledValue = duplicateKeys(value, firstKey, secondKey);
|
|
279
283
|
if (key === firstKey) {
|
|
280
284
|
resultObj[secondKey] = recursivelyCalledValue;
|
|
@@ -290,11 +294,12 @@ function duplicateKeys(input, firstKey, secondKey) {
|
|
|
290
294
|
return input;
|
|
291
295
|
}
|
|
292
296
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
297
|
+
* Normalizes a capability value to a string array. If already an array, returns it;
|
|
298
|
+
* if a string, parses as JSON array when possible, otherwise returns a single-element array.
|
|
295
299
|
*
|
|
296
|
-
* @param
|
|
297
|
-
* @returns
|
|
300
|
+
* @param capValue - Capability value: string (including JSON array like `"[\"a\",\"b\"]"`) or string[].
|
|
301
|
+
* @returns Array of strings.
|
|
302
|
+
* @throws {TypeError} If value is not a string/array or JSON parsing fails for array-like input.
|
|
298
303
|
*/
|
|
299
304
|
function parseCapsArray(capValue) {
|
|
300
305
|
if (lodash_1.default.isArray(capValue)) {
|
|
@@ -311,7 +316,7 @@ function parseCapsArray(capValue) {
|
|
|
311
316
|
if (lodash_1.default.isString(capValue) && lodash_1.default.startsWith(lodash_1.default.trimStart(capValue), '[')) {
|
|
312
317
|
throw new TypeError(message);
|
|
313
318
|
}
|
|
314
|
-
logger_1.
|
|
319
|
+
logger_1.log.warn(message);
|
|
315
320
|
}
|
|
316
321
|
if (lodash_1.default.isString(capValue)) {
|
|
317
322
|
return [capValue];
|
|
@@ -319,25 +324,69 @@ function parseCapsArray(capValue) {
|
|
|
319
324
|
throw new TypeError(`Expected a string or a valid JSON array; received '${capValue}'`);
|
|
320
325
|
}
|
|
321
326
|
/**
|
|
322
|
-
*
|
|
327
|
+
* Builds a short log prefix for a driver instance (e.g. `UiAutomator2@a1b2`).
|
|
323
328
|
*
|
|
324
|
-
* @param
|
|
325
|
-
* @param
|
|
326
|
-
*
|
|
327
|
-
* @returns {string}
|
|
329
|
+
* @param obj - Driver or other object; its constructor name and a short id are used.
|
|
330
|
+
* @param _sessionId - Deprecated and unused; kept for {@link DriverHelpers} interface compatibility.
|
|
331
|
+
* @returns Prefix string like `DriverName@xxxx`, or `UnknownDriver@????` if `obj` is null.
|
|
328
332
|
*/
|
|
329
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
330
|
-
function generateDriverLogPrefix(obj,
|
|
333
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- DriverHelpers interface
|
|
334
|
+
function generateDriverLogPrefix(obj, _sessionId) {
|
|
335
|
+
if (!obj) {
|
|
336
|
+
// This should not happen
|
|
337
|
+
return 'UnknownDriver@????';
|
|
338
|
+
}
|
|
331
339
|
return `${obj.constructor.name}@${support_1.node.getObjectId(obj).substring(0, 4)}`;
|
|
332
340
|
}
|
|
341
|
+
function parseAppLink(appLink) {
|
|
342
|
+
try {
|
|
343
|
+
return new URL(appLink);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return {};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function isEnvOptionEnabled(optionName, defaultValue = null) {
|
|
350
|
+
const value = process.env[optionName];
|
|
351
|
+
if (!lodash_1.default.isNull(defaultValue) && lodash_1.default.isEmpty(value)) {
|
|
352
|
+
return defaultValue;
|
|
353
|
+
}
|
|
354
|
+
return !lodash_1.default.isEmpty(value) && !['0', 'false', 'no'].includes(lodash_1.default.toLower(value));
|
|
355
|
+
}
|
|
356
|
+
function isSupportedUrl(app) {
|
|
357
|
+
try {
|
|
358
|
+
const { protocol } = parseAppLink(app);
|
|
359
|
+
return ['http:', 'https:'].includes(protocol ?? '');
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
333
365
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* @param {string} appLink The URL to download an app from
|
|
338
|
-
* @param {import('axios').RawAxiosRequestHeaders} reqHeaders Additional HTTP request headers
|
|
339
|
-
* @returns {Promise<RemoteAppData>}
|
|
366
|
+
* Transforms the given app link to the cache key.
|
|
367
|
+
* Necessary to properly cache apps having the same address but different query strings,
|
|
368
|
+
* e.g. ones stored in S3 using presigned URLs.
|
|
340
369
|
*/
|
|
370
|
+
function toCacheKey(app) {
|
|
371
|
+
if (!isEnvOptionEnabled('APPIUM_APPS_CACHE_IGNORE_URL_QUERY') || !isSupportedUrl(app)) {
|
|
372
|
+
return app;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const parsed = parseAppLink(app);
|
|
376
|
+
const href = 'href' in parsed ? parsed.href : undefined;
|
|
377
|
+
const search = 'search' in parsed ? parsed.search : undefined;
|
|
378
|
+
if (href && search) {
|
|
379
|
+
return href.replace(search, '');
|
|
380
|
+
}
|
|
381
|
+
if (href) {
|
|
382
|
+
return href;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// ignore
|
|
387
|
+
}
|
|
388
|
+
return app;
|
|
389
|
+
}
|
|
341
390
|
async function queryAppLink(appLink, reqHeaders) {
|
|
342
391
|
const url = new URL(appLink);
|
|
343
392
|
// Extract credentials, then remove them from the URL for axios
|
|
@@ -345,14 +394,7 @@ async function queryAppLink(appLink, reqHeaders) {
|
|
|
345
394
|
url.username = '';
|
|
346
395
|
url.password = '';
|
|
347
396
|
const axiosUrl = url.href;
|
|
348
|
-
|
|
349
|
-
const axiosAuth = username ? {
|
|
350
|
-
username,
|
|
351
|
-
password,
|
|
352
|
-
} : undefined;
|
|
353
|
-
/**
|
|
354
|
-
* @type {import('axios').RawAxiosRequestConfig}
|
|
355
|
-
*/
|
|
397
|
+
const axiosAuth = username ? { username, password } : undefined;
|
|
356
398
|
const requestOpts = {
|
|
357
399
|
url: axiosUrl,
|
|
358
400
|
auth: axiosAuth,
|
|
@@ -363,24 +405,12 @@ async function queryAppLink(appLink, reqHeaders) {
|
|
|
363
405
|
};
|
|
364
406
|
try {
|
|
365
407
|
const { data: stream, headers, status } = await (0, axios_1.default)(requestOpts);
|
|
366
|
-
return {
|
|
367
|
-
stream,
|
|
368
|
-
headers,
|
|
369
|
-
status,
|
|
370
|
-
};
|
|
408
|
+
return { stream, headers, status };
|
|
371
409
|
}
|
|
372
410
|
catch (err) {
|
|
373
411
|
throw new Error(`Cannot download the app from ${axiosUrl}: ${err.message}`);
|
|
374
412
|
}
|
|
375
413
|
}
|
|
376
|
-
/**
|
|
377
|
-
* Retrieves app payload from the given stream. Also meters the download performance.
|
|
378
|
-
*
|
|
379
|
-
* @param {import('stream').Readable} srcStream The incoming stream
|
|
380
|
-
* @param {string} dstPath The target file path to be written
|
|
381
|
-
* @returns {Promise<string>} The same dstPath
|
|
382
|
-
* @throws {Error} If there was a failure while downloading the file
|
|
383
|
-
*/
|
|
384
414
|
async function fetchApp(srcStream, dstPath) {
|
|
385
415
|
const timer = new support_1.timing.Timer().start();
|
|
386
416
|
try {
|
|
@@ -388,7 +418,7 @@ async function fetchApp(srcStream, dstPath) {
|
|
|
388
418
|
srcStream.pipe(writer);
|
|
389
419
|
await new bluebird_1.default((resolve, reject) => {
|
|
390
420
|
srcStream.once('error', reject);
|
|
391
|
-
writer.once('finish', resolve);
|
|
421
|
+
writer.once('finish', () => resolve());
|
|
392
422
|
writer.once('error', (e) => {
|
|
393
423
|
srcStream.unpipe(writer);
|
|
394
424
|
reject(e);
|
|
@@ -400,78 +430,25 @@ async function fetchApp(srcStream, dstPath) {
|
|
|
400
430
|
}
|
|
401
431
|
const secondsElapsed = timer.getDuration().asSeconds;
|
|
402
432
|
const { size } = await support_1.fs.stat(dstPath);
|
|
403
|
-
logger_1.
|
|
433
|
+
logger_1.log.debug(`The application (${support_1.util.toReadableSizeString(size)}) ` +
|
|
404
434
|
`has been downloaded to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`);
|
|
405
435
|
// it does not make much sense to approximate the speed for short downloads
|
|
406
436
|
if (secondsElapsed >= AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC) {
|
|
407
437
|
const bytesPerSec = Math.floor(size / secondsElapsed);
|
|
408
|
-
logger_1.
|
|
438
|
+
logger_1.log.debug(`Approximate download speed: ${support_1.util.toReadableSizeString(bytesPerSec)}/s`);
|
|
409
439
|
}
|
|
410
440
|
return dstPath;
|
|
411
441
|
}
|
|
412
|
-
/**
|
|
413
|
-
* Transforms the given app link to the cache key.
|
|
414
|
-
* This is necessary to properly cache apps
|
|
415
|
-
* having the same address, but different query strings,
|
|
416
|
-
* for example ones stored in S3 using presigned URLs.
|
|
417
|
-
*
|
|
418
|
-
* @param {string} app App link.
|
|
419
|
-
* @returns {string} Transformed app link or the original arg if
|
|
420
|
-
* no transformation is needed.
|
|
421
|
-
*/
|
|
422
|
-
function toCacheKey(app) {
|
|
423
|
-
if (!isEnvOptionEnabled('APPIUM_APPS_CACHE_IGNORE_URL_QUERY') || !isSupportedUrl(app)) {
|
|
424
|
-
return app;
|
|
425
|
-
}
|
|
426
|
-
try {
|
|
427
|
-
const { href, search } = parseAppLink(app);
|
|
428
|
-
if (href && search) {
|
|
429
|
-
return href.replace(search, '');
|
|
430
|
-
}
|
|
431
|
-
if (href) {
|
|
432
|
-
return href;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
catch { }
|
|
436
|
-
return app;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Safely parses the given app link to a URL object
|
|
440
|
-
*
|
|
441
|
-
* @param {string} appLink
|
|
442
|
-
* @returns {URL|import('@appium/types').StringRecord} Parsed URL object
|
|
443
|
-
* or an empty object if the parsing has failed
|
|
444
|
-
*/
|
|
445
|
-
function parseAppLink(appLink) {
|
|
446
|
-
try {
|
|
447
|
-
return new URL(appLink);
|
|
448
|
-
}
|
|
449
|
-
catch {
|
|
450
|
-
return {};
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Tries to determine the file name of the payload that is going
|
|
455
|
-
* to be downloaded from an URL
|
|
456
|
-
*
|
|
457
|
-
* @param {import('axios').RawAxiosRequestHeaders} headers
|
|
458
|
-
* @param {string} pathname
|
|
459
|
-
* @param {string[]} supportedAppExtensions
|
|
460
|
-
* @returns {string}
|
|
461
|
-
*/
|
|
462
442
|
function determineFilename(headers, pathname, supportedAppExtensions) {
|
|
463
443
|
const basename = support_1.fs.sanitizeName(node_path_1.default.basename(decodeURIComponent(pathname ?? '')), {
|
|
464
444
|
replacement: SANITIZE_REPLACEMENT,
|
|
465
445
|
});
|
|
466
446
|
const extname = node_path_1.default.extname(basename);
|
|
467
|
-
if (headers['content-disposition'] && /^attachment/i.test(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const match = /filename="([^"]+)/i.exec(/** @type {string} */ (headers['content-disposition']));
|
|
447
|
+
if (headers['content-disposition'] && /^attachment/i.test(String(headers['content-disposition']))) {
|
|
448
|
+
logger_1.log.debug(`Content-Disposition: ${headers['content-disposition']}`);
|
|
449
|
+
const match = /filename="([^"]+)/i.exec(String(headers['content-disposition']));
|
|
471
450
|
if (match) {
|
|
472
|
-
return support_1.fs.sanitizeName(match[1], {
|
|
473
|
-
replacement: SANITIZE_REPLACEMENT,
|
|
474
|
-
});
|
|
451
|
+
return support_1.fs.sanitizeName(match[1], { replacement: SANITIZE_REPLACEMENT });
|
|
475
452
|
}
|
|
476
453
|
}
|
|
477
454
|
// assign the default file name and the extension if none has been detected
|
|
@@ -480,61 +457,12 @@ function determineFilename(headers, pathname, supportedAppExtensions) {
|
|
|
480
457
|
: DEFAULT_BASENAME;
|
|
481
458
|
let resultingExt = extname;
|
|
482
459
|
if (!supportedAppExtensions.map(lodash_1.default.toLower).includes(lodash_1.default.toLower(resultingExt))) {
|
|
483
|
-
logger_1.
|
|
460
|
+
logger_1.log.info(`The current file extension '${resultingExt}' is not supported. ` +
|
|
484
461
|
`Defaulting to '${lodash_1.default.first(supportedAppExtensions)}'`);
|
|
485
|
-
resultingExt =
|
|
462
|
+
resultingExt = lodash_1.default.first(supportedAppExtensions);
|
|
486
463
|
}
|
|
487
464
|
return `${resultingName}${resultingExt}`;
|
|
488
465
|
}
|
|
489
|
-
/**
|
|
490
|
-
* Checks whether we can threat the given app link
|
|
491
|
-
* as a URL,
|
|
492
|
-
*
|
|
493
|
-
* @param {string} app
|
|
494
|
-
* @returns {boolean} True if app is a supported URL
|
|
495
|
-
*/
|
|
496
|
-
function isSupportedUrl(app) {
|
|
497
|
-
try {
|
|
498
|
-
const { protocol } = parseAppLink(app);
|
|
499
|
-
return ['http:', 'https:'].includes(protocol);
|
|
500
|
-
}
|
|
501
|
-
catch {
|
|
502
|
-
return false;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Check if the given environment option is enabled
|
|
507
|
-
*
|
|
508
|
-
* @param {string} optionName Option name
|
|
509
|
-
* @param {boolean|null} [defaultValue=null] The value to return if the given env value
|
|
510
|
-
* is not set explicitly
|
|
511
|
-
* @returns {boolean} True if the option is enabled
|
|
512
|
-
*/
|
|
513
|
-
function isEnvOptionEnabled(optionName, defaultValue = null) {
|
|
514
|
-
const value = process.env[optionName];
|
|
515
|
-
if (!lodash_1.default.isNull(defaultValue) && lodash_1.default.isEmpty(value)) {
|
|
516
|
-
return defaultValue;
|
|
517
|
-
}
|
|
518
|
-
return !lodash_1.default.isEmpty(value) && !['0', 'false', 'no'].includes(lodash_1.default.toLower(value));
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
*
|
|
522
|
-
* @param {string} [envVarName]
|
|
523
|
-
* @param {number} defaultValue
|
|
524
|
-
* @returns {number}
|
|
525
|
-
*/
|
|
526
|
-
function toNaturalNumber(defaultValue, envVarName) {
|
|
527
|
-
if (!envVarName || lodash_1.default.isUndefined(process.env[envVarName])) {
|
|
528
|
-
return defaultValue;
|
|
529
|
-
}
|
|
530
|
-
const num = parseInt(`${process.env[envVarName]}`, 10);
|
|
531
|
-
return num > 0 ? num : defaultValue;
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* @param {string} app
|
|
535
|
-
* @param {string[]} supportedAppExtensions
|
|
536
|
-
* @returns {string}
|
|
537
|
-
*/
|
|
538
466
|
function verifyAppExtension(app, supportedAppExtensions) {
|
|
539
467
|
if (supportedAppExtensions.map(lodash_1.default.toLower).includes(lodash_1.default.toLower(node_path_1.default.extname(app)))) {
|
|
540
468
|
return app;
|
|
@@ -543,25 +471,12 @@ function verifyAppExtension(app, supportedAppExtensions) {
|
|
|
543
471
|
`${support_1.util.pluralize('extension', supportedAppExtensions.length, false)}: ` +
|
|
544
472
|
supportedAppExtensions);
|
|
545
473
|
}
|
|
546
|
-
/**
|
|
547
|
-
* @param {string} folderPath
|
|
548
|
-
* @returns {Promise<number>}
|
|
549
|
-
*/
|
|
550
474
|
async function calculateFolderIntegrity(folderPath) {
|
|
551
475
|
return (await support_1.fs.glob('**/*', { cwd: folderPath })).length;
|
|
552
476
|
}
|
|
553
|
-
/**
|
|
554
|
-
* @param {string} filePath
|
|
555
|
-
* @returns {Promise<string>}
|
|
556
|
-
*/
|
|
557
477
|
async function calculateFileIntegrity(filePath) {
|
|
558
478
|
return await support_1.fs.hash(filePath);
|
|
559
479
|
}
|
|
560
|
-
/**
|
|
561
|
-
* @param {string} currentPath
|
|
562
|
-
* @param {import('@appium/types').StringRecord} expectedIntegrity
|
|
563
|
-
* @returns {Promise<boolean>}
|
|
564
|
-
*/
|
|
565
480
|
async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
566
481
|
if (!(await support_1.fs.exists(currentPath))) {
|
|
567
482
|
return false;
|
|
@@ -574,10 +489,17 @@ async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
|
574
489
|
// more precise, but we don't need to be very precise here and also don't want to
|
|
575
490
|
// overuse RAM and have a performance drop.
|
|
576
491
|
return (await support_1.fs.stat(currentPath)).isDirectory()
|
|
577
|
-
? (await calculateFolderIntegrity(currentPath)) >= expectedIntegrity?.folder
|
|
492
|
+
? (await calculateFolderIntegrity(currentPath)) >= (expectedIntegrity?.folder ?? 0)
|
|
578
493
|
: (await calculateFileIntegrity(currentPath)) === expectedIntegrity?.file;
|
|
579
494
|
}
|
|
580
|
-
|
|
495
|
+
function toNaturalNumber(defaultValue, envVarName) {
|
|
496
|
+
if (!envVarName || lodash_1.default.isUndefined(process.env[envVarName])) {
|
|
497
|
+
return defaultValue;
|
|
498
|
+
}
|
|
499
|
+
const num = parseInt(`${process.env[envVarName]}`, 10);
|
|
500
|
+
return num > 0 ? num : defaultValue;
|
|
501
|
+
}
|
|
502
|
+
// #endregion
|
|
581
503
|
exports.default = {
|
|
582
504
|
configureApp,
|
|
583
505
|
isPackageOrBundle,
|
|
@@ -585,17 +507,4 @@ exports.default = {
|
|
|
585
507
|
parseCapsArray,
|
|
586
508
|
generateDriverLogPrefix,
|
|
587
509
|
};
|
|
588
|
-
/**
|
|
589
|
-
* @typedef RemoteAppProps
|
|
590
|
-
* @property {Date?} lastModified
|
|
591
|
-
* @property {boolean} immutable
|
|
592
|
-
* @property {number?} maxAge
|
|
593
|
-
* @property {string?} etag
|
|
594
|
-
*/
|
|
595
|
-
/**
|
|
596
|
-
* @typedef RemoteAppData Properties of the remote application (e.g. GET HTTP response) to be downloaded.
|
|
597
|
-
* @property {number} status The HTTP status of the response
|
|
598
|
-
* @property {import('stream').Readable} stream The HTTP response body represented as readable stream
|
|
599
|
-
* @property {import('axios').RawAxiosResponseHeaders | import('axios').AxiosResponseHeaders} headers HTTP response headers
|
|
600
|
-
*/
|
|
601
510
|
//# sourceMappingURL=helpers.js.map
|