@appium/support 2.56.1 → 2.57.0

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.
Files changed (66) hide show
  1. package/build/lib/env.d.ts +54 -0
  2. package/build/lib/env.d.ts.map +1 -0
  3. package/build/lib/fs.d.ts +221 -0
  4. package/build/lib/fs.d.ts.map +1 -0
  5. package/build/lib/fs.js +1 -1
  6. package/build/lib/image-util.d.ts +56 -0
  7. package/build/lib/image-util.d.ts.map +1 -0
  8. package/build/lib/image-util.js +2 -3
  9. package/build/lib/index.d.ts +38 -0
  10. package/build/lib/index.d.ts.map +1 -0
  11. package/build/lib/index.js +20 -14
  12. package/build/lib/log-internal.d.ts +74 -0
  13. package/build/lib/log-internal.d.ts.map +1 -0
  14. package/build/lib/log-internal.js +10 -18
  15. package/build/lib/logger.d.ts +3 -0
  16. package/build/lib/logger.d.ts.map +1 -0
  17. package/build/lib/logging.d.ts +45 -0
  18. package/build/lib/logging.d.ts.map +1 -0
  19. package/build/lib/logging.js +11 -13
  20. package/build/lib/mjpeg.d.ts +65 -0
  21. package/build/lib/mjpeg.d.ts.map +1 -0
  22. package/build/lib/mjpeg.js +11 -4
  23. package/build/lib/mkdirp.d.ts +3 -0
  24. package/build/lib/mkdirp.d.ts.map +1 -0
  25. package/build/lib/net.d.ts +95 -0
  26. package/build/lib/net.d.ts.map +1 -0
  27. package/build/lib/net.js +41 -23
  28. package/build/lib/node.d.ts +26 -0
  29. package/build/lib/node.d.ts.map +1 -0
  30. package/build/lib/node.js +4 -2
  31. package/build/lib/npm.d.ts +123 -0
  32. package/build/lib/npm.d.ts.map +1 -0
  33. package/build/lib/npm.js +18 -41
  34. package/build/lib/plist.d.ts +43 -0
  35. package/build/lib/plist.d.ts.map +1 -0
  36. package/build/lib/plist.js +1 -1
  37. package/build/lib/process.d.ts +3 -0
  38. package/build/lib/process.d.ts.map +1 -0
  39. package/build/lib/system.d.ts +7 -0
  40. package/build/lib/system.d.ts.map +1 -0
  41. package/build/lib/tempdir.d.ts +63 -0
  42. package/build/lib/tempdir.d.ts.map +1 -0
  43. package/build/lib/tempdir.js +3 -6
  44. package/build/lib/timing.d.ts +46 -0
  45. package/build/lib/timing.d.ts.map +1 -0
  46. package/build/lib/util.d.ts +183 -0
  47. package/build/lib/util.d.ts.map +1 -0
  48. package/build/lib/util.js +4 -8
  49. package/build/lib/zip.d.ts +180 -0
  50. package/build/lib/zip.d.ts.map +1 -0
  51. package/build/lib/zip.js +6 -2
  52. package/build/tsconfig.tsbuildinfo +1 -0
  53. package/lib/fs.js +9 -4
  54. package/lib/image-util.js +23 -7
  55. package/lib/index.js +2 -8
  56. package/lib/log-internal.js +31 -38
  57. package/lib/logging.js +40 -16
  58. package/lib/mjpeg.js +14 -5
  59. package/lib/net.js +116 -60
  60. package/lib/node.js +4 -4
  61. package/lib/npm.js +33 -90
  62. package/lib/plist.js +3 -1
  63. package/lib/tempdir.js +8 -7
  64. package/lib/util.js +10 -11
  65. package/lib/zip.js +24 -13
  66. package/package.json +14 -7
@@ -3,12 +3,6 @@ import _ from 'lodash';
3
3
 
4
4
  const DEFAULT_REPLACER = '**SECURE**';
5
5
 
6
- /**
7
- * @typedef SecureValuePreprocessingRule
8
- * @property {RegExp} pattern The parsed pattern which is going to be used for replacement
9
- * @property {string} replacer [DEFAULT_SECURE_REPLACER] The replacer value to use. By default
10
- * equals to `DEFAULT_SECURE_REPLACER`
11
- */
12
6
 
