@speedkit/cli 3.20.0 → 3.22.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ # [3.22.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v3.21.0...v3.22.0) (2026-01-30)
2
+
3
+
4
+ ### Features
5
+
6
+ * add socksConnector to run localDocHandler in proxyMode ([eb3d3a2](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/eb3d3a2ce197aad600506df38032ba6e1fc25594))
7
+ * set correct variations ([6021e22](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/6021e2258a9afe61ee9fced046d1f944b0134d57))
8
+ * update crawler for docHandler to respect variations ([5880526](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/58805265156bde07b61bcb5599dd19cacffc7680))
9
+
10
+ # [3.21.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v3.20.0...v3.21.0) (2026-01-30)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **wizard:** soft to hard nav conversion ([501c490](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/501c49092a50d91a2b7986ffa41ea6916282c005))
16
+ * **wizard:** soft to hard nav conversion ([9801d0f](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/9801d0f90b6cbf4f2d895aed56483c505b84a8cc))
17
+
18
+
19
+ ### Features
20
+
21
+ * **customer-config:** prevent soft2hard nav conversion for product variant switches ([66c0923](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/66c0923a22e15965c26c11a0ae4370170ec4ccf5))
22
+ * **customer-config:** remove conditional for predictive preloading as it is not necessary ([eead957](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/eead957e2c8b3e094edd7204858c2cb87036c1e7))
23
+ * **customer-config:** update general logic for soft2hard nav conversion ([05a5a61](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/05a5a611476b9dbda9d3afe22a15a7dc637d4d66))
24
+
1
25
  # [3.20.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v3.19.0...v3.20.0) (2026-01-06)
2
26
 
3
27
 
package/README.md CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @speedkit/cli
21
21
  $ sk COMMAND
22
22
  running command...
23
23
  $ sk (--version)
