@speedkit/cli 4.5.0 → 4.7.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 +24 -0
- package/README.md +5 -2
- package/dist/commands/onboarding.js +2 -2
- package/dist/helpers/site-analyzer.d.ts +1 -0
- package/dist/helpers/site-analyzer.js +2 -0
- package/dist/services/customer-config/customer-config-service-model.d.ts +1 -0
- package/dist/services/customer-config/customer-config-service.js +9 -0
- package/dist/services/customer-config/templates/config_documentHandler.js.hbs +3 -0
- package/dist/services/customer-config/templates/config_dynamicStyles.css.hbs +7 -5
- package/dist/services/onboarding/browser/executable/executable-validator.js +4 -3
- package/dist/services/onboarding/browser/extension/devtools-extension-api.d.ts +21 -0
- package/dist/services/onboarding/browser/extension/devtools-extension-api.js +38 -0
- package/dist/services/onboarding/browser/extension/extension-bridge.d.ts +51 -0
- package/dist/services/onboarding/browser/extension/extension-bridge.js +165 -0
- package/dist/services/onboarding/onboarding-model.d.ts +43 -5
- package/dist/services/onboarding/onboarding-model.js +11 -3
- package/dist/services/onboarding/onboarding-model.spec.d.ts +1 -0
- package/dist/services/onboarding/onboarding-model.spec.js +28 -0
- package/dist/services/onboarding/onboarding-service-factory.js +16 -6
- package/dist/services/onboarding/onboarding-service.d.ts +11 -1
- package/dist/services/onboarding/onboarding-service.js +19 -10
- package/dist/services/onboarding/virtual-orestes-app/index.d.ts +8 -1
- package/dist/services/onboarding/virtual-orestes-app/index.js +37 -1
- package/oclif.manifest.json +9 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
# [4.7.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.6.0...v4.7.0) (2026-06-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **customer-config:** improve add2cart disabling ([7d20650](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/7d206503e27919375f78b93876dfa48686aaf5b5))
|
|
7
|
+
|
|
8
|
+
# [4.6.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.5.0...v4.6.0) (2026-05-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **onboarding:** include product name in Windows browser version ([a058447](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/a058447e3a0e3f26a57bc9bf4581943132f2183b))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **customer-config:** add usercentrics handling ([c8b1da7](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/c8b1da76a1e3bd629e029aa79fa8e5252c9faf10))
|
|
19
|
+
* **extension-bridge:** write CLI state to devtools via chrome.storage.local ([0942178](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/09421783de1116c5cf6c346230ed1b4625d8a584))
|
|
20
|
+
* **local-cache-errors:** emit transform failures to devtools panel ([5989846](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/598984629c7dc4de2cc9c064457a410f0e433e54))
|
|
21
|
+
* **onboarding:** configurable mcp port ([ec562f4](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/ec562f42f53ad0144c25373eaca1998a411b3b33))
|
|
22
|
+
* **onboarding:** configurable mcp port ([6649b79](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/6649b798ff935e7714c28d3c046a25d950d18f82))
|
|
23
|
+
* **project:** claude ([0332a46](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/0332a46778e5a4711d7919e53f53b74064679c00))
|
|
24
|
+
|
|
1
25
|
# [4.5.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.4.2...v4.5.0) (2026-05-15)
|
|
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/4.
|
|
24
|
+
@speedkit/cli/4.7.0 linux-x64 node-v22.22.3
|
|
25
25
|
$ sk --help [COMMAND]
|
|
26
26
|
USAGE
|
|
27
27
|
$ sk COMMAND
|
|
@@ -383,7 +383,8 @@ Start a local dev environment for building and testing the Speed Kit configurati
|
|
|
383
383
|
|
|
384
384
|
```
|
|
385
385
|
USAGE
|
|
386
|
-
$ sk onboarding CUSTOMERPATH [-c <value>] [-t] [-d <value>] [-l] [-p] [-v] [-s]
|
|
386
|
+
$ sk onboarding CUSTOMERPATH [-c <value>] [-t] [-d <value>] [-l] [-p] [--debugPort <value>] [-v] [-s]
|
|
387
|
+
[--headless]
|
|
387
388
|
|
|
388
389
|
ARGUMENTS
|
|
389
390
|
CUSTOMERPATH The customer config path
|
|
@@ -396,6 +397,8 @@ FLAGS
|
|
|
396
397
|
-s, --ignoreContentSecurityPolicy Removes header/meta "content-security-policy" for origin-responses
|
|
397
398
|
-t, --useTestConfig Override app with configured testApp
|
|
398
399
|
-v, --verboseLevel Show all messages
|
|
400
|
+
--debugPort=<value> [default: 9222] Chrome remote debugging port (only used with -p/--headless). Use
|
|
401
|
+
distinct ports to run multiple onboarding processes at once.
|
|
399
402
|
--headless Run Chrome in headless mode (for CI environments). Implicitly enables remote
|
|
400
403
|
debugging port.
|
|
401
404
|
|
|
@@ -11,8 +11,8 @@ export default class Onboarding extends Command {
|
|
|
11
11
|
`$ sk onboarding ${CLIParametersExample.CustomerPath} -${CLIParametersChar.ConfigName} ${CLIParametersExample.ConfigName} -${CLIParametersChar.Domain} ${CLIParametersExample.Domain}`,
|
|
12
12
|
];
|
|
13
13
|
async run() {
|
|
14
|
-
const { args: { customerPath }, flags: { configName, domain, local, verboseLevel, ignoreContentSecurityPolicy, useTestConfig, debuggingPort, headless, }, } = await this.parse(Onboarding);
|
|
15
|
-
const service = await new OnboardingServiceFactory(new OnboardingContext(customerPath, configName, domain, local, ignoreContentSecurityPolicy, this.config.root, verboseLevel, useTestConfig, debuggingPort, headless), new CliConfig(this.config).load(customerPath)).getService();
|
|
14
|
+
const { args: { customerPath }, flags: { configName, domain, local, verboseLevel, ignoreContentSecurityPolicy, useTestConfig, debuggingPort, headless, debugPort, }, } = await this.parse(Onboarding);
|
|
15
|
+
const service = await new OnboardingServiceFactory(new OnboardingContext(customerPath, configName, domain, local, ignoreContentSecurityPolicy, this.config.root, verboseLevel, useTestConfig, debuggingPort, headless, debugPort), new CliConfig(this.config).load(customerPath)).getService();
|
|
16
16
|
const shutdown = new AbortController();
|
|
17
17
|
const onSignal = () => shutdown.abort();
|
|
18
18
|
// `once`: a second Ctrl+C falls through to Node's default handler and
|
|
@@ -7,6 +7,7 @@ const EMPTY_FLAGS = {
|
|
|
7
7
|
includeServiceWorker: false,
|
|
8
8
|
removeLazyLoading: false,
|
|
9
9
|
hasSoftNavigations: false,
|
|
10
|
+
handleUsercentrics: false,
|
|
10
11
|
};
|
|
11
12
|
function detectFeatures(html, headers) {
|
|
12
13
|
return {
|
|
@@ -16,6 +17,7 @@ function detectFeatures(html, headers) {
|
|
|
16
17
|
includeServiceWorker: /navigator\.serviceWorker\.register|serviceWorker\.register/.test(html),
|
|
17
18
|
removeLazyLoading: /loading=["']lazy["']|data-src=|class=["'][^"']*lazyload|lazysizes/.test(html),
|
|
18
19
|
hasSoftNavigations: /__NEXT_DATA__|__NUXT__|vue-router|react-router|history\.pushState/.test(html),
|
|
20
|
+
handleUsercentrics: /uc-block(\.[\w-]+)*\.js|usercentrics\.eu|Usercentrics|data-usercentrics|__ucCmp|window\.UC_UI/.test(html),
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
function mergeFeatures(results) {
|
|
@@ -48,6 +48,7 @@ export interface CustomerConfigSettings {
|
|
|
48
48
|
activateCfRocketLoaderPlugin?: boolean;
|
|
49
49
|
hasSoftNavigations?: boolean;
|
|
50
50
|
convertSoftToHardNavigations?: boolean;
|
|
51
|
+
handleUsercentrics?: boolean;
|
|
51
52
|
isPOV?: boolean;
|
|
52
53
|
useDisabledSitesApproach?: boolean;
|
|
53
54
|
shopSystemName?: string;
|
|
@@ -251,6 +251,7 @@ export class CustomerConfigService {
|
|
|
251
251
|
const gaTracking = resolve("useGATracking", true);
|
|
252
252
|
const googleOptimize = resolve("withGoogleOptimize", false);
|
|
253
253
|
const cfRocketLoader = resolve("activateCfRocketLoaderPlugin", false);
|
|
254
|
+
const usercentrics = resolve("handleUsercentrics", false);
|
|
254
255
|
// Detectable features — sorted detected-first when analysis is available
|
|
255
256
|
const detectableChoices = [
|
|
256
257
|
{
|
|
@@ -293,6 +294,12 @@ export class CustomerConfigService {
|
|
|
293
294
|
checked: cfRocketLoader.checked,
|
|
294
295
|
description: "Handle Cloudflare Rocket Loader script deferral",
|
|
295
296
|
},
|
|
297
|
+
{
|
|
298
|
+
name: `Usercentrics Handling${usercentrics.label}`,
|
|
299
|
+
value: "handleUsercentrics",
|
|
300
|
+
checked: usercentrics.checked,
|
|
301
|
+
description: "Set handleUsercentrics: true in DH Config — declares uc-block.bundle.js (Usercentrics CMP) as safe script",
|
|
302
|
+
},
|
|
296
303
|
];
|
|
297
304
|
if (siteAnalysis) {
|
|
298
305
|
const detectionRank = (name) => {
|
|
@@ -352,6 +359,7 @@ export class CustomerConfigService {
|
|
|
352
359
|
const isPOV = has("isPOV");
|
|
353
360
|
const withGoogleOptimize = has("withGoogleOptimize");
|
|
354
361
|
const activateCfRocketLoaderPlugin = has("activateCfRocketLoaderPlugin");
|
|
362
|
+
const handleUsercentrics = has("handleUsercentrics");
|
|
355
363
|
const useScrapingBee = has("useScrapingBee");
|
|
356
364
|
const addDowntimeDetection = has("addDowntimeDetection");
|
|
357
365
|
// Follow-up questions for features that need additional input
|
|
@@ -385,6 +393,7 @@ export class CustomerConfigService {
|
|
|
385
393
|
useScrapingBee,
|
|
386
394
|
withGoogleOptimize,
|
|
387
395
|
activateCfRocketLoaderPlugin,
|
|
396
|
+
handleUsercentrics,
|
|
388
397
|
hasSoftNavigations,
|
|
389
398
|
convertSoftToHardNavigations,
|
|
390
399
|
useDisabledSitesApproach,
|
|
@@ -106,6 +106,9 @@ const config = {
|
|
|
106
106
|
{{#if activateCfRocketLoaderPlugin}}
|
|
107
107
|
handleRocketLoader: true,
|
|
108
108
|
{{/if}}
|
|
109
|
+
{{#if handleUsercentrics}}
|
|
110
|
+
handleUsercentrics: true,
|
|
111
|
+
{{/if}}
|
|
109
112
|
{{#if isShopify}}
|
|
110
113
|
delayScriptPath: "/apps/speed-kit/speed-kit-dom-ready.js", // Shopify specific
|
|
111
114
|
{{else}}
|
|
@@ -99,11 +99,6 @@ html > body {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/* Prevent Add2Cart clicks: */
|
|
103
|
-
.dummy-add2cart-class {
|
|
104
|
-
pointer-events: none !important;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
102
|
/* Skeleton loading style: */
|
|
108
103
|
.dummy-add2cart-class,
|
|
109
104
|
.dummy-skeleton-container-class {
|
|
@@ -133,4 +128,11 @@ html > body {
|
|
|
133
128
|
mask-image: linear-gradient(#000, #000), linear-gradient(#000, #000), linear-gradient(#000, #000);
|
|
134
129
|
mask-repeat: no-repeat;
|
|
135
130
|
}
|
|
131
|
+
|
|
132
|
+
/* Disable Add2Cart: */
|
|
133
|
+
.dummy-add2cart-class {
|
|
134
|
+
pointer-events: none !important;
|
|
135
|
+
opacity: 0.5 !important;
|
|
136
|
+
filter: grayscale(1) !important;
|
|
137
|
+
}
|
|
136
138
|
}
|
|
@@ -51,10 +51,11 @@ export class ExecutableValidator {
|
|
|
51
51
|
}
|
|
52
52
|
getCurrentBrowserVersion() {
|
|
53
53
|
// on windows executing chrome to gather the version will spawn a chromeProcess
|
|
54
|
-
// so we use a powershell instead
|
|
54
|
+
// so we use a powershell instead. Emit `<ProductName> <FileVersion>` so the
|
|
55
|
+
// output is shape-compatible with `chrome --version` on macOS/Linux —
|
|
56
|
+
// downstream consumers (isChromeBrowser, log lines) can stay OS-agnostic.
|
|
55
57
|
if (this.userConfig.runtimeConfig.windows) {
|
|
56
|
-
return this.cli.execSync(
|
|
57
|
-
// return this.userConfig.chromePath;
|
|
58
|
+
return this.cli.execSync(`$i = (Get-Item '${this.userConfig.chromePath}').VersionInfo; "$($i.ProductName) $($i.FileVersion)"`, { shell: "powershell.exe" });
|
|
58
59
|
}
|
|
59
60
|
return this.cli.execSync(`"${this.userConfig.chromePath}" --version`);
|
|
60
61
|
}
|
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { DevelopmentToolsMessage } from "../../onboarding-model.js";
|
|
2
|
+
import { ExtensionBridge } from "./extension-bridge.js";
|
|
3
|
+
/**
|
|
4
|
+
* In-memory queue of `DevelopmentToolsMessage` snapshots delivered to the
|
|
5
|
+
* devtools extension. Producers (factory, onboarding-service CDP listeners)
|
|
6
|
+
* call `addMessage`. Once an `ExtensionBridge` is attached, every change
|
|
7
|
+
* triggers a fire-and-forget push that mirrors the full message array into
|
|
8
|
+
* the extension's `chrome.storage.local`.
|
|
9
|
+
*
|
|
10
|
+
* Messages added before the bridge is attached stay queued — `setBridge`
|
|
11
|
+
* flushes them.
|
|
12
|
+
*/
|
|
2
13
|
export declare class DevtoolsExtensionApi {
|
|
3
14
|
messages: DevelopmentToolsMessage[];
|
|
15
|
+
private bridge;
|
|
4
16
|
getMessages(): DevelopmentToolsMessage[];
|
|
5
17
|
addMessage(message: DevelopmentToolsMessage): void;
|
|
18
|
+
/**
|
|
19
|
+
* Replace the existing message with the same `id`, or push if none yet.
|
|
20
|
+
* Use this for "current state" messages (e.g. `localCacheError` carries
|
|
21
|
+
* the live error set, not a history of events) so the devtools panel
|
|
22
|
+
* doesn't have to dedupe.
|
|
23
|
+
*/
|
|
24
|
+
upsertMessage(message: DevelopmentToolsMessage): void;
|
|
6
25
|
clear(): void;
|
|
26
|
+
setBridge(bridge: ExtensionBridge): void;
|
|
27
|
+
private flush;
|
|
7
28
|
}
|
|
@@ -1,12 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory queue of `DevelopmentToolsMessage` snapshots delivered to the
|
|
3
|
+
* devtools extension. Producers (factory, onboarding-service CDP listeners)
|
|
4
|
+
* call `addMessage`. Once an `ExtensionBridge` is attached, every change
|
|
5
|
+
* triggers a fire-and-forget push that mirrors the full message array into
|
|
6
|
+
* the extension's `chrome.storage.local`.
|
|
7
|
+
*
|
|
8
|
+
* Messages added before the bridge is attached stay queued — `setBridge`
|
|
9
|
+
* flushes them.
|
|
10
|
+
*/
|
|
1
11
|
export class DevtoolsExtensionApi {
|
|
2
12
|
messages = [];
|
|
13
|
+
bridge = null;
|
|
3
14
|
getMessages() {
|
|
4
15
|
return this.messages;
|
|
5
16
|
}
|
|
6
17
|
addMessage(message) {
|
|
7
18
|
this.messages.push(message);
|
|
19
|
+
void this.flush();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Replace the existing message with the same `id`, or push if none yet.
|
|
23
|
+
* Use this for "current state" messages (e.g. `localCacheError` carries
|
|
24
|
+
* the live error set, not a history of events) so the devtools panel
|
|
25
|
+
* doesn't have to dedupe.
|
|
26
|
+
*/
|
|
27
|
+
upsertMessage(message) {
|
|
28
|
+
const idx = this.messages.findIndex((m) => m.id === message.id);
|
|
29
|
+
if (idx >= 0) {
|
|
30
|
+
this.messages[idx] = message;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
this.messages.push(message);
|
|
34
|
+
}
|
|
35
|
+
void this.flush();
|
|
8
36
|
}
|
|
9
37
|
clear() {
|
|
10
38
|
this.messages = [];
|
|
39
|
+
void this.flush();
|
|
40
|
+
}
|
|
41
|
+
setBridge(bridge) {
|
|
42
|
+
this.bridge = bridge;
|
|
43
|
+
void this.flush();
|
|
44
|
+
}
|
|
45
|
+
async flush() {
|
|
46
|
+
if (!this.bridge)
|
|
47
|
+
return;
|
|
48
|
+
await this.bridge.writeMessages(this.messages);
|
|
11
49
|
}
|
|
12
50
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Browser } from "puppeteer";
|
|
2
|
+
import { DevelopmentToolsMessage } from "../../onboarding-model.js";
|
|
3
|
+
import { CliServiceInterface } from "../../../cli/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Writes onboarding state (messages, status) into the devtools extension's
|
|
6
|
+
* chrome.storage.local via a CDP session against the extension's MV3
|
|
7
|
+
* service-worker target. The devtools extension subscribes via
|
|
8
|
+
* chrome.storage.onChanged. Replaces the legacy `window.skCliMessages` page
|
|
9
|
+
* injection — no more customer-visible global state.
|
|
10
|
+
*
|
|
11
|
+
* Lifecycle: construct once per browser, call `init()` after the browser is
|
|
12
|
+
* launched and the devtools extension has loaded, then call `writeMessages`
|
|
13
|
+
* whenever the message list changes.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ExtensionBridge {
|
|
16
|
+
private readonly cli;
|
|
17
|
+
private browser;
|
|
18
|
+
private extensionId;
|
|
19
|
+
constructor(cli: CliServiceInterface);
|
|
20
|
+
/**
|
|
21
|
+
* Bind the bridge to a freshly-launched browser, discover the
|
|
22
|
+
* devtools-extension's service-worker target, and cache its extension ID.
|
|
23
|
+
* Polls for ~INIT_POLL_TIMEOUT_MS because MV3 service workers are
|
|
24
|
+
* sometimes not registered as targets until Chrome has finished loading
|
|
25
|
+
* the extension. Throws if no matching SW target is found in time.
|
|
26
|
+
*/
|
|
27
|
+
init(browser: Browser): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Write the current messages snapshot into chrome.storage.local. One retry
|
|
30
|
+
* on transient failures (MV3 service workers can briefly disappear during
|
|
31
|
+
* respawn). Failures are surfaced via `cli.writeError` but do not throw —
|
|
32
|
+
* a missed write is degraded UX, not a fatal onboarding failure.
|
|
33
|
+
*/
|
|
34
|
+
writeMessages(messages: DevelopmentToolsMessage[]): Promise<void>;
|
|
35
|
+
private evaluateInServiceWorker;
|
|
36
|
+
/**
|
|
37
|
+
* Look up the service-worker target by cached extension ID. Returns null
|
|
38
|
+
* when the SW is dormant — callers handle that via retry.
|
|
39
|
+
*/
|
|
40
|
+
private getServiceWorkerTarget;
|
|
41
|
+
/**
|
|
42
|
+
* Scan service-worker targets, evaluate `chrome.runtime.getManifest().name`
|
|
43
|
+
* on each, and return the first whose name matches the devtools-extension
|
|
44
|
+
* heuristic. We discover the extension by name (not by computing the ID
|
|
45
|
+
* from the unpacked path) because unpacked-extension IDs depend on the
|
|
46
|
+
* absolute path and OS path normalization.
|
|
47
|
+
*/
|
|
48
|
+
private findDevtoolsServiceWorker;
|
|
49
|
+
private readManifestName;
|
|
50
|
+
private extractExtensionId;
|
|
51
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { safe } from "../../../../helpers/safe.js";
|
|
2
|
+
const STORAGE_KEY = "skCli";
|
|
3
|
+
const PROTOCOL_VERSION = 1;
|
|
4
|
+
const SW_LOOKUP_RETRY_MS = 250;
|
|
5
|
+
const INIT_POLL_INTERVAL_MS = 200;
|
|
6
|
+
const INIT_POLL_TIMEOUT_MS = 4000;
|
|
7
|
+
// Same name heuristic as ExtensionValidator.handleDevtoolsExtension.
|
|
8
|
+
const EXTENSION_NAME_MATCHERS = ["speed-kit devtools", "devtools extension"];
|
|
9
|
+
/**
|
|
10
|
+
* Writes onboarding state (messages, status) into the devtools extension's
|
|
11
|
+
* chrome.storage.local via a CDP session against the extension's MV3
|
|
12
|
+
* service-worker target. The devtools extension subscribes via
|
|
13
|
+
* chrome.storage.onChanged. Replaces the legacy `window.skCliMessages` page
|
|
14
|
+
* injection — no more customer-visible global state.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle: construct once per browser, call `init()` after the browser is
|
|
17
|
+
* launched and the devtools extension has loaded, then call `writeMessages`
|
|
18
|
+
* whenever the message list changes.
|
|
19
|
+
*/
|
|
20
|
+
export class ExtensionBridge {
|
|
21
|
+
cli;
|
|
22
|
+
browser = null;
|
|
23
|
+
extensionId = null;
|
|
24
|
+
constructor(cli) {
|
|
25
|
+
this.cli = cli;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Bind the bridge to a freshly-launched browser, discover the
|
|
29
|
+
* devtools-extension's service-worker target, and cache its extension ID.
|
|
30
|
+
* Polls for ~INIT_POLL_TIMEOUT_MS because MV3 service workers are
|
|
31
|
+
* sometimes not registered as targets until Chrome has finished loading
|
|
32
|
+
* the extension. Throws if no matching SW target is found in time.
|
|
33
|
+
*/
|
|
34
|
+
async init(browser) {
|
|
35
|
+
this.browser = browser;
|
|
36
|
+
const deadline = Date.now() + INIT_POLL_TIMEOUT_MS;
|
|
37
|
+
let target = await this.findDevtoolsServiceWorker();
|
|
38
|
+
while (!target && Date.now() < deadline) {
|
|
39
|
+
await sleep(INIT_POLL_INTERVAL_MS);
|
|
40
|
+
target = await this.findDevtoolsServiceWorker();
|
|
41
|
+
}
|
|
42
|
+
if (!target) {
|
|
43
|
+
throw new Error("ExtensionBridge: no devtools-extension service-worker target found " +
|
|
44
|
+
`within ${INIT_POLL_TIMEOUT_MS} ms. Confirm the extension is loaded ` +
|
|
45
|
+
"and its name matches one of: " +
|
|
46
|
+
EXTENSION_NAME_MATCHERS.join(", "));
|
|
47
|
+
}
|
|
48
|
+
this.extensionId = this.extractExtensionId(target.url());
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Write the current messages snapshot into chrome.storage.local. One retry
|
|
52
|
+
* on transient failures (MV3 service workers can briefly disappear during
|
|
53
|
+
* respawn). Failures are surfaced via `cli.writeError` but do not throw —
|
|
54
|
+
* a missed write is degraded UX, not a fatal onboarding failure.
|
|
55
|
+
*/
|
|
56
|
+
async writeMessages(messages) {
|
|
57
|
+
if (!this.browser || !this.extensionId) {
|
|
58
|
+
this.cli.writeWarning("ExtensionBridge.writeMessages called before init()");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const payload = {
|
|
62
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
63
|
+
writtenAt: new Date().toISOString(),
|
|
64
|
+
messages,
|
|
65
|
+
};
|
|
66
|
+
const first = await safe(this.evaluateInServiceWorker(payload));
|
|
67
|
+
if (first.success === true)
|
|
68
|
+
return;
|
|
69
|
+
await sleep(SW_LOOKUP_RETRY_MS);
|
|
70
|
+
const retry = await safe(this.evaluateInServiceWorker(payload));
|
|
71
|
+
if (retry.success !== true) {
|
|
72
|
+
this.cli.writeError(`ExtensionBridge.writeMessages failed: ${retry.error}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async evaluateInServiceWorker(payload) {
|
|
76
|
+
const target = await this.getServiceWorkerTarget();
|
|
77
|
+
if (!target) {
|
|
78
|
+
throw new Error("service-worker target not found");
|
|
79
|
+
}
|
|
80
|
+
const session = await target.createCDPSession();
|
|
81
|
+
try {
|
|
82
|
+
const expression = `chrome.storage.local.set(${JSON.stringify({
|
|
83
|
+
[STORAGE_KEY]: payload,
|
|
84
|
+
})})`;
|
|
85
|
+
const evalResult = await session.send("Runtime.evaluate", {
|
|
86
|
+
expression,
|
|
87
|
+
awaitPromise: true,
|
|
88
|
+
returnByValue: true,
|
|
89
|
+
});
|
|
90
|
+
if (evalResult.exceptionDetails) {
|
|
91
|
+
throw new Error(evalResult.exceptionDetails.text ||
|
|
92
|
+
"Runtime.evaluate threw an unknown exception");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await safe(session.detach());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Look up the service-worker target by cached extension ID. Returns null
|
|
101
|
+
* when the SW is dormant — callers handle that via retry.
|
|
102
|
+
*/
|
|
103
|
+
async getServiceWorkerTarget() {
|
|
104
|
+
if (!this.browser || !this.extensionId)
|
|
105
|
+
return null;
|
|
106
|
+
const prefix = `chrome-extension://${this.extensionId}/`;
|
|
107
|
+
for (const target of this.browser.targets()) {
|
|
108
|
+
if (target.type() === "service_worker" &&
|
|
109
|
+
target.url().startsWith(prefix)) {
|
|
110
|
+
return target;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Scan service-worker targets, evaluate `chrome.runtime.getManifest().name`
|
|
117
|
+
* on each, and return the first whose name matches the devtools-extension
|
|
118
|
+
* heuristic. We discover the extension by name (not by computing the ID
|
|
119
|
+
* from the unpacked path) because unpacked-extension IDs depend on the
|
|
120
|
+
* absolute path and OS path normalization.
|
|
121
|
+
*/
|
|
122
|
+
async findDevtoolsServiceWorker() {
|
|
123
|
+
if (!this.browser)
|
|
124
|
+
return null;
|
|
125
|
+
const swTargets = this.browser
|
|
126
|
+
.targets()
|
|
127
|
+
.filter((t) => t.type() === "service_worker");
|
|
128
|
+
for (const target of swTargets) {
|
|
129
|
+
const name = await safe(this.readManifestName(target));
|
|
130
|
+
if (!name.success || !name.data)
|
|
131
|
+
continue;
|
|
132
|
+
const lower = name.data.toLowerCase();
|
|
133
|
+
if (EXTENSION_NAME_MATCHERS.some((m) => lower.includes(m))) {
|
|
134
|
+
return target;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
async readManifestName(target) {
|
|
140
|
+
const session = await target.createCDPSession();
|
|
141
|
+
try {
|
|
142
|
+
const evalResult = await session.send("Runtime.evaluate", {
|
|
143
|
+
expression: "chrome.runtime.getManifest().name",
|
|
144
|
+
returnByValue: true,
|
|
145
|
+
});
|
|
146
|
+
if (evalResult.exceptionDetails)
|
|
147
|
+
return null;
|
|
148
|
+
const value = evalResult.result.value;
|
|
149
|
+
return typeof value === "string" ? value : null;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await safe(session.detach());
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
extractExtensionId(url) {
|
|
156
|
+
const match = url.match(/^chrome-extension:\/\/([a-p]{32})\//);
|
|
157
|
+
if (!match) {
|
|
158
|
+
throw new Error(`Could not extract extension ID from URL: ${url}`);
|
|
159
|
+
}
|
|
160
|
+
return match[1];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function sleep(ms) {
|
|
164
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
165
|
+
}
|
|
@@ -2,6 +2,7 @@ import { CDPSession } from "puppeteer";
|
|
|
2
2
|
import { Protocol } from "puppeteer-core";
|
|
3
3
|
import { UserCliConfig } from "../../helpers/cli-config.js";
|
|
4
4
|
import { AbortResponse } from "./browser/abort-response.js";
|
|
5
|
+
import { task } from "./todo-collector.js";
|
|
5
6
|
import { VanillaPuppeteer } from "puppeteer-extra";
|
|
6
7
|
import { Command } from "@oclif/core/command";
|
|
7
8
|
export declare const BROWSER_EVENTS: {
|
|
@@ -38,9 +39,10 @@ export declare class BrowserContext {
|
|
|
38
39
|
userAgent?: string;
|
|
39
40
|
readonly debuggingPort: boolean;
|
|
40
41
|
readonly headless: boolean;
|
|
42
|
+
readonly debugPort: number;
|
|
41
43
|
readonly browserArgs: string[];
|
|
42
44
|
readonly chromePath: string;
|
|
43
|
-
constructor(domain: string, browserVersionString?: string, chromeFlags?: string[], userConfig?: UserCliConfig, userAgent?: string, debuggingPort?: boolean, headless?: boolean);
|
|
45
|
+
constructor(domain: string, browserVersionString?: string, chromeFlags?: string[], userConfig?: UserCliConfig, userAgent?: string, debuggingPort?: boolean, headless?: boolean, debugPort?: number);
|
|
44
46
|
private isChromeBrowser;
|
|
45
47
|
private getBrowserExtensionPath;
|
|
46
48
|
getPuppeteerLaunchOptions(): Parameters<VanillaPuppeteer["launch"]>[0];
|
|
@@ -60,12 +62,13 @@ export declare class OnboardingContext {
|
|
|
60
62
|
readonly useTestConfig: boolean;
|
|
61
63
|
readonly debuggingPort: boolean;
|
|
62
64
|
readonly headless: boolean;
|
|
65
|
+
readonly debugPort: number;
|
|
63
66
|
static flags: Record<string, Command.Flag>;
|
|
64
67
|
readonly DEFAULT_DF_PATH = "https://www.baqend.com/speed-kit/latest/dynamic-fetcher.js";
|
|
65
68
|
readonly DEFAULT_DOCUMENT_HANDLER_PATH = "https://www.baqend.com/speed-kit-handler/latest/DocumentHandler.js";
|
|
66
69
|
readonly DEFAULT_SNIPPET_PATH = "https://www.baqend.com/speed-kit/latest/snippet.js";
|
|
67
70
|
readonly DEFAULT_SW_PATH = "https://www.baqend.com/speed-kit/latest/sw.js";
|
|
68
|
-
constructor(customerPath: string, configName: string, domain: string, local: boolean, ignoreContentSecurityPolicy: boolean, cliPath: string, verboseLevel?: boolean, useTestConfig?: boolean, debuggingPort?: boolean, headless?: boolean);
|
|
71
|
+
constructor(customerPath: string, configName: string, domain: string, local: boolean, ignoreContentSecurityPolicy: boolean, cliPath: string, verboseLevel?: boolean, useTestConfig?: boolean, debuggingPort?: boolean, headless?: boolean, debugPort?: number);
|
|
69
72
|
}
|
|
70
73
|
export interface SpeedKitServerConfigAuthentication {
|
|
71
74
|
password: string;
|
|
@@ -105,10 +108,45 @@ export interface SpeedKitServerConfig {
|
|
|
105
108
|
variations: SpeedKitServerConfigVariation;
|
|
106
109
|
};
|
|
107
110
|
}
|
|
108
|
-
export interface
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
export interface SkCliInfo {
|
|
112
|
+
version: string;
|
|
113
|
+
app: string;
|
|
114
|
+
customerConfigPath: string;
|
|
111
115
|
}
|
|
116
|
+
export interface AthenaInfo {
|
|
117
|
+
client: boolean;
|
|
118
|
+
}
|
|
119
|
+
export interface AppTokenInfo {
|
|
120
|
+
app: string;
|
|
121
|
+
token: string;
|
|
122
|
+
}
|
|
123
|
+
export interface LocalCacheError {
|
|
124
|
+
url: string;
|
|
125
|
+
assetUrl: string;
|
|
126
|
+
variation: string;
|
|
127
|
+
error: string;
|
|
128
|
+
stack: string | null;
|
|
129
|
+
occurredAt: number;
|
|
130
|
+
}
|
|
131
|
+
export type DevelopmentToolsMessage = {
|
|
132
|
+
id: "skCli";
|
|
133
|
+
data: SkCliInfo;
|
|
134
|
+
} | {
|
|
135
|
+
id: "athena";
|
|
136
|
+
data: AthenaInfo;
|
|
137
|
+
} | {
|
|
138
|
+
id: "todo";
|
|
139
|
+
data: task[];
|
|
140
|
+
} | {
|
|
141
|
+
id: "appToken";
|
|
142
|
+
data: AppTokenInfo;
|
|
143
|
+
} | {
|
|
144
|
+
id: "serviceWorker";
|
|
145
|
+
data: string;
|
|
146
|
+
} | {
|
|
147
|
+
id: "localCacheError";
|
|
148
|
+
data: LocalCacheError[];
|
|
149
|
+
};
|
|
112
150
|
export declare const USER_AGENT: {
|
|
113
151
|
DESKTOP: string;
|
|
114
152
|
MOBILE: string;
|
|
@@ -46,9 +46,10 @@ export class BrowserContext {
|
|
|
46
46
|
userAgent;
|
|
47
47
|
debuggingPort;
|
|
48
48
|
headless;
|
|
49
|
+
debugPort;
|
|
49
50
|
browserArgs;
|
|
50
51
|
chromePath;
|
|
51
|
-
constructor(domain, browserVersionString = "unknown", chromeFlags, userConfig, userAgent, debuggingPort = false, headless = false) {
|
|
52
|
+
constructor(domain, browserVersionString = "unknown", chromeFlags, userConfig, userAgent, debuggingPort = false, headless = false, debugPort = 9222) {
|
|
52
53
|
this.domain = domain;
|
|
53
54
|
this.browserVersionString = browserVersionString;
|
|
54
55
|
this.chromeFlags = chromeFlags;
|
|
@@ -56,6 +57,7 @@ export class BrowserContext {
|
|
|
56
57
|
this.userAgent = userAgent;
|
|
57
58
|
this.debuggingPort = debuggingPort;
|
|
58
59
|
this.headless = headless;
|
|
60
|
+
this.debugPort = debugPort;
|
|
59
61
|
this.domain = domain;
|
|
60
62
|
this.chromeFlags = chromeFlags || [];
|
|
61
63
|
this.chromePath = resolve(userConfig.chromePath);
|
|
@@ -106,7 +108,7 @@ export class BrowserContext {
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
if (this.debuggingPort || this.headless) {
|
|
109
|
-
options.args.push(
|
|
111
|
+
options.args.push(`--remote-debugging-port=${this.debugPort}`);
|
|
110
112
|
}
|
|
111
113
|
return options;
|
|
112
114
|
}
|
|
@@ -122,6 +124,7 @@ export class OnboardingContext {
|
|
|
122
124
|
useTestConfig;
|
|
123
125
|
debuggingPort;
|
|
124
126
|
headless;
|
|
127
|
+
debugPort;
|
|
125
128
|
static flags = {
|
|
126
129
|
...CLI_CONFIG_NAME,
|
|
127
130
|
...CLI_CONFIG_IS_TEST,
|
|
@@ -139,6 +142,10 @@ export class OnboardingContext {
|
|
|
139
142
|
default: false,
|
|
140
143
|
description: "Enable remote debugging port (disables speculation rules/prerendering)",
|
|
141
144
|
}),
|
|
145
|
+
debugPort: Flags.integer({
|
|
146
|
+
default: 9222,
|
|
147
|
+
description: "Chrome remote debugging port (only used with -p/--headless). Use distinct ports to run multiple onboarding processes at once.",
|
|
148
|
+
}),
|
|
142
149
|
verboseLevel: Flags.boolean({
|
|
143
150
|
char: "v",
|
|
144
151
|
default: false,
|
|
@@ -158,7 +165,7 @@ export class OnboardingContext {
|
|
|
158
165
|
DEFAULT_DOCUMENT_HANDLER_PATH = "https://www.baqend.com/speed-kit-handler/latest/DocumentHandler.js";
|
|
159
166
|
DEFAULT_SNIPPET_PATH = "https://www.baqend.com/speed-kit/latest/snippet.js";
|
|
160
167
|
DEFAULT_SW_PATH = "https://www.baqend.com/speed-kit/latest/sw.js";
|
|
161
|
-
constructor(customerPath, configName, domain, local, ignoreContentSecurityPolicy = false, cliPath, verboseLevel, useTestConfig = false, debuggingPort = false, headless = false) {
|
|
168
|
+
constructor(customerPath, configName, domain, local, ignoreContentSecurityPolicy = false, cliPath, verboseLevel, useTestConfig = false, debuggingPort = false, headless = false, debugPort = 9222) {
|
|
162
169
|
this.customerPath = customerPath;
|
|
163
170
|
this.configName = configName;
|
|
164
171
|
this.domain = domain;
|
|
@@ -169,6 +176,7 @@ export class OnboardingContext {
|
|
|
169
176
|
this.useTestConfig = useTestConfig;
|
|
170
177
|
this.debuggingPort = debuggingPort;
|
|
171
178
|
this.headless = headless;
|
|
179
|
+
this.debugPort = debugPort;
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
182
|
export const USER_AGENT = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { BrowserContext } from "./onboarding-model.js";
|
|
4
|
+
// Providing chromeUserProfilePath keeps BrowserContext off the filesystem
|
|
5
|
+
// (it skips the temp-profile branch in the constructor).
|
|
6
|
+
const userConfig = {
|
|
7
|
+
chromePath: "/usr/bin/google-chrome",
|
|
8
|
+
chromeUserProfilePath: "/tmp/sk-test-profile",
|
|
9
|
+
};
|
|
10
|
+
function remoteDebuggingArg(debuggingPort, headless, debugPort) {
|
|
11
|
+
const context = new BrowserContext("example.com", "Chrome/120", [], userConfig, undefined, debuggingPort, headless, debugPort);
|
|
12
|
+
const args = context.getPuppeteerLaunchOptions().args;
|
|
13
|
+
return args.find((arg) => arg.startsWith("--remote-debugging-port="));
|
|
14
|
+
}
|
|
15
|
+
describe("BrowserContext.getPuppeteerLaunchOptions remote debugging port", () => {
|
|
16
|
+
it("defaults to port 9222 when debugging is enabled without a custom port", () => {
|
|
17
|
+
expect(remoteDebuggingArg(true, false)).to.equal("--remote-debugging-port=9222");
|
|
18
|
+
});
|
|
19
|
+
it("uses the provided debug port when debugging is enabled", () => {
|
|
20
|
+
expect(remoteDebuggingArg(true, false, 9223)).to.equal("--remote-debugging-port=9223");
|
|
21
|
+
});
|
|
22
|
+
it("uses the provided debug port in headless mode", () => {
|
|
23
|
+
expect(remoteDebuggingArg(false, true, 9224)).to.equal("--remote-debugging-port=9224");
|
|
24
|
+
});
|
|
25
|
+
it("omits the remote debugging port when debugging and headless are both off", () => {
|
|
26
|
+
expect(remoteDebuggingArg(false, false, 9223)).to.equal(undefined);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -27,6 +27,7 @@ import { InstallSpeedKitHtmlTemplate } from "./templates/install-speed-kit-html-
|
|
|
27
27
|
import { InstallSpeedKitJsTemplate } from "./templates/install-speed-kit-js-template.js";
|
|
28
28
|
import { ExtensionValidator } from "./browser/extension/extension-validator.js";
|
|
29
29
|
import { DevtoolsExtensionApi } from "./browser/extension/devtools-extension-api.js";
|
|
30
|
+
import { ExtensionBridge } from "./browser/extension/extension-bridge.js";
|
|
30
31
|
import { TodoCollector } from "./todo-collector.js";
|
|
31
32
|
import { Crawler } from "./virtual-orestes-app/crawler.js";
|
|
32
33
|
import { VirtualOrestesApp } from "./virtual-orestes-app/index.js";
|
|
@@ -72,21 +73,30 @@ export class OnboardingServiceFactory {
|
|
|
72
73
|
this.cliConfig.chromeExtensionPaths = extensionPaths.join(",");
|
|
73
74
|
const browserValidator = new ExecutableValidator(this.cliConfig, cli);
|
|
74
75
|
const browserVersionString = await browserValidator.validateOrInstall();
|
|
75
|
-
const browserContext = new BrowserContext(domainToStart, browserVersionString, customerConfig.chromeFlags || [], this.cliConfig, undefined, this.context.debuggingPort, this.context.headless);
|
|
76
|
+
const browserContext = new BrowserContext(domainToStart, browserVersionString, customerConfig.chromeFlags || [], this.cliConfig, undefined, this.context.debuggingPort, this.context.headless, this.context.debugPort);
|
|
76
77
|
if (this.context.debuggingPort || this.context.headless) {
|
|
77
|
-
cli.writeWarning(
|
|
78
|
+
cli.writeWarning(`Chrome remote debugging enabled on port ${this.context.debugPort}.`);
|
|
78
79
|
}
|
|
79
80
|
if (this.context.headless) {
|
|
80
81
|
cli.writeWarning("Running in headless mode.");
|
|
81
82
|
}
|
|
82
83
|
const fileWatcher = new FileWatcher(cli, customerConfig, files, documentHandler, browserContext, cache);
|
|
83
84
|
const athenaClient = (await this.getAthenaClient()) || null;
|
|
84
|
-
const fetchHandler = await this.getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient);
|
|
85
85
|
const messageHandler = await this.prepareMessagehandler(athenaClient, files, customerConfig);
|
|
86
|
-
|
|
86
|
+
const fetchHandler = await this.getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient, messageHandler);
|
|
87
|
+
const extensionBridge = new ExtensionBridge(cli);
|
|
88
|
+
return new OnboardingService(browserContext, customerConfig, fetchHandler, fileWatcher, cli, messageHandler, extensionBridge);
|
|
87
89
|
}
|
|
88
90
|
async prepareMessagehandler(athenaClient, files, customerConfig) {
|
|
89
91
|
const messageHandler = new DevtoolsExtensionApi();
|
|
92
|
+
messageHandler.addMessage({
|
|
93
|
+
id: "skCli",
|
|
94
|
+
data: {
|
|
95
|
+
version: this.cliConfig.runtimeConfig.version,
|
|
96
|
+
app: customerConfig.app,
|
|
97
|
+
customerConfigPath: path.resolve(this.context.customerPath, this.context.configName),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
90
100
|
messageHandler.addMessage({
|
|
91
101
|
id: "athena",
|
|
92
102
|
data: { client: !!athenaClient },
|
|
@@ -135,7 +145,7 @@ export class OnboardingServiceFactory {
|
|
|
135
145
|
const configApiContext = new ConfigApiContext(app);
|
|
136
146
|
return new ConfigApiServiceFactory(configApiContext).getService();
|
|
137
147
|
}
|
|
138
|
-
async getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient) {
|
|
148
|
+
async getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient, messageHandler) {
|
|
139
149
|
const configApi = this.getConfigApi(customerConfig.app);
|
|
140
150
|
const serverConfig = await this.getSpeedKitServerConfig(configApi, cli);
|
|
141
151
|
const agent = this.getCrawlerAgent(customerConfig, cli);
|
|
@@ -156,7 +166,7 @@ export class OnboardingServiceFactory {
|
|
|
156
166
|
new DashboardRequest(new Dashboard(customerConfig, parameterQueryBuilder, athenaClient, requestDiffService, new DiffAgainstCurrentPage(DiffService, documentHandler, crawler, cli), cache)),
|
|
157
167
|
];
|
|
158
168
|
if (this.context.local) {
|
|
159
|
-
const orestesApp = new VirtualOrestesApp(customerConfig, crawler, documentHandler, cache, cli);
|
|
169
|
+
const orestesApp = new VirtualOrestesApp(customerConfig, crawler, documentHandler, cache, cli, messageHandler);
|
|
160
170
|
handlers.push(new SpeedKitAssetRequest(customerConfig, orestesApp), new SpeedKitRumPiRequest(customerConfig, cache, cli));
|
|
161
171
|
}
|
|
162
172
|
return new FetchEventHandler(handlers, customerConfig, cli, configApi);
|
|
@@ -4,6 +4,7 @@ import { FetchEventHandler } from "./fetch-event-handler.js";
|
|
|
4
4
|
import { FileWatcher } from "./file-events/file-watcher.js";
|
|
5
5
|
import { CliService } from "../cli/index.js";
|
|
6
6
|
import { DevtoolsExtensionApi } from "./browser/extension/devtools-extension-api.js";
|
|
7
|
+
import { ExtensionBridge } from "./browser/extension/extension-bridge.js";
|
|
7
8
|
/**
|
|
8
9
|
* Represents the OnboardingService which is responsible for setting up the browser to test a config using Puppeteer.
|
|
9
10
|
*/
|
|
@@ -14,8 +15,9 @@ export declare class OnboardingService {
|
|
|
14
15
|
private fileWatcher;
|
|
15
16
|
private cli;
|
|
16
17
|
private developmentToolsMessages;
|
|
18
|
+
private extensionBridge;
|
|
17
19
|
private reinstallOnNextNavigate;
|
|
18
|
-
constructor(browserContext: BrowserContext, customerConfig: CustomerConfig, fetchEventHandler: FetchEventHandler, fileWatcher: FileWatcher, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi);
|
|
20
|
+
constructor(browserContext: BrowserContext, customerConfig: CustomerConfig, fetchEventHandler: FetchEventHandler, fileWatcher: FileWatcher, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi, extensionBridge: ExtensionBridge);
|
|
19
21
|
/**
|
|
20
22
|
* Prepare the puppeteer browser, wire event listeners, navigate to the
|
|
21
23
|
* install page, then block until either the caller aborts the signal
|
|
@@ -25,6 +27,14 @@ export declare class OnboardingService {
|
|
|
25
27
|
run(externalSignal: AbortSignal): Promise<void>;
|
|
26
28
|
private prepareBrowser;
|
|
27
29
|
private preparePage;
|
|
30
|
+
/**
|
|
31
|
+
* Bind the injected ExtensionBridge to the freshly-launched browser so
|
|
32
|
+
* message updates are mirrored into the devtools extension's
|
|
33
|
+
* `chrome.storage.local`. A failure here is non-fatal: onboarding still
|
|
34
|
+
* proceeds, but the devtools panel will not receive live updates from
|
|
35
|
+
* sk-cli.
|
|
36
|
+
*/
|
|
37
|
+
private attachExtensionBridge;
|
|
28
38
|
private findPage;
|
|
29
39
|
private prepareCleanState;
|
|
30
40
|
private reinstallSpeedKit;
|
|
@@ -11,14 +11,16 @@ export class OnboardingService {
|
|
|
11
11
|
fileWatcher;
|
|
12
12
|
cli;
|
|
13
13
|
developmentToolsMessages;
|
|
14
|
+
extensionBridge;
|
|
14
15
|
reinstallOnNextNavigate = false;
|
|
15
|
-
constructor(browserContext, customerConfig, fetchEventHandler, fileWatcher, cli, developmentToolsMessages) {
|
|
16
|
+
constructor(browserContext, customerConfig, fetchEventHandler, fileWatcher, cli, developmentToolsMessages, extensionBridge) {
|
|
16
17
|
this.browserContext = browserContext;
|
|
17
18
|
this.customerConfig = customerConfig;
|
|
18
19
|
this.fetchEventHandler = fetchEventHandler;
|
|
19
20
|
this.fileWatcher = fileWatcher;
|
|
20
21
|
this.cli = cli;
|
|
21
22
|
this.developmentToolsMessages = developmentToolsMessages;
|
|
23
|
+
this.extensionBridge = extensionBridge;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Prepare the puppeteer browser, wire event listeners, navigate to the
|
|
@@ -34,6 +36,7 @@ export class OnboardingService {
|
|
|
34
36
|
localController.signal,
|
|
35
37
|
]);
|
|
36
38
|
const browser = await this.prepareBrowser(localController);
|
|
39
|
+
await this.attachExtensionBridge(browser);
|
|
37
40
|
const page = await this.preparePage(browser, shutdownSignal);
|
|
38
41
|
this.cli.successAction("ONBOARDING:PREPARE:BROWSER");
|
|
39
42
|
await this.navigate(page, `${this.customerConfig.scope}speed-kit-install.html?url=${this.customerConfig.scope}`);
|
|
@@ -58,17 +61,23 @@ export class OnboardingService {
|
|
|
58
61
|
await safe(this.prepareCleanState(page));
|
|
59
62
|
this.registerFileWatcher(page, shutdownSignal);
|
|
60
63
|
this.registerReinstallSpeedKitOnNavigate(page);
|
|
61
|
-
this.developmentToolsMessages.addMessage({
|
|
62
|
-
id: "cli",
|
|
63
|
-
data: `config loaded for app "${this.customerConfig.app}"`,
|
|
64
|
-
});
|
|
65
|
-
const messages = this.developmentToolsMessages.messages;
|
|
66
|
-
await page.evaluateOnNewDocument((messages) => {
|
|
67
|
-
// @ts-expect-error unknown method getSkCliMessages
|
|
68
|
-
window.skCliMessages = messages;
|
|
69
|
-
}, messages);
|
|
70
64
|
return page;
|
|
71
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Bind the injected ExtensionBridge to the freshly-launched browser so
|
|
68
|
+
* message updates are mirrored into the devtools extension's
|
|
69
|
+
* `chrome.storage.local`. A failure here is non-fatal: onboarding still
|
|
70
|
+
* proceeds, but the devtools panel will not receive live updates from
|
|
71
|
+
* sk-cli.
|
|
72
|
+
*/
|
|
73
|
+
async attachExtensionBridge(browser) {
|
|
74
|
+
const result = await safe(this.extensionBridge.init(browser));
|
|
75
|
+
if (result.success !== true) {
|
|
76
|
+
this.cli.writeWarning(`ExtensionBridge unavailable, devtools panel will not receive live updates: ${result.error}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.developmentToolsMessages.setBridge(this.extensionBridge);
|
|
80
|
+
}
|
|
72
81
|
async findPage(browser) {
|
|
73
82
|
if (!browser)
|
|
74
83
|
throw new Error("Please start a browser first.");
|
|
@@ -5,15 +5,22 @@ import { Cache } from "../request-cache/cache.js";
|
|
|
5
5
|
import { CliService } from "../../cli/index.js";
|
|
6
6
|
import { Crawler } from "./crawler.js";
|
|
7
7
|
import { CustomerConfig } from "../../integration-api/index.js";
|
|
8
|
+
import { DevtoolsExtensionApi } from "../browser/extension/devtools-extension-api.js";
|
|
8
9
|
export declare class VirtualOrestesApp {
|
|
9
10
|
private customerConfig;
|
|
10
11
|
private crawler;
|
|
11
12
|
private documentHandler;
|
|
12
13
|
private cache;
|
|
13
14
|
private cli;
|
|
14
|
-
|
|
15
|
+
private developmentToolsMessages;
|
|
16
|
+
private localCacheErrors;
|
|
17
|
+
constructor(customerConfig: CustomerConfig, crawler: Crawler, documentHandler: DocumentHandlerServer, cache: Cache, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi);
|
|
15
18
|
fetchAssetResponse(event: Protocol.Fetch.RequestPausedEvent): Promise<Partial<Protocol.Fetch.FulfillRequestRequest> | AbortResponse>;
|
|
16
19
|
private getResponseText;
|
|
20
|
+
private localCacheErrorKey;
|
|
21
|
+
private recordLocalCacheError;
|
|
22
|
+
private clearLocalCacheError;
|
|
23
|
+
private emitLocalCacheErrors;
|
|
17
24
|
private returnErrorResponse;
|
|
18
25
|
private convertResponseHeadersToOrestesFormat;
|
|
19
26
|
private returnRedirect;
|
|
@@ -13,12 +13,19 @@ export class VirtualOrestesApp {
|
|
|
13
13
|
documentHandler;
|
|
14
14
|
cache;
|
|
15
15
|
cli;
|
|
16
|
-
|
|
16
|
+
developmentToolsMessages;
|
|
17
|
+
// Local-mode transform errors keyed by `${url}|${variation}`. Emitted to
|
|
18
|
+
// the devtools panel via DevtoolsExtensionApi.upsertMessage so the panel
|
|
19
|
+
// can surface an "error for this URL" affordance in the Causes & sources
|
|
20
|
+
// card. Cleared per-URL on the next successful transform.
|
|
21
|
+
localCacheErrors = new Map();
|
|
22
|
+
constructor(customerConfig, crawler, documentHandler, cache, cli, developmentToolsMessages) {
|
|
17
23
|
this.customerConfig = customerConfig;
|
|
18
24
|
this.crawler = crawler;
|
|
19
25
|
this.documentHandler = documentHandler;
|
|
20
26
|
this.cache = cache;
|
|
21
27
|
this.cli = cli;
|
|
28
|
+
this.developmentToolsMessages = developmentToolsMessages;
|
|
22
29
|
}
|
|
23
30
|
async fetchAssetResponse(event) {
|
|
24
31
|
const originUrl = this.getAssetUrl(event.request.url);
|
|
@@ -56,8 +63,10 @@ export class VirtualOrestesApp {
|
|
|
56
63
|
}
|
|
57
64
|
const customResponse = await safe(this.documentHandler.transform(`text/html;charset=${textEncoding}`, responseContent, variation, originUrl, this.convertResponseHeadersToOrestesFormat(response)));
|
|
58
65
|
if (customResponse.success === false) {
|
|
66
|
+
this.recordLocalCacheError(originUrl, event.request.url, variation, customResponse);
|
|
59
67
|
return this.returnErrorResponse(customResponse, customHeaders);
|
|
60
68
|
}
|
|
69
|
+
this.clearLocalCacheError(originUrl, variation);
|
|
61
70
|
const skResponse = customResponse.data;
|
|
62
71
|
if (!Buffer.isBuffer(skResponse.body) && this.customerConfig.forceInstall) {
|
|
63
72
|
skResponse.body = this.rewriteInstallResource(skResponse.body);
|
|
@@ -108,6 +117,33 @@ export class VirtualOrestesApp {
|
|
|
108
117
|
textEncoding: encodingFromBodyMeta ?? encodingFromHeader,
|
|
109
118
|
};
|
|
110
119
|
}
|
|
120
|
+
localCacheErrorKey(url, variation) {
|
|
121
|
+
return `${url}|${variation}`;
|
|
122
|
+
}
|
|
123
|
+
recordLocalCacheError(url, assetUrl, variation, customResponse) {
|
|
124
|
+
this.localCacheErrors.set(this.localCacheErrorKey(url, variation), {
|
|
125
|
+
url,
|
|
126
|
+
assetUrl,
|
|
127
|
+
variation,
|
|
128
|
+
error: customResponse.error,
|
|
129
|
+
stack: customResponse.errorObj?.stack ?? null,
|
|
130
|
+
occurredAt: Date.now(),
|
|
131
|
+
});
|
|
132
|
+
this.emitLocalCacheErrors();
|
|
133
|
+
}
|
|
134
|
+
clearLocalCacheError(url, variation) {
|
|
135
|
+
const key = this.localCacheErrorKey(url, variation);
|
|
136
|
+
if (!this.localCacheErrors.has(key))
|
|
137
|
+
return;
|
|
138
|
+
this.localCacheErrors.delete(key);
|
|
139
|
+
this.emitLocalCacheErrors();
|
|
140
|
+
}
|
|
141
|
+
emitLocalCacheErrors() {
|
|
142
|
+
this.developmentToolsMessages.upsertMessage({
|
|
143
|
+
id: "localCacheError",
|
|
144
|
+
data: [...this.localCacheErrors.values()],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
111
147
|
returnErrorResponse(customResponse, customHeaders) {
|
|
112
148
|
const message = `Could not transform html: - ${customResponse.error}`;
|
|
113
149
|
this.cli.writeError(`[VirtualOrestes]: ${message}`);
|
package/oclif.manifest.json
CHANGED
|
@@ -500,6 +500,14 @@
|
|
|
500
500
|
"allowNo": false,
|
|
501
501
|
"type": "boolean"
|
|
502
502
|
},
|
|
503
|
+
"debugPort": {
|
|
504
|
+
"description": "Chrome remote debugging port (only used with -p/--headless). Use distinct ports to run multiple onboarding processes at once.",
|
|
505
|
+
"name": "debugPort",
|
|
506
|
+
"default": 9222,
|
|
507
|
+
"hasDynamicHelp": false,
|
|
508
|
+
"multiple": false,
|
|
509
|
+
"type": "option"
|
|
510
|
+
},
|
|
503
511
|
"verboseLevel": {
|
|
504
512
|
"char": "v",
|
|
505
513
|
"description": "Show all messages",
|
|
@@ -751,5 +759,5 @@
|
|
|
751
759
|
]
|
|
752
760
|
}
|
|
753
761
|
},
|
|
754
|
-
"version": "4.
|
|
762
|
+
"version": "4.7.0"
|
|
755
763
|
}
|