13
7
  class SecureValuesPreprocessor {
14
8
  constructor () {
@@ -23,19 +17,6 @@ class SecureValuesPreprocessor {
23
17
  return this._rules;
24
18
  }
25
19
 
26
- /**
27
- * @typedef Rule
28
- * @property {string} pattern A valid RegExp pattern to be replaced
29
- * @property {string} text A text match to replace. Either this property or the
30
- * above one must be provided. `pattern` has priority over `text` if both are provided.
31
- * @property {string} flags ['g'] Regular expression flags for the given pattern.
32
- * Supported flag are the same as for the standard JavaScript RegExp constructor:
33
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Advanced_searching_with_flags_2
34
- * The 'g' (global matching) is always enabled though.
35
- * @property {string} replacer [DEFAULT_SECURE_REPLACER] The replacer value to use. By default
36
- * equals to `DEFAULT_SECURE_REPLACER`
37
- */
38
-
39
20
  /**
40
21
  * Parses single rule from the given JSON file
41
22
  *
@@ -45,32 +26,28 @@ class SecureValuesPreprocessor {
45
26
  * @returns {SecureValuePreprocessingRule} The parsed rule
46
27
  */
47
28
  parseRule (rule) {
48
- const raiseError = (msg) => {
49
- throw new Error(`${JSON.stringify(rule)} -> ${msg}`);
50
- };
51
-
52
29
  let pattern;
53
30
  let replacer = DEFAULT_REPLACER;
54
31
  let flags = ['g'];
55
32
  if (_.isString(rule)) {
56
33
  if (rule.length === 0) {
57
- raiseError('The value must not be empty');
34
+ throw new Error(`${JSON.stringify(rule)} -> The value must not be empty`);
58
35
  }
59
36
  pattern = `\\b${_.escapeRegExp(rule)}\\b`;
60
37
  } else if (_.isPlainObject(rule)) {
61
38
  if (_.has(rule, 'pattern')) {
62
39
  if (!_.isString(rule.pattern) || rule.pattern.length === 0) {
63
- raiseError(`The value of 'pattern' must be a valid non-empty string`);
40
+ throw new Error(`${JSON.stringify(rule)} -> The value of 'pattern' must be a valid non-empty string`);
64
41
  }
65
42
  pattern = rule.pattern;
66
43
  } else if (_.has(rule, 'text')) {
67
44
  if (!_.isString(rule.text) || rule.text.length === 0) {
68
- raiseError(`The value of 'text' must be a valid non-empty string`);
45
+ throw new Error(`${JSON.stringify(rule)} -> The value of 'text' must be a valid non-empty string`);
69
46
  }
70
47
  pattern = `\\b${_.escapeRegExp(rule.text)}\\b`;
71
48
  }
72
49
  if (!pattern) {
73
- raiseError(`Must either have a field named 'pattern' or 'text'`);
50
+ throw new Error(`${JSON.stringify(rule)} -> Must either have a field named 'pattern' or 'text'`);
74
51
  }
75
52
 
76
53
  if (_.has(rule, 'flags')) {
@@ -87,27 +64,23 @@ class SecureValuesPreprocessor {
87
64
  replacer = rule.replacer;
88
65
  }
89
66
  } else {
90
- raiseError('Must either be a string or an object');
67
+ throw new Error(`${JSON.stringify(rule)} -> Must either be a string or an object`);
91
68
  }
92
69
 
93
- try {
94
- return {
95
- pattern: new RegExp(pattern, flags.join('')),
96
- replacer,
97
- };
98
- } catch (e) {
99
- raiseError(e.message);
100
- }
70
+ return {
71
+ pattern: new RegExp(pattern, flags.join('')),
72
+ replacer,
73
+ };
101
74
  }
102
75
 
103
76
  /**
104
77
  * Loads rules from the given JSON file
105
78
  *
106
- * @param {string|string[]|Rule[]>} source The full path to the JSON file containing secure
79
+ * @param {string|string[]|Rule[]} source The full path to the JSON file containing secure
107
80
  * values replacement rules or the rules themselves represented as an array
108
81
  * @throws {Error} If the format of the source file is invalid or
109
82
  * it does not exist
110
- * @returns {Array<string>} The list of issues found while parsing each rule.
83
+ * @returns {Promise<string[]>} The list of issues found while parsing each rule.
111
84
  * An empty list is returned if no rule parsing issues were found
112
85
  */
113
86
  async loadRules (source) {
@@ -165,3 +138,23 @@ const SECURE_VALUES_PREPROCESSOR = new SecureValuesPreprocessor();
165
138
 
166
139
  export { SECURE_VALUES_PREPROCESSOR, SecureValuesPreprocessor };
167
140
  export default SECURE_VALUES_PREPROCESSOR;
141
+
142
+ /**
143
+ * @typedef Rule
144
+ * @property {string} pattern A valid RegExp pattern to be replaced
145
+ * @property {string} text A text match to replace. Either this property or the
146
+ * above one must be provided. `pattern` has priority over `text` if both are provided.
147
+ * @property {string} [flags] Regular expression flags for the given pattern.
148
+ * Supported flag are the same as for the standard JavaScript RegExp constructor:
149
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Advanced_searching_with_flags_2
150
+ * The 'g' (global matching) is always enabled though.
151
+ * @property {string} [replacer] The replacer value to use. By default
152
+ * equals to `DEFAULT_SECURE_REPLACER`
153
+ */
154
+
155
+ /**
156
+ * @typedef SecureValuePreprocessingRule
157
+ * @property {RegExp} pattern The parsed pattern which is going to be used for replacement
158
+ * @property {string} [replacer] The replacer value to use. By default
159
+ * equals to `DEFAULT_SECURE_REPLACER`
160
+ */
package/lib/logging.js CHANGED
@@ -22,10 +22,14 @@ function patchLogger (logger) {
22
22
  }
23
23
  }
24
24
 
25
+ /**
26
+ *
27
+ * @returns {[npmlog.Logger, boolean]}
28
+ */
25
29
  function _getLogger () {
26
30
  // check if the user set the `_TESTING` or `_FORCE_LOGS` flag
27
- const testingMode = parseInt(process.env._TESTING, 10) === 1;
28
- const forceLogMode = parseInt(process.env._FORCE_LOGS, 10) === 1;
31
+ const testingMode = process.env._TESTING === '1';
32
+ const forceLogMode = process.env._FORCE_LOGS === '1';
29
33
 
30
34
  // if is possible that there is a logger instance that is already around,
31
35
  // in which case we want t o use that
@@ -44,19 +48,32 @@ function _getLogger () {
44
48
  return [logger, usingGlobalLog];
45
49
  }
46
50
 
51
+ /**
52
+ * @param {Prefix?} prefix
53
+ * @param {boolean} logTimestamp whether to include timestamps into log prefixes
54
+ * @returns {string}
55
+ */
47
56
  function getActualPrefix (prefix, logTimestamp = false) {
48
- let actualPrefix = _.isFunction(prefix) ? prefix() : prefix;
49
- if (logTimestamp) {
50
- actualPrefix = `[${moment().format(PREFIX_TIMESTAMP_FORMAT)}] ${actualPrefix}`;
51
- }
52
- return actualPrefix;
57
+ const result = (_.isFunction(prefix) ? prefix() : prefix) ?? '';
58
+ return logTimestamp
59
+ ? `[${moment().format(PREFIX_TIMESTAMP_FORMAT)}] ${result}`
60
+ : result;
53
61
  }
54
62
 
63
+ /**
64
+ *
65
+ * @param {Prefix?} prefix
66
+ * @returns {AppiumLogger}
67
+ */
55
68
  function getLogger (prefix = null) {
56
69
  let [logger, usingGlobalLog] = _getLogger();
57
70
 
58
71
  // wrap the logger so that we can catch and modify any logging
59
- let wrappedLogger = {unwrap: () => logger};
72
+ let wrappedLogger = {
73
+ unwrap: () => logger,
74
+ levels: NPM_LEVELS,
75
+ prefix,
76
+ };
60
77
 
61
78
  // allow access to the level of the underlying logger
62
79
  Object.defineProperty(wrappedLogger, 'level', {
@@ -70,12 +87,12 @@ function getLogger (prefix = null) {
70
87
  configurable: true
71
88
  });
72
89
 
73
- const logTimestamp = parseInt(process.env._LOG_TIMESTAMP, 10) === 1;
90
+ const logTimestamp = process.env._LOG_TIMESTAMP === '1';
74
91
 
75
92
  // add all the levels from `npmlog`, and map to the underlying logger
76
93
  for (const level of NPM_LEVELS) {
77
94
  wrappedLogger[level] = function (...args) {
78
- const actualPrefix = getActualPrefix(prefix, logTimestamp);
95
+ const actualPrefix = getActualPrefix(this.prefix, logTimestamp);
79
96
  for (const arg of args) {
80
97
  const out = (_.isError(arg) && arg.stack) ? arg.stack : `${arg}`;
81
98
  for (const line of out.split('\n')) {
@@ -99,15 +116,14 @@ function getLogger (prefix = null) {
99
116
  // package set the log level
100
117
  wrappedLogger.level = 'verbose';
101
118
  }
102
- wrappedLogger.levels = NPM_LEVELS;
103
- return wrappedLogger;
119
+ return /** @type {AppiumLogger} */(wrappedLogger);
104
120
  }
105
121
 
106
122
  /**
107
123
  * @typedef LoadResult
108
- * @property {List<string>} issues The list of rule parsing issues (one item per rule).
124
+ * @property {string[]} issues The list of rule parsing issues (one item per rule).
109
125
  * Rules with issues are skipped. An empty list is returned if no parsing issues exist.
110
- * @property {List<SecureValuePreprocessingRule>} rules The list of successfully loaded
126
+ * @property {import('./log-internal').SecureValuePreprocessingRule[]} rules The list of successfully loaded
111
127
  * replacement rules. The list could be empty if no rules were loaded.
112
128
  */
113
129
 
@@ -117,12 +133,12 @@ function getLogger (prefix = null) {
117
133
  * appear in Appium logs.
118
134
  * Each call to this method replaces the previously loaded rules if any existed.
119
135
  *
120
- * @param {string|string[]|Rule[]} rulesJsonPath The full path to the JSON file containing
136
+ * @param {string|string[]|import('./log-internal').Rule[]} rulesJsonPath The full path to the JSON file containing
121
137
  * the replacement rules. Each rule could either be a string to be replaced
122
138
  * or an object with predefined properties. See the `Rule` type definition in
123
139
  * `log-internals.js` to get more details on its format.
124
140
  * @throws {Error} If the given file cannot be loaded
125
- * @returns {LoadResult}
141
+ * @returns {Promise<LoadResult>}
126
142
  */
127
143
  async function loadSecureValuesPreprocessingRules (rulesJsonPath) {
128
144
  const issues = await SECURE_VALUES_PREPROCESSOR.loadRules(rulesJsonPath);
@@ -137,3 +153,11 @@ const log = getLogger();
137
153
 
138
154
  export { log, patchLogger, getLogger, loadSecureValuesPreprocessingRules };
139
155
  export default log;
156
+
157
+ /**
158
+ * @typedef {import('@appium/types').Prefix} Prefix
159
+ */
160
+
161
+ /**
162
+ * @typedef {import('@appium/types').AppiumLogger} AppiumLogger
163
+ */
package/lib/mjpeg.js CHANGED
@@ -30,6 +30,11 @@ const MJPEG_SERVER_TIMEOUT_MS = 10000;
30
30
  /** Class which stores the last bit of data streamed into it */
31
31
  class MJpegStream extends Writable {
32
32
 
33
+ /**
34
+ * @type {number}
35
+ */
36
+ updateCount = 0;
37
+
33
38
  /**
34
39
  * Create an MJpegStream
35
40
  * @param {string} mJpegUrl - URL of MJPEG-over-HTTP stream
@@ -52,24 +57,26 @@ class MJpegStream extends Writable {
52
57
  * or `null` if no image can be parsed
53
58
  */
54
59
  get lastChunkBase64 () {
60
+ const lastChunk = /** @type {Buffer} */(this.lastChunk);
55
61
  return !_.isEmpty(this.lastChunk) && _.isBuffer(this.lastChunk)
56
- ? this.lastChunk.toString('base64')
62
+ ? lastChunk.toString('base64')
57
63
  : null;
58
64
  }
59
65
 
60
66
  /**
61
67
  * Get the PNG version of the JPEG buffer
62
68
  *
63
- * @returns {?Buffer} PNG image data or `null` if no PNG
69
+ * @returns {Promise<Buffer?>} PNG image data or `null` if no PNG
64
70
  * image can be parsed
65
71
  */
66
72
  async lastChunkPNG () {
67
- if (_.isEmpty(this.lastChunk) || !_.isBuffer(this.lastChunk)) {
73
+ const lastChunk = /** @type {Buffer} */(this.lastChunk);
74
+ if (_.isEmpty(lastChunk) || !_.isBuffer(lastChunk)) {
68
75
  return null;
69
76
  }
70
77
 
71
78
  try {
72
- const jpg = await getJimpImage(this.lastChunk);
79
+ const jpg = await getJimpImage(lastChunk);
73
80
  return await jpg.getBuffer(MIME_PNG);
74
81
  } catch (e) {
75
82
  return null;
@@ -79,7 +86,7 @@ class MJpegStream extends Writable {
79
86
  /**
80
87
  * Get the base64-encoded version of the PNG
81
88
  *
82
- * @returns {?string} base64-encoded PNG image data
89
+ * @returns {Promise<string?>} base64-encoded PNG image data
83
90
  * or `null` if no image can be parsed
84
91
  */
85
92
  async lastChunkPNGBase64 () {
@@ -185,6 +192,8 @@ class MJpegStream extends Writable {
185
192
  this.registerStartSuccess();
186
193
  this.registerStartSuccess = null;
187
194
  }
195
+
196
+ return true;
188
197
  }
189
198
  }
190
199
 
package/lib/net.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import _ from 'lodash';
2
2
  import fs from './fs';
3
- import url from 'url';
4
3
  import B from 'bluebird';
5
4
  import { toReadableSizeString } from './util';
6
5
  import log from './logger';
@@ -11,19 +10,29 @@ import FormData from 'form-data';
11
10
 
12
11
  const DEFAULT_TIMEOUT_MS = 4 * 60 * 1000;
13
12
 
13
+ /**
14
+ * Converts {@linkcode AuthCredentials} to credentials understood by {@linkcode axios}.
15
+ * @param {AuthCredentials | import('axios').AxiosBasicCredentials} auth
16
+ * @returns {import('axios').AxiosBasicCredentials?}
17
+ */
14
18
  function toAxiosAuth (auth) {
15
19
  if (!_.isPlainObject(auth)) {
16
20
  return null;
17
21
  }
18
22
 
19
23
  const axiosAuth = {
20
- username: auth.username || auth.user,
21
- password: auth.password || auth.pass,
24
+ username: _.get(auth, 'username', _.get(auth, 'user')),
25
+ password: _.get(auth, 'password', _.get(auth, 'pass')),
22
26
  };
23
27
  return (axiosAuth.username && axiosAuth.password) ? axiosAuth : null;
24
28
  }
25
29
 
26
- async function uploadFileToHttp (localFileStream, parsedUri, uploadOptions = {}) {
30
+ /**
31
+ * @param {NodeJS.ReadableStream} localFileStream
32
+ * @param {URL} parsedUri
33
+ * @param {HttpUploadOptions & NetOptions} [uploadOptions]
34
+ */
35
+ async function uploadFileToHttp (localFileStream, parsedUri, uploadOptions = /** @type {HttpUploadOptions & NetOptions} */({})) {
27
36
  const {
28
37
  method = 'POST',
29
38
  timeout = DEFAULT_TIMEOUT_MS,
@@ -34,6 +43,7 @@ async function uploadFileToHttp (localFileStream, parsedUri, uploadOptions = {})
34
43
  } = uploadOptions;
35
44
  const { href } = parsedUri;
36
45
 
46
+ /** @type {import('axios').AxiosRequestConfig} */
37
47
  const requestOpts = {
38
48
  url: href,
39
49
  method,
@@ -61,8 +71,10 @@ async function uploadFileToHttp (localFileStream, parsedUri, uploadOptions = {})
61
71
  }
62
72
  }
63
73
  }
64
- requestOpts.headers = Object.assign({}, _.isPlainObject(headers) ? headers : {},
65
- form.getHeaders());
74
+ requestOpts.headers = {
75
+ ...(_.isPlainObject(headers) ? headers : {}),
76
+ ...form.getHeaders()
77
+ };
66
78
  requestOpts.data = form;
67
79
  } else {
68
80
  if (_.isPlainObject(headers)) {
@@ -77,11 +89,14 @@ async function uploadFileToHttp (localFileStream, parsedUri, uploadOptions = {})
77
89
  log.info(`Server response: ${status} ${statusText}`);
78
90
  }
79
91
 
80
- async function uploadFileToFtp (localFileStream, parsedUri, uploadOptions = {}) {
92
+ /**
93
+ * @param {string | Buffer | NodeJS.ReadableStream} localFileStream
94
+ * @param {URL} parsedUri
95
+ * @param {NotHttpUploadOptions & NetOptions} [uploadOptions]
96
+ */
97
+ async function uploadFileToFtp (localFileStream, parsedUri, uploadOptions = /** @type {NotHttpUploadOptions & NetOptions} */({})) {
81
98
  const {
82
99
  auth,
83
- user,
84
- pass,
85
100
  } = uploadOptions;
86
101
  const {
87
102
  hostname,
@@ -92,11 +107,11 @@ async function uploadFileToFtp (localFileStream, parsedUri, uploadOptions = {})
92
107
 
93
108
  const ftpOpts = {
94
109
  host: hostname,
95
- port: port || 21,
110
+ port: !_.isUndefined(port) ? _.parseInt(port) : 21,
96
111
  };
97
- if ((auth?.user && auth?.pass) || (user && pass)) {
98
- ftpOpts.user = auth?.user || user;
99
- ftpOpts.pass = auth?.pass || pass;
112
+ if (auth?.user && auth?.pass) {
113
+ ftpOpts.user = auth.user;
114
+ ftpOpts.pass = auth.pass;
100
115
  }
101
116
  log.debug(`${protocol} upload options: ${JSON.stringify(ftpOpts)}`);
102
117
  return await new B((resolve, reject) => {
@@ -111,42 +126,44 @@ async function uploadFileToFtp (localFileStream, parsedUri, uploadOptions = {})
111
126
  }
112
127
 
113
128
  /**
114
- * @typedef AuthCredentials
115
- * @property {string} user - Non-empty user name
116
- * @property {string} pass - Non-empty password
117
- */
118
-
119
- /**
120
- * @typedef FtpUploadOptions
121
- * @property {boolean} isMetered [true] - Whether to log the actual upload performance
122
- * (e.g. timings and speed)
123
- * @property {AuthCredentials} auth
129
+ * Returns `true` if params are valid for {@linkcode uploadFileToHttp}.
130
+ * @param {any} opts
131
+ * @param {URL} url
132
+ * @returns {opts is HttpUploadOptions & NetOptions}
124
133
  */
134
+ function isHttpUploadOptions (opts, url) {
135
+ try {
136
+ const {protocol} = new URL(url);
137
+ return protocol === 'http:' || protocol === 'https:';
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
125
142
 
126
143
  /**
127
- * @typedef HttpUploadOptions
128
- * @property {boolean} isMetered [true] - Whether to log the actual upload performance
129
- * (e.g. timings and speed)
130
- * @property {string} method [POST] - The HTTP method used for file upload
131
- * @property {AuthCredentials} auth
132
- * @property {number} timeout [240000] - The actual request timeout in milliseconds
133
- * @property {Object} headers - Additional request headers mapping
134
- * @property {?string} fileFieldName [file] - The name of the form field containing the file
135
- * content to be uploaded. Any falsy value make the request to use non-multipart upload
136
- * @property {Array<Pair>|Object} formFields - The additional form fields
137
- * to be included into the upload request. This property is only considered if
138
- * `fileFieldName` is set
144
+ * Returns `true` if params are valid for {@linkcode uploadFileToFtp}.
145
+ * @param {any} opts
146
+ * @param {URL} url
147
+ * @returns {opts is NotHttpUploadOptions & NetOptions}
139
148
  */
140
-
149
+ function isNotHttpUploadOptions (opts, url) {
150
+ try {
151
+ const {protocol} = new URL(url);
152
+ return protocol === 'ftp:';
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
141
157
  /**
142
158
  * Uploads the given file to a remote location. HTTP(S) and FTP
143
159
  * protocols are supported.
144
160
  *
145
161
  * @param {string} localPath - The path to a file on the local storage.
146
162
  * @param {string} remoteUri - The remote URI to upload the file to.
147
- * @param {?FtpUploadOptions|HttpUploadOptions} uploadOptions
163
+ * @param {(HttpUploadOptions|NotHttpUploadOptions) & NetOptions} [uploadOptions]
164
+ * @returns {Promise<void>}
148
165
  */
149
- async function uploadFile (localPath, remoteUri, uploadOptions = {}) {
166
+ async function uploadFile (localPath, remoteUri, uploadOptions = /** @type {(HttpUploadOptions|NotHttpUploadOptions) & NetOptions} */({})) {
150
167
  if (!await fs.exists(localPath)) {
151
168
  throw new Error (`'${localPath}' does not exists or is not accessible`);
152
169
  }
@@ -154,26 +171,25 @@ async function uploadFile (localPath, remoteUri, uploadOptions = {}) {
154
171
  const {
155
172
  isMetered = true,
156
173
  } = uploadOptions;
157
-
158
- const parsedUri = url.parse(remoteUri);
174
+ const url = new URL(remoteUri);
159
175
  const {size} = await fs.stat(localPath);
160
176
  if (isMetered) {
161
177
  log.info(`Uploading '${localPath}' of ${toReadableSizeString(size)} size to '${remoteUri}'`);
162
178
  }
163
179
  const timer = new Timer().start();
164
- if (['http:', 'https:'].includes(parsedUri.protocol)) {
180
+ if (isHttpUploadOptions(uploadOptions, url)) {
165
181
  if (!uploadOptions.fileFieldName) {
166
- uploadOptions.headers = Object.assign({},
167
- _.isPlainObject(uploadOptions.headers) ? uploadOptions.headers : {},
168
- {'Content-Length': size}
169
- );
182
+ uploadOptions.headers = {
183
+ ...(_.isPlainObject(uploadOptions.headers) ? uploadOptions.headers : {}),
184
+ 'Content-Length': size
185
+ };
170
186
  }
171
- await uploadFileToHttp(fs.createReadStream(localPath), parsedUri, uploadOptions);
172
- } else if (parsedUri.protocol === 'ftp:') {
173
- await uploadFileToFtp(fs.createReadStream(localPath), parsedUri, uploadOptions);
187
+ await uploadFileToHttp(fs.createReadStream(localPath), url, uploadOptions);
188
+ } else if (isNotHttpUploadOptions(uploadOptions, url)) {
189
+ await uploadFileToFtp(fs.createReadStream(localPath), url, uploadOptions);
174
190
  } else {
175
191
  throw new Error(`Cannot upload the file at '${localPath}' to '${remoteUri}'. ` +
176
- `Unsupported remote protocol '${parsedUri.protocol}'. ` +
192
+ `Unsupported remote protocol '${url.protocol}'. ` +
177
193
  `Only http/https and ftp/ftps protocols are supported.`);
178
194
  }
179
195
  if (isMetered) {
@@ -182,24 +198,15 @@ async function uploadFile (localPath, remoteUri, uploadOptions = {}) {
182
198
  }
183
199
  }
184
200
 
185
- /**
186
- * @typedef DownloadOptions
187
- * @property {boolean} isMetered [true] - Whether to log the actual download performance
188
- * (e.g. timings and speed)
189
- * @property {AuthCredentials} auth
190
- * @property {number} timeout [240000] - The actual request timeout in milliseconds
191
- * @property {Object} headers - Request headers mapping
192
- */
193
-
194
201
  /**
195
202
  * Downloads the given file via HTTP(S)
196
203
  *
197
204
  * @param {string} remoteUrl - The remote url
198
205
  * @param {string} dstPath - The local path to download the file to
199
- * @param {?DownloadOptions} downloadOptions
206
+ * @param {DownloadOptions & NetOptions} [downloadOptions]
200
207
  * @throws {Error} If download operation fails
201
208
  */
202
- async function downloadFile (remoteUrl, dstPath, downloadOptions = {}) {
209
+ async function downloadFile (remoteUrl, dstPath, downloadOptions = /** @type {DownloadOptions & NetOptions} */({})) {
203
210
  const {
204
211
  isMetered = true,
205
212
  auth,
@@ -207,6 +214,9 @@ async function downloadFile (remoteUrl, dstPath, downloadOptions = {}) {
207
214
  headers,
208
215
  } = downloadOptions;
209
216
 
217
+ /**
218
+ * @type {import('axios').AxiosRequestConfig}
219
+ */
210
220
  const requestOpts = {
211
221
  url: remoteUrl,
212
222
  responseType: 'stream',
@@ -261,3 +271,49 @@ async function downloadFile (remoteUrl, dstPath, downloadOptions = {}) {
261
271
  }
262
272
 
263
273
  export { uploadFile, downloadFile };
274
+
275
+ /**
276
+ * Common options for {@linkcode uploadFile} and {@linkcode downloadFile}.
277
+ * @typedef NetOptions
278
+ * @property {boolean} [isMetered=true] - Whether to log the actual download performance
279
+ * (e.g. timings and speed)
280
+ * @property {AuthCredentials} auth
281
+ */
282
+
283
+ /**
284
+ * Specific options for {@linkcode downloadFile}.
285
+ * @typedef DownloadOptions
286
+ * @property {number} [timeout] - The actual request timeout in milliseconds; defaults to {@linkcode DEFAULT_TIMEOUT_MS}
287
+ * @property {Record<string,any>} headers - Request headers mapping
288
+ */
289
+
290
+ /**
291
+ * Basic auth credentials; used by {@linkcode NetOptions}.
292
+ * @typedef AuthCredentials
293
+ * @property {string} user - Non-empty user name
294
+ * @property {string} pass - Non-empty password
295
+ */
296
+
297
+ /**
298
+ * This type is used in {@linkcode uploadFile} if the remote location uses the `ftp` protocol, and distinguishes the type from {@linkcode HttpUploadOptions}.
299
+ * @typedef NotHttpUploadOptions
300
+ * @property {never} headers
301
+ * @property {never} method
302
+ * @property {never} timeout
303
+ * @property {never} fileFieldName
304
+ * @property {never} formFields
305
+ */
306
+
307
+ /**
308
+ * Specific options for {@linkcode uploadFile} if the remote location uses the `http(s)` protocol
309
+ * @typedef HttpUploadOptions
310
+ * @property {Record<string,any>} headers - Additional request headers mapping
311
+ * @property {import('axios').Method} [method='POST'] - The HTTP method used for file upload
312
+ * @property {number} [timeout] - The actual request timeout in milliseconds; defaults to {@linkcode DEFAULT_TIMEOUT_MS}
313
+ * @property {string} [fileFieldName='file'] - The name of the form field containing the file
314
+ * content to be uploaded. Any falsy value make the request to use non-multipart upload
315
+ * @property {Record<string,any>} [formFields] - The additional form fields
316
+ * to be included into the upload request. This property is only considered if
317
+ * `fileFieldName` is set
318
+ */
319
+
package/lib/node.js CHANGED
@@ -14,7 +14,7 @@ const ECMA_SIZES = Object.freeze({
14
14
  /**
15
15
  * Internal utility to link global package to local context
16
16
  *
17
- * @returns {string} - name of the package to link
17
+ * @param {string} packageName - name of the package to link
18
18
  * @throws {Error} If the command fails
19
19
  */
20
20
  async function linkGlobalPackage (packageName) {
@@ -40,7 +40,7 @@ async function linkGlobalPackage (packageName) {
40
40
  * this will attempt to link the package and then re-require it
41
41
  *
42
42
  * @param {string} packageName - the name of the package to be required
43
- * @returns {object} - the package object
43
+ * @returns {Promise<unknown>} - the package object
44
44
  * @throws {Error} If the package is not found locally or globally
45
45
  */
46
46
  async function requirePackage (packageName) {
@@ -54,7 +54,7 @@ async function requirePackage (packageName) {
54
54
 
55
55
  // second, get it from where it ought to be in the global node_modules
56
56
  try {
57
- const globalPackageName = path.resolve(process.env.npm_config_prefix, 'lib', 'node_modules', packageName);
57
+ const globalPackageName = path.resolve(process.env.npm_config_prefix ?? '', 'lib', 'node_modules', packageName);
58
58
  log.debug(`Loading global package '${globalPackageName}'`);
59
59
  return require(globalPackageName);
60
60
  } catch (err) {
@@ -128,7 +128,7 @@ function getCalculator (seen) {
128
128
  return ECMA_SIZES.NUMBER;
129
129
  case 'symbol':
130
130
  return _.isFunction(Symbol.keyFor) && Symbol.keyFor(obj)
131
- ? Symbol.keyFor(obj).length * ECMA_SIZES.STRING
131
+ ? /** @type {string} */(Symbol.keyFor(obj)).length * ECMA_SIZES.STRING
132
132
  : (obj.toString().length - 8) * ECMA_SIZES.STRING;
133
133
  case 'object':
134
134
  return _.isArray(obj)