24
- @speedkit/cli/3.20.0 linux-x64 node-v20.19.6
24
+ @speedkit/cli/3.22.0 linux-x64 node-v20.20.0
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -37,6 +37,7 @@ export interface CustomerConfigSettings {
37
37
  withGoogleOptimize?: boolean;
38
38
  activateCfRocketLoaderPlugin?: boolean;
39
39
  hasSoftNavigations?: boolean;
40
+ convertSoftToHardNavigations?: boolean;
40
41
  isPOV?: boolean;
41
42
  isShopify?: boolean;
42
43
  isShopware?: boolean;
@@ -136,7 +136,7 @@ class CustomerConfigService {
136
136
  return true;
137
137
  }
138
138
  async getConfigSettings() {
139
- let { production, staging, activateScopedDeployments, subRouteScope, includeServiceWorker, activateImageOptimisation, removeLazyLoading, addSSR, addSSRInnerShadowDomSupport, addDeviceDetection, useGATracking, useScrapingBee, withGoogleOptimize, activateCfRocketLoaderPlugin, hasSoftNavigations, isShopify, isShopware, isSalesforce, isOxid, isPlentymarkets, isPOV, } = {};
139
+ let { production, staging, activateScopedDeployments, subRouteScope, includeServiceWorker, activateImageOptimisation, removeLazyLoading, addSSR, addSSRInnerShadowDomSupport, addDeviceDetection, useGATracking, useScrapingBee, withGoogleOptimize, activateCfRocketLoaderPlugin, hasSoftNavigations, convertSoftToHardNavigations, isShopify, isShopware, isSalesforce, isOxid, isPlentymarkets, isPOV, } = {};
140
140
  this.appName = await this.cli.prompt(`[App Name] Enter desired SK app name:`, {
141
141
  validator: (input) => /^[\da-z]+(?:-[\da-z]+)*$/.test(input) ? "" : "No valid kebab-case",
142
142
  defaultAnswer: this.appName,
@@ -198,6 +198,9 @@ class CustomerConfigService {
198
198
  useGATracking = await this.cli.confirm("[Tracking] Add tracking based on Google Analytics' DataLayer?");
199
199
  }
200
200
  hasSoftNavigations = await this.cli.confirm("[Tracking] Add tracking for Soft Navigations?", false);
201
+ if (hasSoftNavigations) {
202
+ convertSoftToHardNavigations = await this.cli.confirm("[Soft2Hard Navigations] Should soft navigations be converted to hard navigations?", false);
203
+ }
201
204
  isPOV = await this.cli.confirm("[Tracking] Is a POV (proof of value) anticipated, so that additional tracking is needed?", false);
202
205
  withGoogleOptimize = await this.cli.confirm("[Services] Is Google Optimize used?", false);
203
206
  activateCfRocketLoaderPlugin = await this.cli.confirm("[Services] Is Cloudflare's Rocket Loader used, for which we need to activate our rocket loader plugin?", false);
@@ -218,6 +221,7 @@ class CustomerConfigService {
218
221
  withGoogleOptimize,
219
222
  activateCfRocketLoaderPlugin,
220
223
  hasSoftNavigations,
224
+ convertSoftToHardNavigations,
221
225
  isShopify,
222
226
  isShopware,
223
227
  isSalesforce,
@@ -269,6 +269,22 @@ import { PredictivePreloading } from 'predictive-preloading/predictivePreloading
269
269
  },
270
270
  {{/if}}
271
271
  {{/if}}
272
+ {{#if convertSoftToHardNavigations}}
273
+ {
274
+ on: "domInteractive",
275
+ track: () => {
276
+ if (
277
+ // check that dashboard is not disabled and user is in group A
278
+ window?.SpeedKit.lastNavigate.disconnectCause.indexOf("DashboardDisabled") > -1 ||
279
+ !window?.SpeedKit?.skSupported ||
280
+ SpeedKit.group !== "A"
281
+ ) {
282
+ return;
283
+ }
284
+ convertSoftToHardNavigation();
285
+ },
286
+ },
287
+ {{/if}}
272
288
  {{#if isPlentymarkets}}
273
289
  {
274
290
  on: "domInteractive",
@@ -367,12 +383,81 @@ import { PredictivePreloading } from 'predictive-preloading/predictivePreloading
367
383
  });
368
384
  {{/if}}
369
385
 
370
- {{#if hasSoftNavigations}}
371
- new PredictivePreloading({
372
- // TODO: configure according to the needs of the customer e.g. preload API-calls, soft-nav to hard-nav conversion...
373
- });
374
- {{else}}
386
+ {{#if convertSoftToHardNavigations}}
387
+ function softNavShouldBeConverted(url) {
388
+ // no link item found
389
+ if (!url || url === location.href) {
390
+ return false;
391
+ }
392
+
393
+ try {
394
+ // Always resolve the target relative to the current location
395
+ var resolvedTarget = new URL(url, location.href);
396
+
397
+ if (location.origin !== resolvedTarget?.origin) {
398
+ return false;
399
+ }
400
+
401
+ // must be a different path
402
+ if (location.pathname === resolvedTarget?.pathname) {
403
+ return false;
404
+ }
405
+
406
+ if (!SpeedKit?.isPageTypeEnabled(resolvedTarget.href)) {
407
+ return false;
408
+ }
409
+
410
+ if (
411
+ // speedKit sw is known
412
+ window?.speedKit?.sw &&
413
+ // can detect sw
414
+ navigator?.serviceWorker?.controller?.scriptURL &&
415
+ navigator.serviceWorker.controller.scriptURL.indexOf(new URL(window.speedKit.sw, location.origin).pathname) > -1
416
+ ) {
417
+ return true;
418
+ }
419
+ } catch (e) {}
420
+ return false;
421
+ }
422
+
423
+ function convertSoftToHardNavigation() {
424
+ function speedKitClickEventListener(event) {
425
+ var target = event.target.closest("a");
426
+
427
+ // Drop out if no link address is found or the target is the current page
428
+ if (!target || !target.href || target.href === location.href) {
429
+ return;
430
+ }
431
+
432
+ // Don't convert to hard navigation if it is just a product variant switch
433
+ // TODO: adjust conditions to customer-specific implementation as needed
434
+ var targetPathname = new URL(target.href).pathname;
435
+ var isPDP = targetPathname.indexOf("/p/") !== -1;
436
+ var isSamePathname = new URL(location.href).pathname === targetPathname;
437
+ var variantSwitchIdentifier = target.matches('.color-option, .size-option, .swatch-option');
438
+ if (
439
+ (isPDP && isSamePathname) || // url-based check
440
+ variantSwitchIdentifier // element-based check
441
+ ) {
442
+ return;
443
+ }
444
+
445
+ // Check if the target is enabled by SpeedKit and is a valid page for conversion
446
+ if (!softNavShouldBeConverted(target?.href)) {
447
+ return;
448
+ }
449
+
450
+ // Prevent soft navigation
451
+ event.preventDefault();
452
+
453
+ // Execute hard navigation instead
454
+ location.href = target.href;
455
+ }
456
+ window.addEventListener("click", speedKitClickEventListener, true);
457
+ }
458
+
459
+ {{/if}}
460
+
375
461
  // TODO: check if prerendering needs to be scoped to certain pages
376
462
  new PredictivePreloading({ prerender: true });
377
- {{/if}}
378
463
  }();
@@ -190,7 +190,7 @@ class DocumentHandlerServer {
190
190
  if (module === "baqend") {
191
191
  return { baqend: { message: { RevalidateAssets: () => true } } };
192
192
  }
193
- if (module.includes("fetch")) {
193
+ if (module.toLowerCase().includes("fetch")) {
194
194
  return this.createFetchMock();
195
195
  }
196
196
  const fileExtension = node_path_1.default.extname(module);
@@ -74,6 +74,8 @@ export type SpeedKitServerConfigVariation = {
74
74
  encodePrefixedUrl?: boolean;
75
75
  headers?: {
76
76
  cookie?: string;
77
+ accept?: string;
78
+ "user-agent"?: string;
77
79
  };
78
80
  queryParams?: [];
79
81
  regex?: null | string;
@@ -15,4 +15,6 @@ export declare class OnboardingServiceFactory {
15
15
  private getIntegrationFiles;
16
16
  private getUrlForTest;
17
17
  private ensureHttps;
18
+ private getCrawlerAgent;
19
+ private getCrawlerProxyConfig;
18
20
  }
@@ -43,6 +43,8 @@ const static_recipe_1 = require("../integration-api/virtual/static-recipe");
43
43
  const diff_against_current_page_1 = require("./dashboard/diff-against-current-page");
44
44
  const extension_downloader_1 = require("./browser/extension/extension-downloader");
45
45
  const executable_validator_1 = require("./browser/executable/executable-validator");
46
+ const fetch_socks_1 = require("fetch-socks");
47
+ const node_crypto_1 = tslib_1.__importDefault(require("node:crypto"));
46
48
  class OnboardingServiceFactory {
47
49
  context;
48
50
  cliConfig;
@@ -119,7 +121,8 @@ class OnboardingServiceFactory {
119
121
  async getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient) {
120
122
  const configApi = this.getConfigApi(customerConfig.app);
121
123
  const serverConfig = await this.getSpeedKitServerConfig(configApi, cli);
122
- const crawler = new crawler_1.Crawler(cli, serverConfig);
124
+ const agent = this.getCrawlerAgent(customerConfig, cli);
125
+ const crawler = new crawler_1.Crawler(cli, agent, serverConfig);
123
126
  const parameterQueryBuilder = new parameter_query_builder_1.ParameterQueryBuilder(customerConfig, files.getByName(files_1.INTEGRATION_FILES.CONFIG.SPEED_KIT));
124
127
  const DiffService = new diff_1.DiffServiceFactory(new diff_1.DiffContext(this.cliConfig.diffExec, this.cliConfig.diffExecTemplate)).getService();
125
128
  const requestDiffService = new request_diff_service_1.RequestDiffService(DiffService, documentHandler);
@@ -214,5 +217,43 @@ class OnboardingServiceFactory {
214
217
  ensureHttps(url = "") {
215
218
  return url.startsWith("https://") ? url : `https://${url}`;
216
219
  }
220
+ getCrawlerAgent(customerConfig, cli) {
221
+ const proxy = [];
222
+ const connect = {
223
+ keepAlive: true,
224
+ };
225
+ const agentOptions = {
226
+ rejectUnauthorized: false,
227
+ secureOptions: node_crypto_1.default.constants.SSL_OP_LEGACY_SERVER_CONNECT,
228
+ };
229
+ const crawlerProxyConfig = this.getCrawlerProxyConfig(customerConfig);
230
+ if (crawlerProxyConfig) {
231
+ cli.writeWarning(`use proxy from chromeFlags for local documenthandler`);
232
+ proxy.push(crawlerProxyConfig);
233
+ }
234
+ return (0, fetch_socks_1.socksDispatcher)(proxy, { ...agentOptions, connect });
235
+ }
236
+ getCrawlerProxyConfig(customerConfig) {
237
+ if (!customerConfig.chromeFlags ||
238
+ customerConfig.chromeFlags.length === 0) {
239
+ return;
240
+ }
241
+ //"--proxy-server=socks5://ap-northeast-1.proxy.baqend.com:1080"
242
+ const selector = /--proxy-server=socks(4|5):(\/\/[^:]*):(\d+)/is;
243
+ for (const flag of customerConfig.chromeFlags) {
244
+ if (!flag.includes("--proxy-server=socks")) {
245
+ continue;
246
+ }
247
+ const [, type, host, port] = flag.match(selector) || [];
248
+ if (!type || !host || !port) {
249
+ continue;
250
+ }
251
+ return {
252
+ type: Number(type),
253
+ host,
254
+ port: Number(port),
255
+ };
256
+ }
257
+ }
217
258
  }
218
259
  exports.OnboardingServiceFactory = OnboardingServiceFactory;
@@ -1,9 +1,11 @@
1
1
  import { SpeedKitServerConfig } from "../onboarding-model";
2
2
  import { CliService } from "../../cli";
3
+ import { Agent } from "undici";
3
4
  export declare class Crawler {
4
5
  private cli;
6
+ private agentConfig;
5
7
  private speedKitServerConfig?;
6
- constructor(cli: CliService, speedKitServerConfig?: SpeedKitServerConfig);
8
+ constructor(cli: CliService, agentConfig: Agent, speedKitServerConfig?: SpeedKitServerConfig);
7
9
  fetchRemote(originUrl: string, variation: string): Promise<Response>;
8
10
  private setDefaultUserAgent;
9
11
  private prepareOriginRequest;
@@ -1,58 +1,62 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Crawler = void 0;
4
- const tslib_1 = require("tslib");
5
4
  const onboarding_model_1 = require("../onboarding-model");
6
5
  const safe_1 = require("../../../helpers/safe");
7
- const node_https_1 = require("node:https");
8
- const node_crypto_1 = tslib_1.__importDefault(require("node:crypto"));
9
- const agent = new node_https_1.Agent({
10
- rejectUnauthorized: false,
11
- keepAlive: true,
12
- secureOptions: node_crypto_1.default.constants.SSL_OP_LEGACY_SERVER_CONNECT,
13
- });
6
+ const cli_1 = require("../../cli");
14
7
  class Crawler {
15
8
  cli;
9
+ agentConfig;
16
10
  speedKitServerConfig;
17
- constructor(cli, speedKitServerConfig) {
11
+ constructor(cli, agentConfig, speedKitServerConfig) {
18
12
  this.cli = cli;
13
+ this.agentConfig = agentConfig;
19
14
  this.speedKitServerConfig = speedKitServerConfig;
20
15
  }
21
16
  async fetchRemote(originUrl, variation) {
22
17
  const userAgent = this.setDefaultUserAgent(variation);
23
- const headers = { "User-Agent": userAgent };
18
+ const headers = {
19
+ "User-Agent": userAgent,
20
+ };
24
21
  const requestInit = {
25
22
  ...this.prepareOriginRequest(originUrl, variation, {
26
23
  headers,
27
24
  }),
28
- agent,
25
+ agent: this.agentConfig,
29
26
  };
27
+ this.cli.startAction(`[documentHandler:fetch]: ${originUrl}`);
30
28
  const fetchResponse = await (0, safe_1.safe)(fetch(originUrl, requestInit));
31
29
  if (fetchResponse.success === true) {
30
+ this.cli.endAction(cli_1.CliActionStatus.COMPLETED);
32
31
  return fetchResponse.data;
33
32
  }
33
+ this.cli.endAction(cli_1.CliActionStatus.FAILED);
34
34
  this.cli.writeError(`error while fetching Asset: ${originUrl}`);
35
35
  this.cli.writeError(fetchResponse.error);
36
36
  this.cli.writeError(JSON.stringify(fetchResponse.errorObj));
37
37
  }
38
38
  setDefaultUserAgent(variation) {
39
- if (variation.includes("mobile")) {
39
+ if (/^mobile$/is.test(variation)) {
40
40
  return onboarding_model_1.USER_AGENT.MOBILE;
41
41
  }
42
- if (variation.includes("tablet")) {
42
+ if (/^tablet$/is.test(variation)) {
43
43
  return onboarding_model_1.USER_AGENT.TABLET;
44
44
  }
45
- if (variation.includes("tv")) {
46
- return onboarding_model_1.USER_AGENT.TV;
47
- }
48
45
  return onboarding_model_1.USER_AGENT.DESKTOP;
49
46
  }
50
47
  prepareOriginRequest(originUrl, variationParameter, init) {
51
- if (!this.speedKitServerConfig || !variationParameter) {
48
+ if (!this.speedKitServerConfig) {
49
+ return init;
50
+ }
51
+ if (this.speedKitServerConfig.speedKit.variations?.["*"]) {
52
+ const config = this.speedKitServerConfig.speedKit.variations["*"];
53
+ init.headers = { ...init.headers, ...config.headers };
54
+ }
55
+ if (!variationParameter) {
52
56
  return init;
53
57
  }
54
58
  for (const variationKey in this.speedKitServerConfig.speedKit.variations) {
55
- if (variationParameter === variationKey.toUpperCase()) {
59
+ if (variationParameter === variationKey) {
56
60
  const variationConfig = this.speedKitServerConfig.speedKit.variations[variationKey];
57
61
  init.headers = { ...init.headers, ...variationConfig.headers };
58
62
  return init;
@@ -27,7 +27,7 @@ class VirtualOrestesApp {
27
27
  const UrlObject = new URL(event.request.url);
28
28
  const variation = UrlObject.searchParams.has("bqvariation")
29
29
  ? UrlObject.searchParams.get("bqvariation").trim()
30
- : "DEFAULT";
30
+ : "default";
31
31
  const cachedResponse = this.getCachedResponse(event.request.url);
32
32
  if (cachedResponse instanceof baqend_response_1.BaqendResponse) {
33
33
  return cachedResponse;
@@ -35,11 +35,11 @@ class VirtualOrestesApp {
35
35
  const response = await this.crawler.fetchRemote(originUrl, variation);
36
36
  // todo check if we can handle more then htmlResponses
37
37
  // only handle html responses
38
- if (!response.headers.get("Content-Type").includes("html")) {
38
+ if (!response?.headers?.get("Content-Type")?.includes("html")) {
39
39
  return;
40
40
  }
41
- const hasNoStoreFlag = response.headers
42
- .get("Cache-Control")
41
+ const hasNoStoreFlag = response?.headers
42
+ ?.get("Cache-Control")
43
43
  ?.includes("no-store");
44
44
  const customHeaders = this.prepareCustomHeaders(originUrl, hasNoStoreFlag);
45
45
  // CDN Redirect
@@ -50,7 +50,7 @@ class VirtualOrestesApp {
50
50
  return this.returnRedirect(customHeaders, response, event);
51
51
  }
52
52
  const { responseContent, responseEncoding } = await this.getResponseText(response);
53
- if (!this.isHtmlContentHeader(response.headers) ||
53
+ if (!this.isHtmlContentHeader(response?.headers) ||
54
54
  !this.isValidHtmlContent(responseContent)) {
55
55
  const customResponse = new baqend_response_1.BaqendResponse(responseContent, `text/html; charset=${responseEncoding}`, customHeaders);
56
56
  this.cache.addEntry(event.request.url, customResponse);
@@ -68,28 +68,28 @@ class VirtualOrestesApp {
68
68
  if (!Buffer.isBuffer(skResponse.body) && this.customerConfig.forceInstall) {
69
69
  skResponse.body = this.rewriteInstallResource(skResponse.body);
70
70
  }
71
- for (const key in skResponse.headers) {
71
+ for (const key in skResponse?.headers) {
72
72
  customHeaders.push({
73
73
  name: key,
74
- value: skResponse.headers[key],
74
+ value: skResponse?.headers[key],
75
75
  });
76
76
  }
77
77
  const originHeaders = [];
78
78
  // eslint-disable-next-line unicorn/no-array-for-each
79
- response.headers.forEach((value, key) => {
79
+ response?.headers.forEach((value, key) => {
80
80
  originHeaders.push({ name: `x-origin-${key}`, value: value });
81
81
  });
82
82
  customHeaders.push({
83
83
  name: "x-sk-detected-encoding",
84
84
  value: responseEncoding,
85
85
  });
86
- const baqendResponse = new baqend_response_1.BaqendResponse(skResponse.body, response.headers.get("Content-Type"), [...customHeaders, ...originHeaders]);
86
+ const baqendResponse = new baqend_response_1.BaqendResponse(skResponse.body, response?.headers.get("Content-Type"), [...customHeaders, ...originHeaders]);
87
87
  this.cache.addEntry(event.request.url, baqendResponse);
88
88
  return baqendResponse;
89
89
  }
90
90
  async getResponseText(response) {
91
91
  const buffer = Buffer.from(await response.arrayBuffer());
92
- const contentType = response.headers.get("content-type") || "";
92
+ const contentType = response?.headers.get("content-type") || "";
93
93
  const charsetRegex = /charset=([^\s"(),./:;<=>?@[\]]*)/i;
94
94
  const encodingFromHeader = contentType && charsetRegex.test(contentType)
95
95
  ? charsetRegex.exec(contentType)[1]
@@ -125,7 +125,7 @@ class VirtualOrestesApp {
125
125
  convertResponseHeadersToOrestesFormat(response) {
126
126
  const convertedResponseHeaders = {};
127
127
  // eslint-disable-next-line unicorn/no-array-for-each
128
- response.headers.forEach((value, key) => {
128
+ response?.headers?.forEach((value, key) => {
129
129
  convertedResponseHeaders[key] = value;
130
130
  });
131
131
  return convertedResponseHeaders;
@@ -133,7 +133,7 @@ class VirtualOrestesApp {
133
133
  returnRedirect(customHeaders, response, event) {
134
134
  customHeaders.push({
135
135
  name: "location",
136
- value: response.headers.get("location") || "",
136
+ value: response?.headers.get("location") || "",
137
137
  });
138
138
  const redirectResponse = new baqend_response_1.BaqendResponse("", null, customHeaders, response.status);
139
139
  this.cache.addEntry(event.request.url, redirectResponse);
@@ -738,5 +738,5 @@
738
738
  ]
739
739
  }
740
740
  },
741
- "version": "3.20.0"
741
+ "version": "3.22.0"
742
742
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "3.20.0",
4
+ "version": "3.22.0",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"
@@ -85,6 +85,7 @@
85
85
  "encoding-japanese": "^2.2.0",
86
86
  "esbuild": "^0.20.2",
87
87
  "extract-zip": "^2.0.1",
88
+ "fetch-socks": "^1.3.2",
88
89
  "fluent-ffmpeg": "^2.1.3",
89
90
  "fs-extra": "^11.2.0",
90
91
  "handlebars": "^4.7.8",