@speedkit/cli 4.20.0 → 4.20.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [4.20.2](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.20.1...v4.20.2) (2026-08-07)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **onboarding:** partial variations local mode ([bf809b1](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/bf809b10b3f6ef8918675971ee950735c3250213))
7
+
8
+ ## [4.20.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.20.0...v4.20.1) (2026-08-07)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **customer-config:** marker detectDevice reads the passed doc, not document ([6f1b697](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/6f1b697b52cc6786c7e975edfcbc85c52750749b))
14
+
1
15
  # [4.20.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.19.0...v4.20.0) (2026-08-07)
2
16
 
3
17
 
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/4.20.0 linux-x64 node-v22.23.2
24
+ @speedkit/cli/4.20.2 linux-x64 node-v22.23.2
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -34,14 +34,23 @@
34
34
 
35
35
  {{#if isShopify}}
36
36
  import { ShopifyPlugin } from "speed-kit-config/ShopifyPlugin";
37
+ {{/if}}
38
+ {{#if addSSR}}
39
+ import { SSRPlugin } from "speed-kit-config/SSRPlugin";
40
+ {{/if}}
37
41
 
38
42
  (function() {
43
+ {{#if isShopify}}
39
44
  SpeedKit.configPlugins = SpeedKit.configPlugins || [];
40
45
  SpeedKit.configPlugins.push(new ShopifyPlugin());
41
-
42
- {{else}}
43
- (function() {
44
46
  {{/if}}
47
+ {{#if addSSR}}
48
+ {{#unless isShopify}}
49
+ SpeedKit.configPlugins = SpeedKit.configPlugins || [];
50
+ {{/unless}}
51
+ SpeedKit.configPlugins.push(new SSRPlugin());
52
+ {{/if}}
53
+
45
54
  var ENABLED_HOSTS_CONFIGS = [
46
55
  // Production:
47
56
  {
@@ -98,15 +107,20 @@ import { ShopifyPlugin } from "speed-kit-config/ShopifyPlugin";
98
107
  rumTracking: true,
99
108
  {{#if addDeviceDetection}}
100
109
  {{#unless addSSR}}
110
+ // Server-side responsive: origin returns different HTML per device (UA-keyed).
101
111
  userAgentDetection: true,
102
112
  detectDevice: function(doc) {
103
- // TODO: adjust condition to your needs
113
+ // TODO: set a marker present only in the mobile variation. Read it from `doc`.
104
114
  return doc.querySelector('<mobile-specific-element>') ? "mobile" : "desktop";
105
115
  },
106
116
  {{/unless}}
107
117
  {{#if addSSR}}
108
- customDevice: () => {
109
- return customDevice();
118
+ // SSR: pick the device by viewport width; SSRPlugin (above) handles resize/zoom/bfcache.
119
+ detectDevice: function(doc) {
120
+ // TODO: set the real breakpoint — discover it, don't guess (tools/sk-breakpoints.mjs).
121
+ var width = document.documentElement.clientWidth;
122
+ if (!width) return null;
123
+ return width <= 820 ? "mobile" : "desktop";
110
124
  },
111
125
  {{/if}}
112
126
  {{/if}}
@@ -458,29 +472,6 @@ import { ShopifyPlugin } from "speed-kit-config/ShopifyPlugin";
458
472
  // Utility Functions
459
473
  //================================================================================
460
474
 
461
- {{#if addSSR}}
462
- function customDevice() {
463
- var width = window.innerWidth || 1025;
464
- // TODO: adjust devices + breakpoints to your needs
465
- if (width <= 820) {
466
- return "mobile";
467
- }
468
- return "desktop";
469
- }
470
-
471
- // Re-evaluate device on orientation change and push to service worker
472
- window.addEventListener("orientationchange", function () {
473
- if (typeof SpeedKit !== "undefined" && SpeedKit.updateDevice) {
474
- SpeedKit.updateDevice(customDevice());
475
- }
476
- });
477
- window.addEventListener("resize", function () {
478
- if (typeof SpeedKit !== "undefined" && SpeedKit.updateDevice) {
479
- SpeedKit.updateDevice(customDevice());
480
- }
481
- });
482
- {{/if}}
483
-
484
475
  function getEnabledHosts(enabledHostsConfigs) {
485
476
  var allHosts = [];
486
477
  enabledHostsConfigs.forEach(function (enabledHostsConfig) {
@@ -7,12 +7,19 @@
7
7
  {{#if addDeviceDetection}}
8
8
  {{#unless addSSR}}
9
9
  detectDevice: (doc) => {
10
- // keep only if you need to check the DOM for screen width checks the SK config's detectDevice is sufficient
11
-
12
- // TODO: adjust condition to your needs
10
+ // Only if you need a DOM marker; for width checks the SK config's detectDevice suffices.
11
+ // TODO: set a marker present only in the mobile variation. Read it from `doc`.
13
12
  return doc.querySelector('<mobile-specific-element>') ? "mobile" : "desktop";
14
13
  },
15
14
  {{/unless}}
15
+ {{#if addSSR}}
16
+ // Mirror config_SpeedKit.js detectDevice — keep the breakpoint identical.
17
+ detectDevice: (doc) => {
18
+ let width = document.documentElement.clientWidth;
19
+ if (!width) return null;
20
+ return width <= 820 ? "mobile" : "desktop";
21
+ },
22
+ {{/if}}
16
23
  {{/if}}
17
24
  blocks: [
18
25
  // TODO: adjust/replace this exemplary list
@@ -13,8 +13,8 @@ export class DiffAgainstCurrentPage {
13
13
  async postDiff(currentHtml, currentUrl, variant = "default", transform = false) {
14
14
  this.cli.writeWarning(`received diff for url: ${currentUrl}`);
15
15
  this.cli.startAction(`DIFF:FETCH:FILE:${currentUrl}`, `fetch remote`);
16
- const originResponse = await this.crawler.fetchRemote(currentUrl, variant);
17
- let originHtml = await originResponse?.text();
16
+ const fetchResult = await this.crawler.fetchRemote(currentUrl, variant);
17
+ let originHtml = await fetchResult?.response?.text();
18
18
  if (!originHtml) {
19
19
  this.cli.failAction(`DIFF:FETCH:FILE:${currentUrl}`);
20
20
  throw new Error("Could not fetch remote page");
@@ -83,7 +83,7 @@ export type SpeedKitServerConfigVariation = Record<string, {
83
83
  accept?: string;
84
84
  "user-agent"?: string;
85
85
  };
86
- queryParams?: [];
86
+ queryParams?: string[];
87
87
  regex?: null | string;
88
88
  urlPrefix?: string;
89
89
  }>;
@@ -6,7 +6,10 @@ export declare class Crawler {
6
6
  private agentConfig;
7
7
  private speedKitServerConfig?;
8
8
  constructor(cli: CliService, agentConfig: Agent, speedKitServerConfig?: SpeedKitServerConfig);
9
- fetchRemote(originUrl: string, variation: string): Promise<Response>;
9
+ fetchRemote(originUrl: string, variation: string): Promise<{
10
+ response: Response;
11
+ url: string;
12
+ } | undefined>;
10
13
  private setDefaultUserAgent;
11
14
  /**
12
15
  * Mirrors how the Baqend asset server applies the `variations` block from
@@ -28,8 +31,17 @@ export declare class Crawler {
28
31
  * cookie like `fmarktcookie=${storeId}` becomes
29
32
  * `fmarktcookie=2879130`.
30
33
  * 3. Origin-level basic auth fallback (only if no variation matched).
34
+ *
35
+ * Returns the (query-param-augmented) origin URL alongside the request init,
36
+ * so query-param variations like `Sortierung=${sort}` reach the origin.
31
37
  */
32
38
  private prepareOriginRequest;
39
+ /**
40
+ * Applies a matched variation: merges its headers into `init` and appends its
41
+ * `queryParams` onto `url`, substituting regex named groups (`${name}`) — so
42
+ * `Sortierung=${sort}` with `{ sort: "3" }` becomes `Sortierung=3`.
43
+ */
44
+ private applyVariation;
33
45
  /**
34
46
  * Replaces `${name}` placeholders in each header value with the matching
35
47
  * named capture group from a regex match. Used by `prepareOriginRequest`
@@ -16,18 +16,21 @@ export class Crawler {
16
16
  const headers = {
17
17
  "User-Agent": userAgent,
18
18
  };
19
+ // origin URL with the variation's query params applied — returned so callers compare it
20
+ // (not the bare originUrl) against response.url for redirect detection
21
+ const { url, init } = this.prepareOriginRequest(originUrl, variation, {
22
+ headers,
23
+ });
19
24
  const requestInit = {
20
- ...this.prepareOriginRequest(originUrl, variation, {
21
- headers,
22
- }),
25
+ ...init,
23
26
  agent: this.agentConfig,
24
27
  };
25
28
  const uuid = randomUUID();
26
- this.cli.startAction(`DOCUMENTHANDLER:FETCH:${uuid}`, `[documentHandler:fetch]: ${originUrl}`);
27
- const fetchResponse = await safe(fetch(originUrl, requestInit));
29
+ this.cli.startAction(`DOCUMENTHANDLER:FETCH:${uuid}`, `[documentHandler:fetch]: ${url}`);
30
+ const fetchResponse = await safe(fetch(url, requestInit));
28
31
  if (fetchResponse.success === true) {
29
32
  this.cli.successAction(`DOCUMENTHANDLER:FETCH:${uuid}`);
30
- return fetchResponse.data;
33
+ return { response: fetchResponse.data, url };
31
34
  }
32
35
  this.cli.failAction(`DOCUMENTHANDLER:FETCH:${uuid}`, `error while fetching Asset: ${originUrl}`);
33
36
  this.cli.writeError(fetchResponse.error);
@@ -62,32 +65,35 @@ export class Crawler {
62
65
  * cookie like `fmarktcookie=${storeId}` becomes
63
66
  * `fmarktcookie=2879130`.
64
67
  * 3. Origin-level basic auth fallback (only if no variation matched).
68
+ *
69
+ * Returns the (query-param-augmented) origin URL alongside the request init,
70
+ * so query-param variations like `Sortierung=${sort}` reach the origin.
65
71
  */
66
72
  prepareOriginRequest(originUrl, variationParameter, init) {
73
+ const url = new URL(originUrl);
67
74
  if (!this.speedKitServerConfig) {
68
- return init;
75
+ return { url: url.toString(), init };
69
76
  }
70
77
  const variations = this.speedKitServerConfig.speedKit.variations;
71
- // Wildcard variation headers apply to every request, regardless of which
78
+ // Wildcard variation applies to every request, regardless of which
72
79
  // specific variation (if any) is requested.
73
80
  if (variations?.["*"]) {
74
- const config = variations["*"];
75
- init.headers = { ...init.headers, ...config.headers };
81
+ this.applyVariation(url, init, variations["*"], undefined);
76
82
  }
77
83
  if (!variationParameter) {
78
- return init;
84
+ return { url: url.toString(), init };
79
85
  }
80
86
  // First-match-wins over the variations in declaration order. Each entry
81
87
  // can match by exact key or by its Java-style `regex` field; whichever
82
- // entry matches first applies its headers and short-circuits the rest.
88
+ // entry matches first applies its headers + query params and short-circuits.
83
89
  for (const variationKey in variations) {
84
90
  if (variationKey === "*") {
85
91
  continue;
86
92
  }
87
93
  const variationConfig = variations[variationKey];
88
94
  if (variationParameter === variationKey) {
89
- init.headers = { ...init.headers, ...variationConfig.headers };
90
- return init;
95
+ this.applyVariation(url, init, variationConfig, undefined);
96
+ return { url: url.toString(), init };
91
97
  }
92
98
  if (!variationConfig.regex) {
93
99
  continue;
@@ -102,18 +108,35 @@ export class Crawler {
102
108
  if (!match) {
103
109
  continue;
104
110
  }
105
- init.headers = {
106
- ...init.headers,
107
- ...this.substituteNamedGroups(variationConfig.headers, match.groups),
108
- };
109
- return init;
111
+ this.applyVariation(url, init, variationConfig, match.groups);
112
+ return { url: url.toString(), init };
110
113
  }
111
114
  // No variation matched: fall back to origin basic auth if configured.
112
115
  const authentication = this.prepareAuthRequest(originUrl);
113
116
  if (authentication) {
114
117
  init.headers["Authorization"] = authentication;
115
118
  }
116
- return init;
119
+ return { url: url.toString(), init };
120
+ }
121
+ /**
122
+ * Applies a matched variation: merges its headers into `init` and appends its
123
+ * `queryParams` onto `url`, substituting regex named groups (`${name}`) — so
124
+ * `Sortierung=${sort}` with `{ sort: "3" }` becomes `Sortierung=3`.
125
+ */
126
+ applyVariation(url, init, variationConfig, groups) {
127
+ init.headers = {
128
+ ...init.headers,
129
+ ...this.substituteNamedGroups(variationConfig.headers, groups),
130
+ };
131
+ for (const queryParam of variationConfig.queryParams ?? []) {
132
+ const substituted = groups
133
+ ? queryParam.replace(/\$\{(\w+)}/g, (match, name) => groups[name] ?? match)
134
+ : queryParam;
135
+ const eq = substituted.indexOf("=");
136
+ const key = eq === -1 ? substituted : substituted.slice(0, eq);
137
+ const value = eq === -1 ? "" : substituted.slice(eq + 1);
138
+ url.searchParams.set(key, value);
139
+ }
117
140
  }
118
141
  /**
119
142
  * Replaces `${name}` placeholders in each header value with the matching
@@ -113,7 +113,10 @@ export class VirtualOrestesApp {
113
113
  const variation = UrlObject.searchParams.has("bqvariation")
114
114
  ? UrlObject.searchParams.get("bqvariation").trim()
115
115
  : "default";
116
- const response = await this.crawler.fetchRemote(originUrl, variation);
116
+ const fetchResult = await this.crawler.fetchRemote(originUrl, variation);
117
+ const response = fetchResult?.response;
118
+ // URL we actually fetched (origin + variation query params); used for the redirect check below
119
+ const fetchedUrl = fetchResult?.url;
117
120
  // todo check if we can handle more then htmlResponses
118
121
  // only handle html responses
119
122
  if (!response?.headers?.get("content-type")?.includes("html")) {
@@ -123,8 +126,8 @@ export class VirtualOrestesApp {
123
126
  ?.get("Cache-Control")
124
127
  ?.includes("no-store");
125
128
  const customHeaders = this.prepareCustomHeaders(originUrl, hasNoStoreFlag);
126
- // CDN Redirect
127
- if (originUrl !== response.url) {
129
+ // CDN Redirect — compare the fetched URL, not bare originUrl (variation params aren't a redirect)
130
+ if (fetchedUrl !== response.url) {
128
131
  return this.handleCdnRedirect(customHeaders, response, requestUrl);
129
132
  }
130
133
  if (response.status > 300 && response.status < 400) {
@@ -1001,5 +1001,5 @@
1001
1001
  ]
1002
1002
  }
1003
1003
  },
1004
- "version": "4.20.0"
1004
+ "version": "4.20.2"
1005
1005
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "4.20.0",
4
+ "version": "4.20.2",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"