@speedkit/cli 4.0.0 → 4.2.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,18 @@
1
+ # [4.2.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.1.0...v4.2.0) (2026-05-08)
2
+
3
+
4
+ ### Features
5
+
6
+ * **onboarding:** local mode ([0a36f37](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/0a36f376d27f6bb369d38f073e5a5e5811cd1e11))
7
+ * **onboarding:** local mode ([e3e44ef](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/e3e44ef55bd7c6f71e776d978ce5cbd3fcf117e8))
8
+
9
+ # [4.1.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.0.0...v4.1.0) (2026-05-06)
10
+
11
+
12
+ ### Features
13
+
14
+ * **wizard:** shopify ([f7686ce](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/f7686ce4c24fd2f4d9fa3158f465ab2454dc4959))
15
+
1
16
  # [4.0.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v3.43.2...v4.0.0) (2026-05-06)
2
17
 
3
18
 
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.0.0 linux-x64 node-v22.22.2
24
+ @speedkit/cli/4.2.0 linux-x64 node-v22.22.2
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -0,0 +1 @@
1
+ export declare function javaRegexToJs(javaRegex: string): RegExp | undefined;
@@ -0,0 +1,21 @@
1
+ // Converts a Java-style regex (e.g. with leading `(?i)` inline flags) into a
2
+ // JS RegExp. Named groups `(?<name>...)` are already supported in JS.
3
+ export function javaRegexToJs(javaRegex) {
4
+ let pattern = javaRegex;
5
+ let flags = "";
6
+ const inlineFlags = pattern.match(/^\(\?([a-zA-Z]+)\)/);
7
+ if (inlineFlags) {
8
+ pattern = pattern.slice(inlineFlags[0].length);
9
+ for (const flag of inlineFlags[1]) {
10
+ if ("imsu".includes(flag) && !flags.includes(flag)) {
11
+ flags += flag;
12
+ }
13
+ }
14
+ }
15
+ try {
16
+ return new RegExp(pattern, flags);
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
@@ -109,13 +109,22 @@ const config = {
109
109
  wrapPreRenderedElementsWithCustomRootTags(document);
110
110
  addPolyFillAfterMainPreRenderTemplate(document);
111
111
  triggerShadowRendering(document);
112
+ {{#if isShopify}}
113
+ // Shopify specific: inject scripts into sk-html which set css variables (normally set to html element)
114
+ injectCSSVariableScripts(document);
115
+ {{/if}}
112
116
  {{#if addSSRInnerShadowDomSupport}}
113
117
 
114
118
  overrideCustomElementDefine(document);
115
119
  walkPreRenderedElements(document);
116
120
  {{/if}}
117
121
  {{else}}
118
- // do nothing
122
+ {{#if isShopify}}
123
+ // Shopify specific: inject script which sets css variables on the html element
124
+ injectCSSVariableScripts(document);
125
+ {{else}}
126
+ // do nothing
127
+ {{/if}}
119
128
  {{/if}}
120
129
  },
121
130
  {{#if isPlentymarkets}}
@@ -652,3 +661,81 @@ export const post = async (db, req, res) => {
652
661
  }
653
662
  {{/if}}
654
663
  {{/if}}
664
+ {{#if isShopify}}
665
+
666
+ /**
667
+ * Injects scripts into sk-html within each shadow root that set CSS custom
668
+ * properties (e.g. --window-height, --announcement-bar-height, --header-height)
669
+ * on sk-html instead of the document's html element.
670
+ *
671
+ * @param document
672
+ */
673
+ function injectCSSVariableScripts(document) {
674
+ {{#if addSSR}}
675
+ const customShadowRoots = getCustomShadowElements(document);
676
+ for (const customShadowRoot of customShadowRoots) {
677
+ const skHtml = customShadowRoot.querySelector("sk-html");
678
+ if (!skHtml) continue;
679
+
680
+ const script = document.createElement("script");
681
+ script.textContent = `(${setCSSVariablesOnSkHtml.toString()})();`;
682
+ // insert script before #main so styles are applied earlier
683
+ const main = skHtml.querySelector("#main");
684
+ if (main) {
685
+ main.before(script);
686
+ } else {
687
+ skHtml.appendChild(script);
688
+ }
689
+ }
690
+ {{else}}
691
+ const script = document.createElement("script");
692
+ script.textContent = `(${setCSSVariablesOnHtml.toString()})();`;
693
+ // insert script before #main so styles are applied earlier
694
+ const main = document.querySelector("#main");
695
+ if (!main) return;
696
+ main.before(script);
697
+ {{/if}}
698
+ }
699
+
700
+ /**
701
+ * Client-side script that sets CSS custom properties on {{#if addSSR}}sk-html{{else}}the html element{{/if}}
702
+ * (e.g. --window-height, --announcement-bar-height, --header-height).
703
+ * Injected via .toString() into the document.
704
+ *
705
+ * TODO: double check whether the set CSS variables are being used by the theme
706
+ * and that the queried selectors are correct for this Shopify store.
707
+ */
708
+ /* eslint-disable no-undef -- client-side function serialised via .toString() */
709
+ function {{#if addSSR}}setCSSVariablesOnSkHtml{{else}}setCSSVariablesOnHtml{{/if}}() {
710
+ {{#if addSSR}}
711
+ const host = document.querySelector("sk-shadow-app-root");
712
+ const root = host?.shadowRoot;
713
+ if (!root) return;
714
+ const skHtml = root.querySelector("sk-html");
715
+ if (!skHtml) return;
716
+ {{else}}
717
+ const html = document.querySelector("html");
718
+ if (!html) return;
719
+ {{/if}}
720
+
721
+ requestAnimationFrame(() => {
722
+ const viewportHeight = window.visualViewport ? window.visualViewport.height : document.documentElement.clientHeight;
723
+ {{#if addSSR}}skHtml{{else}}html{{/if}}.style.setProperty("--window-height", viewportHeight + "px");
724
+ });
725
+
726
+ const announcementBar = {{#if addSSR}}root{{else}}document{{/if}}.querySelector('[id^="shopify-section"][id$="announcement-bar"]');
727
+ if (announcementBar) {
728
+ {{#if addSSR}}skHtml{{else}}html{{/if}}.style.setProperty("--announcement-bar-height", announcementBar.clientHeight + "px");
729
+ }
730
+
731
+ const headerElement = {{#if addSSR}}root{{else}}document{{/if}}.querySelector('[id^="shopify-section"][id$="header"]');
732
+ if (headerElement) {
733
+ const headerHeight = headerElement.clientHeight;
734
+ const headerHeightWithoutBottomNav = headerElement.querySelector(".header__wrapper")?.clientHeight;
735
+ {{#if addSSR}}skHtml{{else}}html{{/if}}.style.setProperty("--header-height", headerHeight + "px");
736
+ if (headerHeightWithoutBottomNav != null) {
737
+ {{#if addSSR}}skHtml{{else}}html{{/if}}.style.setProperty("--header-height-without-bottom-nav", headerHeightWithoutBottomNav + "px");
738
+ }
739
+ }
740
+ }
741
+ {{/if}}
@@ -8,6 +8,43 @@ export declare class Crawler {
8
8
  constructor(cli: CliService, agentConfig: Agent, speedKitServerConfig?: SpeedKitServerConfig);
9
9
  fetchRemote(originUrl: string, variation: string): Promise<Response>;
10
10
  private setDefaultUserAgent;
11
+ /**
12
+ * Mirrors how the Baqend asset server applies the `variations` block from
13
+ * the Speed Kit server config to outgoing origin fetches. The cookie /
14
+ * user-agent / etc. headers configured per variation must reach the origin
15
+ * so that the response matches what the variation is supposed to represent
16
+ * (e.g. a logged-in user, a specific store, a mobile device).
17
+ *
18
+ * Resolution order (matches Orestes server behavior):
19
+ * 1. The wildcard `"*"` variation (if any) — its headers always apply.
20
+ * 2. First-match-wins iteration over the remaining variations: for each
21
+ * entry in declaration order, try an exact key match against
22
+ * `variationParameter` first, then fall back to its Java-style
23
+ * `regex` (e.g. `(?i)desktop-fmarkt-variant-(?<storeId>.*)`). The
24
+ * first variation that matches by either means is applied — exact
25
+ * matches do NOT take priority over regex matches in earlier
26
+ * entries. When matched via regex, named capture groups are
27
+ * substituted into header values via `${name}` placeholders, so a
28
+ * cookie like `fmarktcookie=${storeId}` becomes
29
+ * `fmarktcookie=2879130`.
30
+ * 3. Origin-level basic auth fallback (only if no variation matched).
31
+ */
11
32
  private prepareOriginRequest;
33
+ /**
34
+ * Replaces `${name}` placeholders in each header value with the matching
35
+ * named capture group from a regex match. Used by `prepareOriginRequest`
36
+ * to materialize parameterized variation headers — e.g. given:
37
+ *
38
+ * headers = { cookie: "fmarktcookie=${storeId}" }
39
+ * groups = { storeId: "2879130" }
40
+ *
41
+ * the result is `{ cookie: "fmarktcookie=2879130" }`.
42
+ *
43
+ * Placeholders that don't match a captured group are left intact rather
44
+ * than replaced with `undefined`, so a misconfigured variation still
45
+ * produces a recognisable header value instead of a silent corruption.
46
+ * Non-string header values are passed through unchanged.
47
+ */
48
+ private substituteNamedGroups;
12
49
  private prepareAuthRequest;
13
50
  }
@@ -1,5 +1,6 @@
1
1
  import { USER_AGENT } from "../onboarding-model.js";
2
2
  import { safe } from "../../../helpers/safe.js";
3
+ import { javaRegexToJs } from "../../../helpers/java-regex.js";
3
4
  import { randomUUID } from "node:crypto";
4
5
  export class Crawler {
5
6
  cli;
@@ -41,30 +42,110 @@ export class Crawler {
41
42
  }
42
43
  return USER_AGENT.DESKTOP;
43
44
  }
45
+ /**
46
+ * Mirrors how the Baqend asset server applies the `variations` block from
47
+ * the Speed Kit server config to outgoing origin fetches. The cookie /
48
+ * user-agent / etc. headers configured per variation must reach the origin
49
+ * so that the response matches what the variation is supposed to represent
50
+ * (e.g. a logged-in user, a specific store, a mobile device).
51
+ *
52
+ * Resolution order (matches Orestes server behavior):
53
+ * 1. The wildcard `"*"` variation (if any) — its headers always apply.
54
+ * 2. First-match-wins iteration over the remaining variations: for each
55
+ * entry in declaration order, try an exact key match against
56
+ * `variationParameter` first, then fall back to its Java-style
57
+ * `regex` (e.g. `(?i)desktop-fmarkt-variant-(?<storeId>.*)`). The
58
+ * first variation that matches by either means is applied — exact
59
+ * matches do NOT take priority over regex matches in earlier
60
+ * entries. When matched via regex, named capture groups are
61
+ * substituted into header values via `${name}` placeholders, so a
62
+ * cookie like `fmarktcookie=${storeId}` becomes
63
+ * `fmarktcookie=2879130`.
64
+ * 3. Origin-level basic auth fallback (only if no variation matched).
65
+ */
44
66
  prepareOriginRequest(originUrl, variationParameter, init) {
45
67
  if (!this.speedKitServerConfig) {
46
68
  return init;
47
69
  }
48
- if (this.speedKitServerConfig.speedKit.variations?.["*"]) {
49
- const config = this.speedKitServerConfig.speedKit.variations["*"];
70
+ const variations = this.speedKitServerConfig.speedKit.variations;
71
+ // Wildcard variation headers apply to every request, regardless of which
72
+ // specific variation (if any) is requested.
73
+ if (variations?.["*"]) {
74
+ const config = variations["*"];
50
75
  init.headers = { ...init.headers, ...config.headers };
51
76
  }
52
77
  if (!variationParameter) {
53
78
  return init;
54
79
  }
55
- for (const variationKey in this.speedKitServerConfig.speedKit.variations) {
80
+ // First-match-wins over the variations in declaration order. Each entry
81
+ // 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.
83
+ for (const variationKey in variations) {
84
+ if (variationKey === "*") {
85
+ continue;
86
+ }
87
+ const variationConfig = variations[variationKey];
56
88
  if (variationParameter === variationKey) {
57
- const variationConfig = this.speedKitServerConfig.speedKit.variations[variationKey];
58
89
  init.headers = { ...init.headers, ...variationConfig.headers };
59
90
  return init;
60
91
  }
92
+ if (!variationConfig.regex) {
93
+ continue;
94
+ }
95
+ const regex = javaRegexToJs(variationConfig.regex);
96
+ if (!regex) {
97
+ // Unsupported Java regex construct — skip rather than crash so later
98
+ // variations still get a chance to match.
99
+ continue;
100
+ }
101
+ const match = variationParameter.match(regex);
102
+ if (!match) {
103
+ continue;
104
+ }
105
+ init.headers = {
106
+ ...init.headers,
107
+ ...this.substituteNamedGroups(variationConfig.headers, match.groups),
108
+ };
109
+ return init;
61
110
  }
111
+ // No variation matched: fall back to origin basic auth if configured.
62
112
  const authentication = this.prepareAuthRequest(originUrl);
63
113
  if (authentication) {
64
114
  init.headers["Authorization"] = authentication;
65
115
  }
66
116
  return init;
67
117
  }
118
+ /**
119
+ * Replaces `${name}` placeholders in each header value with the matching
120
+ * named capture group from a regex match. Used by `prepareOriginRequest`
121
+ * to materialize parameterized variation headers — e.g. given:
122
+ *
123
+ * headers = { cookie: "fmarktcookie=${storeId}" }
124
+ * groups = { storeId: "2879130" }
125
+ *
126
+ * the result is `{ cookie: "fmarktcookie=2879130" }`.
127
+ *
128
+ * Placeholders that don't match a captured group are left intact rather
129
+ * than replaced with `undefined`, so a misconfigured variation still
130
+ * produces a recognisable header value instead of a silent corruption.
131
+ * Non-string header values are passed through unchanged.
132
+ */
133
+ substituteNamedGroups(headers, groups) {
134
+ if (!headers) {
135
+ return {};
136
+ }
137
+ if (!groups) {
138
+ return { ...headers };
139
+ }
140
+ const result = {};
141
+ for (const [key, value] of Object.entries(headers)) {
142
+ result[key] =
143
+ typeof value === "string"
144
+ ? value.replace(/\$\{(\w+)}/g, (match, name) => groups[name] ?? match)
145
+ : value;
146
+ }
147
+ return result;
148
+ }
68
149
  prepareAuthRequest(originUrl) {
69
150
  if (!this.speedKitServerConfig ||
70
151
  this.speedKitServerConfig.speedKit?.authentications?.length < 1) {
@@ -751,5 +751,5 @@
751
751
  ]
752
752
  }
753
753
  },
754
- "version": "4.0.0"
754
+ "version": "4.2.0"
755
755
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "4.0.0",
4
+ "version": "4.2.0",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"
@@ -85,7 +85,7 @@
85
85
  "@aws-sdk/client-athena": "^3.529",
86
86
  "@eslint/eslintrc": "^3.3.5",
87
87
  "@eslint/js": "^10.0.1",
88
- "@inquirer/prompts": "^8.4.1",
88
+ "@inquirer/prompts": "^8.4.2",
89
89
  "@oclif/core": "^4.10.5",
90
90
  "@oclif/errors": "^1.3.6",
91
91
  "@oclif/plugin-autocomplete": "^3.2.45",
@@ -131,7 +131,7 @@
131
131
  "@types/uuid": "^9.0.8",
132
132
  "chai": "^4.4.1",
133
133
  "copyfiles": "^2.4.1",
134
- "eslint": "10.2.0",
134
+ "eslint": "10.2.1",
135
135
  "eslint-config-prettier": "^10.1.8",
136
136
  "eslint-plugin-prettier": "^5.5.5",
137
137
  "eslint-plugin-unicorn": "^64.0.0",
@@ -139,9 +139,9 @@
139
139
  "husky": "^8.0.3",
140
140
  "lint-staged": "^16.4.0",
141
141
  "memfs": "^4.8.0",
142
- "mocha": "^12.0.0-beta-3",
142
+ "mocha": "^12.0.0-beta-9.2",
143
143
  "mock-fs": "^5.5.0",
144
- "nock": "^14.0.12",
144
+ "nock": "^14.0.13",
145
145
  "nyc": "^18.0.0",
146
146
  "oclif": "^4.23.0",
147
147
  "shx": "^0.4.0",