@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 +15 -0
- package/README.md +1 -1
- package/dist/helpers/java-regex.d.ts +1 -0
- package/dist/helpers/java-regex.js +21 -0
- package/dist/services/customer-config/templates/config_documentHandler.js.hbs +88 -1
- package/dist/services/onboarding/virtual-orestes-app/crawler.d.ts +37 -0
- package/dist/services/onboarding/virtual-orestes-app/crawler.js +85 -4
- package/oclif.manifest.json +1 -1
- package/package.json +5 -5
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
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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) {
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speedkit/cli",
|
|
3
3
|
"description": "Speed Kit CLI",
|
|
4
|
-
"version": "4.
|
|
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.
|
|
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.
|
|
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-
|
|
142
|
+
"mocha": "^12.0.0-beta-9.2",
|
|
143
143
|
"mock-fs": "^5.5.0",
|
|
144
|
-
"nock": "^14.0.
|
|
144
|
+
"nock": "^14.0.13",
|
|
145
145
|
"nyc": "^18.0.0",
|
|
146
146
|
"oclif": "^4.23.0",
|
|
147
147
|
"shx": "^0.4.0",
|