@speedkit/cli 4.22.0 → 4.23.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 +19 -0
- package/README.md +37 -1
- package/dist/commands/origin-request.d.ts +21 -0
- package/dist/commands/origin-request.js +49 -0
- package/dist/services/bundler/bundle-service.d.ts +12 -0
- package/dist/services/bundler/bundle-service.js +28 -1
- package/dist/services/bundler/bundle-service.spec.d.ts +1 -0
- package/dist/services/bundler/bundle-service.spec.js +54 -0
- package/dist/services/onboarding/virtual-orestes-app/crawler.d.ts +12 -43
- package/dist/services/onboarding/virtual-orestes-app/crawler.js +42 -133
- package/dist/services/origin-request/asset-variations.d.ts +47 -0
- package/dist/services/origin-request/asset-variations.js +170 -0
- package/dist/services/origin-request/asset-variations.spec.d.ts +1 -0
- package/dist/services/origin-request/asset-variations.spec.js +74 -0
- package/dist/services/origin-request/index.d.ts +4 -0
- package/dist/services/origin-request/index.js +4 -0
- package/dist/services/origin-request/origin-request-context.d.ts +13 -0
- package/dist/services/origin-request/origin-request-context.js +23 -0
- package/dist/services/origin-request/origin-request-service-factory.d.ts +13 -0
- package/dist/services/origin-request/origin-request-service-factory.js +32 -0
- package/dist/services/origin-request/origin-request-service.d.ts +33 -0
- package/dist/services/origin-request/origin-request-service.js +135 -0
- package/oclif.manifest.json +90 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# [4.23.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.22.1...v4.23.0) (2026-08-21)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **onboarding:** fetch origin exactly as the server does ([8da54b0](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/8da54b0ac0ffa04671cb3c7e97988e8ac871f2c5))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **origin-request:** simulate the server's origin request for a variation ([dc414e0](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/dc414e03142cab154c8f179cdb67f58922a6d38b))
|
|
12
|
+
|
|
13
|
+
## [4.22.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.22.0...v4.22.1) (2026-08-21)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **bundler:** resolve alias targets from the customer directory ([ad30e2a](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/ad30e2ac48a84c7592dd59d41004c301df212205))
|
|
19
|
+
|
|
1
20
|
# [4.22.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.21.0...v4.22.0) (2026-08-20)
|
|
2
21
|
|
|
3
22
|
|
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.23.0 linux-x64 node-v22.23.2
|
|
25
25
|
$ sk --help [COMMAND]
|
|
26
26
|
USAGE
|
|
27
27
|
$ sk COMMAND
|
|
@@ -88,6 +88,7 @@ By either...
|
|
|
88
88
|
* [`sk help [COMMAND]`](#sk-help-command)
|
|
89
89
|
* [`sk login [APP]`](#sk-login-app)
|
|
90
90
|
* [`sk onboarding CUSTOMERPATH`](#sk-onboarding-customerpath)
|
|
91
|
+
* [`sk origin-request CUSTOMERPATH PAGEURL`](#sk-origin-request-customerpath-pageurl)
|
|
91
92
|
* [`sk param-diff PAGEURL`](#sk-param-diff-pageurl)
|
|
92
93
|
* [`sk prewarm APP PATH`](#sk-prewarm-app-path)
|
|
93
94
|
* [`sk pull CUSTOMERPATH`](#sk-pull-customerpath)
|
|
@@ -620,6 +621,41 @@ EXAMPLES
|
|
|
620
621
|
$ sk onboarding customers/decathlon.de -c production -d www.decathlon.de
|
|
621
622
|
```
|
|
622
623
|
|
|
624
|
+
## `sk origin-request CUSTOMERPATH PAGEURL`
|
|
625
|
+
|
|
626
|
+
Request a page from origin with the exact headers Speed Kit's server sends for a cache variation, using the app's deployed runtime config
|
|
627
|
+
|
|
628
|
+
```
|
|
629
|
+
USAGE
|
|
630
|
+
$ sk origin-request CUSTOMERPATH PAGEURL [-c <value>] [-v <value>] [--host <value>] [--body <value>] [--grep
|
|
631
|
+
<value>] [--json] [--variations <value>]
|
|
632
|
+
|
|
633
|
+
ARGUMENTS
|
|
634
|
+
CUSTOMERPATH The customer config path
|
|
635
|
+
PAGEURL Page to request from origin
|
|
636
|
+
|
|
637
|
+
FLAGS
|
|
638
|
+
-c, --configName=<value> [default: production] The costumer config name
|
|
639
|
+
-v, --variation=<value> Cache variation to simulate, e.g. desktop-fmarkt-variant-e_1907090. Omitted means the
|
|
640
|
+
default variation
|
|
641
|
+
--body=<value> Write the response body to this file
|
|
642
|
+
--grep=<value> Print body lines matching this regex, case-insensitive
|
|
643
|
+
--host=<value> Send this Host header, emulating an origin-map host override
|
|
644
|
+
--json Machine-readable output
|
|
645
|
+
--variations=<value> Simulate against a runtime config in a local file instead of the app's deployed one
|
|
646
|
+
|
|
647
|
+
DESCRIPTION
|
|
648
|
+
Request a page from origin with the exact headers Speed Kit's server sends for a cache variation, using the app's
|
|
649
|
+
deployed runtime config
|
|
650
|
+
|
|
651
|
+
EXAMPLES
|
|
652
|
+
$ sk origin-request <customerPath> <pageUrl>
|
|
653
|
+
|
|
654
|
+
$ sk origin-request customers/decathlon.de https://www.decathlon.de/ -v mobile
|
|
655
|
+
|
|
656
|
+
$ sk origin-request customers/decathlon.de https://www.decathlon.de/ --grep "storeId" --body /tmp/origin.html
|
|
657
|
+
```
|
|
658
|
+
|
|
623
659
|
## `sk param-diff PAGEURL`
|
|
624
660
|
|
|
625
661
|
Shows difference in documents with query parameters on/off
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
export default class OriginRequest extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static args: {
|
|
5
|
+
pageUrl: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
customerPath: import("@oclif/core/interfaces").Arg<string, {
|
|
7
|
+
exists?: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
static flags: {
|
|
11
|
+
variation: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
host: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
body: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
grep: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
variations: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
configName: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
};
|
|
19
|
+
static examples: string[];
|
|
20
|
+
run(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
2
|
+
import { CLI_CONFIG_NAME, CLI_CUSTOMER_CONFIG, CLIParameters, CLIParametersChar, CLIParametersExample, } from "../models/cli-parameters.js";
|
|
3
|
+
import { CliConfig } from "../helpers/cli-config.js";
|
|
4
|
+
import { OriginRequestContext, OriginRequestServiceFactory, } from "../services/origin-request/index.js";
|
|
5
|
+
export default class OriginRequest extends Command {
|
|
6
|
+
static description = "Request a page from origin with the exact headers Speed Kit's server sends for a cache variation, using the app's deployed runtime config";
|
|
7
|
+
static args = {
|
|
8
|
+
...CLI_CUSTOMER_CONFIG,
|
|
9
|
+
[CLIParameters.PageUrl]: Args.string({
|
|
10
|
+
name: CLIParameters.PageUrl,
|
|
11
|
+
required: true,
|
|
12
|
+
description: "Page to request from origin",
|
|
13
|
+
example: CLIParametersExample.PageUrl,
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
static flags = {
|
|
17
|
+
...CLI_CONFIG_NAME,
|
|
18
|
+
[CLIParameters.Variation]: Flags.string({
|
|
19
|
+
char: CLIParametersChar.Variation,
|
|
20
|
+
description: "Cache variation to simulate, e.g. desktop-fmarkt-variant-e_1907090. Omitted means the default variation",
|
|
21
|
+
}),
|
|
22
|
+
host: Flags.string({
|
|
23
|
+
description: "Send this Host header, emulating an origin-map host override",
|
|
24
|
+
}),
|
|
25
|
+
body: Flags.string({
|
|
26
|
+
description: "Write the response body to this file",
|
|
27
|
+
}),
|
|
28
|
+
grep: Flags.string({
|
|
29
|
+
description: "Print body lines matching this regex, case-insensitive",
|
|
30
|
+
}),
|
|
31
|
+
json: Flags.boolean({
|
|
32
|
+
description: "Machine-readable output",
|
|
33
|
+
}),
|
|
34
|
+
variations: Flags.string({
|
|
35
|
+
description: "Simulate against a runtime config in a local file instead of the app's deployed one",
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
static examples = [
|
|
39
|
+
`$ sk origin-request <${CLIParameters.CustomerPath}> <${CLIParameters.PageUrl}>`,
|
|
40
|
+
`$ sk origin-request ${CLIParametersExample.CustomerPath} ${CLIParametersExample.PageUrl} -${CLIParametersChar.Variation} mobile`,
|
|
41
|
+
`$ sk origin-request ${CLIParametersExample.CustomerPath} ${CLIParametersExample.PageUrl} --grep "storeId" --body /tmp/origin.html`,
|
|
42
|
+
];
|
|
43
|
+
async run() {
|
|
44
|
+
const { args: { customerPath, pageUrl }, flags: { configName, variation, host, body, grep, json, variations }, } = await this.parse(OriginRequest);
|
|
45
|
+
const context = new OriginRequestContext(customerPath, configName, pageUrl, variation, host, body, grep, json, variations);
|
|
46
|
+
const service = await new OriginRequestServiceFactory(context, new CliConfig(this.config).load()).buildService();
|
|
47
|
+
await service.run();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -14,6 +14,18 @@ export declare class BundleService {
|
|
|
14
14
|
logLevel?: LogLevel;
|
|
15
15
|
platform?: "browser" | "node" | "neutral";
|
|
16
16
|
}): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Turns each alias target into an absolute path, resolved from the same directory the bundled
|
|
19
|
+
* code resolves its own imports from.
|
|
20
|
+
*
|
|
21
|
+
* esbuild resolves an alias target from its working directory, not from the entry point's
|
|
22
|
+
* resolveDir. `{ "node-fetch": "node-fetch-native" }` therefore only resolved while `sk` ran
|
|
23
|
+
* inside the customer checkout, and failed from anywhere else — even though the bundled code
|
|
24
|
+
* could import the very same module by name.
|
|
25
|
+
*
|
|
26
|
+
* A target that is not installed stays a bare name, so esbuild reports it as before.
|
|
27
|
+
*/
|
|
28
|
+
private resolveAliases;
|
|
17
29
|
private entryPointsPlugin;
|
|
18
30
|
baqendPlugin(basePaths: BundleBasePaths): {
|
|
19
31
|
name: string;
|
|
@@ -4,6 +4,7 @@ import { URL } from "node:url";
|
|
|
4
4
|
import { BundleResolveError } from "./error/bundle-resolve-error.js";
|
|
5
5
|
import { BundleFetchError } from "./error/bundle-fetch-error.js";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
7
8
|
const importCache = {};
|
|
8
9
|
export class BundleService {
|
|
9
10
|
moduleDiscoveryDir;
|
|
@@ -29,13 +30,39 @@ export class BundleService {
|
|
|
29
30
|
entryPoints: Object.keys(entryPoints),
|
|
30
31
|
platform: platform || "neutral",
|
|
31
32
|
logLevel,
|
|
32
|
-
alias,
|
|
33
|
+
alias: this.resolveAliases(alias),
|
|
33
34
|
});
|
|
34
35
|
return result.outputFiles
|
|
35
36
|
.map((outputFile) => outputFile.text)
|
|
36
37
|
.join("")
|
|
37
38
|
.trim();
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Turns each alias target into an absolute path, resolved from the same directory the bundled
|
|
42
|
+
* code resolves its own imports from.
|
|
43
|
+
*
|
|
44
|
+
* esbuild resolves an alias target from its working directory, not from the entry point's
|
|
45
|
+
* resolveDir. `{ "node-fetch": "node-fetch-native" }` therefore only resolved while `sk` ran
|
|
46
|
+
* inside the customer checkout, and failed from anywhere else — even though the bundled code
|
|
47
|
+
* could import the very same module by name.
|
|
48
|
+
*
|
|
49
|
+
* A target that is not installed stays a bare name, so esbuild reports it as before.
|
|
50
|
+
*/
|
|
51
|
+
resolveAliases(alias) {
|
|
52
|
+
if (Object.keys(alias).length === 0) {
|
|
53
|
+
return alias;
|
|
54
|
+
}
|
|
55
|
+
// The filename never has to exist; only its directory decides where the lookup starts.
|
|
56
|
+
const requireFrom = createRequire(resolve(this.moduleDiscoveryDir, "noop.js"));
|
|
57
|
+
return Object.fromEntries(Object.entries(alias).map(([from, to]) => {
|
|
58
|
+
try {
|
|
59
|
+
return [from, requireFrom.resolve(to)];
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [from, to];
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
39
66
|
entryPointsPlugin(entryPoints) {
|
|
40
67
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
41
68
|
const me = this;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { after, before, describe, it } from "mocha";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { BundleService } from "./bundle-service.js";
|
|
7
|
+
/**
|
|
8
|
+
* The customer checkout installs the modules a document handler imports, so resolution must follow
|
|
9
|
+
* the customer directory rather than the directory `sk` happens to run in. The fixture below is
|
|
10
|
+
* only reachable from the customer directory, which is what makes the assertion meaningful.
|
|
11
|
+
*/
|
|
12
|
+
describe("BundleService alias resolution", () => {
|
|
13
|
+
const MARKER = "fixture-fetch-implementation";
|
|
14
|
+
let customerDirectory;
|
|
15
|
+
before(() => {
|
|
16
|
+
customerDirectory = mkdtempSync(join(tmpdir(), "sk-bundle-"));
|
|
17
|
+
const packageDirectory = join(customerDirectory, "node_modules", "fetch-native-fixture");
|
|
18
|
+
mkdirSync(packageDirectory, { recursive: true });
|
|
19
|
+
writeFileSync(join(packageDirectory, "package.json"), JSON.stringify({
|
|
20
|
+
name: "fetch-native-fixture",
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
main: "index.js",
|
|
23
|
+
}));
|
|
24
|
+
writeFileSync(join(packageDirectory, "index.js"), `module.exports = "${MARKER}";`);
|
|
25
|
+
});
|
|
26
|
+
after(() => {
|
|
27
|
+
rmSync(customerDirectory, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
it("resolves an alias target from the customer directory, not from the working directory", async () => {
|
|
30
|
+
const bundle = await new BundleService(customerDirectory).bundle({
|
|
31
|
+
entryPoints: {
|
|
32
|
+
"handler.js": "module.exports = require('node-fetch');",
|
|
33
|
+
},
|
|
34
|
+
minify: false,
|
|
35
|
+
platform: "node",
|
|
36
|
+
logLevel: "silent",
|
|
37
|
+
alias: { "node-fetch": "fetch-native-fixture" },
|
|
38
|
+
});
|
|
39
|
+
expect(bundle).to.contain(MARKER);
|
|
40
|
+
});
|
|
41
|
+
it("leaves an uninstalled alias target to esbuild", async () => {
|
|
42
|
+
const bundling = new BundleService(customerDirectory).bundle({
|
|
43
|
+
entryPoints: {
|
|
44
|
+
"handler.js": "module.exports = require('node-fetch');",
|
|
45
|
+
},
|
|
46
|
+
minify: false,
|
|
47
|
+
platform: "node",
|
|
48
|
+
logLevel: "silent",
|
|
49
|
+
alias: { "node-fetch": "not-installed-anywhere" },
|
|
50
|
+
});
|
|
51
|
+
const error = await bundling.then(() => undefined, (reason) => reason);
|
|
52
|
+
expect(String(error)).to.contain("not-installed-anywhere");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -5,58 +5,27 @@ export declare class Crawler {
|
|
|
5
5
|
private cli;
|
|
6
6
|
private agentConfig;
|
|
7
7
|
private speedKitServerConfig?;
|
|
8
|
+
/** Warnings already shown, so a per-fetch condition is reported once per session. */
|
|
9
|
+
private readonly warned;
|
|
8
10
|
constructor(cli: CliService, agentConfig: Agent, speedKitServerConfig?: SpeedKitServerConfig);
|
|
11
|
+
private warnOnce;
|
|
9
12
|
fetchRemote(originUrl: string, variation: string): Promise<{
|
|
10
13
|
response: Response;
|
|
11
14
|
url: string;
|
|
12
15
|
} | undefined>;
|
|
13
|
-
private setDefaultUserAgent;
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* user-agent / etc. headers configured per variation must reach the origin
|
|
18
|
-
* so that the response matches what the variation is supposed to represent
|
|
19
|
-
* (e.g. a logged-in user, a specific store, a mobile device).
|
|
17
|
+
* Builds the origin request exactly as the Baqend asset server builds it, so a locally cached
|
|
18
|
+
* document matches what production would cache for the same variation.
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `variationParameter` first, then fall back to its Java-style
|
|
26
|
-
* `regex` (e.g. `(?i)desktop-fmarkt-variant-(?<storeId>.*)`). The
|
|
27
|
-
* first variation that matches by either means is applied — exact
|
|
28
|
-
* matches do NOT take priority over regex matches in earlier
|
|
29
|
-
* entries. When matched via regex, named capture groups are
|
|
30
|
-
* substituted into header values via `${name}` placeholders, so a
|
|
31
|
-
* cookie like `fmarktcookie=${storeId}` becomes
|
|
32
|
-
* `fmarktcookie=2879130`.
|
|
33
|
-
* 3. Origin-level basic auth fallback (only if no variation matched).
|
|
20
|
+
* The resolution itself lives in `services/origin-request` and is shared with
|
|
21
|
+
* `sk origin-request`, so there is one owner for it and one place the server's behaviour is
|
|
22
|
+
* pinned by specs. That covers the base headers, the wildcard-then-variation order, the regex
|
|
23
|
+
* templates with their `${group}` substitution, and the query params.
|
|
34
24
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
25
|
+
* Two things stay local: origin basic auth, which no variation carries, and tolerance for a
|
|
26
|
+
* variation the config does not define — the server rejects that request, but failing a whole
|
|
27
|
+
* onboarding session over it would be unhelpful, so it falls back to the base headers and warns.
|
|
37
28
|
*/
|
|
38
29
|
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;
|
|
45
|
-
/**
|
|
46
|
-
* Replaces `${name}` placeholders in each header value with the matching
|
|
47
|
-
* named capture group from a regex match. Used by `prepareOriginRequest`
|
|
48
|
-
* to materialize parameterized variation headers — e.g. given:
|
|
49
|
-
*
|
|
50
|
-
* headers = { cookie: "fmarktcookie=${storeId}" }
|
|
51
|
-
* groups = { storeId: "2879130" }
|
|
52
|
-
*
|
|
53
|
-
* the result is `{ cookie: "fmarktcookie=2879130" }`.
|
|
54
|
-
*
|
|
55
|
-
* Placeholders that don't match a captured group are left intact rather
|
|
56
|
-
* than replaced with `undefined`, so a misconfigured variation still
|
|
57
|
-
* produces a recognisable header value instead of a silent corruption.
|
|
58
|
-
* Non-string header values are passed through unchanged.
|
|
59
|
-
*/
|
|
60
|
-
private substituteNamedGroups;
|
|
61
30
|
private prepareAuthRequest;
|
|
62
31
|
}
|
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
import { USER_AGENT } from "../onboarding-model.js";
|
|
2
1
|
import { safe } from "../../../helpers/safe.js";
|
|
3
|
-
import {
|
|
2
|
+
import { resolveRequest, UndefinedVariationError, } from "../../origin-request/index.js";
|
|
4
3
|
import { randomUUID } from "node:crypto";
|
|
5
4
|
export class Crawler {
|
|
6
5
|
cli;
|
|
7
6
|
agentConfig;
|
|
8
7
|
speedKitServerConfig;
|
|
8
|
+
/** Warnings already shown, so a per-fetch condition is reported once per session. */
|
|
9
|
+
warned = new Set();
|
|
9
10
|
constructor(cli, agentConfig, speedKitServerConfig) {
|
|
10
11
|
this.cli = cli;
|
|
11
12
|
this.agentConfig = agentConfig;
|
|
12
13
|
this.speedKitServerConfig = speedKitServerConfig;
|
|
13
14
|
}
|
|
15
|
+
warnOnce(key, message) {
|
|
16
|
+
if (this.warned.has(key))
|
|
17
|
+
return;
|
|
18
|
+
this.warned.add(key);
|
|
19
|
+
this.cli.writeWarning(message);
|
|
20
|
+
}
|
|
14
21
|
async fetchRemote(originUrl, variation) {
|
|
15
|
-
const userAgent = this.setDefaultUserAgent(variation);
|
|
16
|
-
const headers = {
|
|
17
|
-
"User-Agent": userAgent,
|
|
18
|
-
};
|
|
19
22
|
// origin URL with the variation's query params applied — returned so callers compare it
|
|
20
23
|
// (not the bare originUrl) against response.url for redirect detection
|
|
21
|
-
const { url, init } = this.prepareOriginRequest(originUrl, variation
|
|
22
|
-
headers,
|
|
23
|
-
});
|
|
24
|
+
const { url, init } = this.prepareOriginRequest(originUrl, variation);
|
|
24
25
|
const requestInit = {
|
|
25
26
|
...init,
|
|
26
27
|
agent: this.agentConfig,
|
|
@@ -36,138 +37,46 @@ export class Crawler {
|
|
|
36
37
|
this.cli.writeError(fetchResponse.error);
|
|
37
38
|
this.cli.writeError(JSON.stringify(fetchResponse.errorObj));
|
|
38
39
|
}
|
|
39
|
-
setDefaultUserAgent(variation) {
|
|
40
|
-
if (/^mobile$/is.test(variation)) {
|
|
41
|
-
return USER_AGENT.MOBILE;
|
|
42
|
-
}
|
|
43
|
-
if (/^tablet$/is.test(variation)) {
|
|
44
|
-
return USER_AGENT.TABLET;
|
|
45
|
-
}
|
|
46
|
-
return USER_AGENT.DESKTOP;
|
|
47
|
-
}
|
|
48
40
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* user-agent / etc. headers configured per variation must reach the origin
|
|
52
|
-
* so that the response matches what the variation is supposed to represent
|
|
53
|
-
* (e.g. a logged-in user, a specific store, a mobile device).
|
|
41
|
+
* Builds the origin request exactly as the Baqend asset server builds it, so a locally cached
|
|
42
|
+
* document matches what production would cache for the same variation.
|
|
54
43
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* `variationParameter` first, then fall back to its Java-style
|
|
60
|
-
* `regex` (e.g. `(?i)desktop-fmarkt-variant-(?<storeId>.*)`). The
|
|
61
|
-
* first variation that matches by either means is applied — exact
|
|
62
|
-
* matches do NOT take priority over regex matches in earlier
|
|
63
|
-
* entries. When matched via regex, named capture groups are
|
|
64
|
-
* substituted into header values via `${name}` placeholders, so a
|
|
65
|
-
* cookie like `fmarktcookie=${storeId}` becomes
|
|
66
|
-
* `fmarktcookie=2879130`.
|
|
67
|
-
* 3. Origin-level basic auth fallback (only if no variation matched).
|
|
44
|
+
* The resolution itself lives in `services/origin-request` and is shared with
|
|
45
|
+
* `sk origin-request`, so there is one owner for it and one place the server's behaviour is
|
|
46
|
+
* pinned by specs. That covers the base headers, the wildcard-then-variation order, the regex
|
|
47
|
+
* templates with their `${group}` substitution, and the query params.
|
|
68
48
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
prepareOriginRequest(originUrl, variationParameter, init) {
|
|
73
|
-
const url = new URL(originUrl);
|
|
74
|
-
if (!this.speedKitServerConfig) {
|
|
75
|
-
return { url: url.toString(), init };
|
|
76
|
-
}
|
|
77
|
-
const variations = this.speedKitServerConfig.speedKit.variations;
|
|
78
|
-
// Wildcard variation applies to every request, regardless of which
|
|
79
|
-
// specific variation (if any) is requested.
|
|
80
|
-
if (variations?.["*"]) {
|
|
81
|
-
this.applyVariation(url, init, variations["*"], undefined);
|
|
82
|
-
}
|
|
83
|
-
if (!variationParameter) {
|
|
84
|
-
return { url: url.toString(), init };
|
|
85
|
-
}
|
|
86
|
-
// First-match-wins over the variations in declaration order. Each entry
|
|
87
|
-
// can match by exact key or by its Java-style `regex` field; whichever
|
|
88
|
-
// entry matches first applies its headers + query params and short-circuits.
|
|
89
|
-
for (const variationKey in variations) {
|
|
90
|
-
if (variationKey === "*") {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const variationConfig = variations[variationKey];
|
|
94
|
-
if (variationParameter === variationKey) {
|
|
95
|
-
this.applyVariation(url, init, variationConfig, undefined);
|
|
96
|
-
return { url: url.toString(), init };
|
|
97
|
-
}
|
|
98
|
-
if (!variationConfig.regex) {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const regex = javaRegexToJs(variationConfig.regex);
|
|
102
|
-
if (!regex) {
|
|
103
|
-
// Unsupported Java regex construct — skip rather than crash so later
|
|
104
|
-
// variations still get a chance to match.
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
const match = variationParameter.match(regex);
|
|
108
|
-
if (!match) {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
this.applyVariation(url, init, variationConfig, match.groups);
|
|
112
|
-
return { url: url.toString(), init };
|
|
113
|
-
}
|
|
114
|
-
// No variation matched: fall back to origin basic auth if configured.
|
|
115
|
-
const authentication = this.prepareAuthRequest(originUrl);
|
|
116
|
-
if (authentication) {
|
|
117
|
-
init.headers["Authorization"] = authentication;
|
|
118
|
-
}
|
|
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`.
|
|
49
|
+
* Two things stay local: origin basic auth, which no variation carries, and tolerance for a
|
|
50
|
+
* variation the config does not define — the server rejects that request, but failing a whole
|
|
51
|
+
* onboarding session over it would be unhelpful, so it falls back to the base headers and warns.
|
|
125
52
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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);
|
|
53
|
+
prepareOriginRequest(originUrl, variationParameter) {
|
|
54
|
+
const variations = this.speedKitServerConfig?.speedKit?.variations ?? {};
|
|
55
|
+
// An empty map means the runtime config never loaded, so no fetch can carry variation headers.
|
|
56
|
+
// Say that once, rather than blaming each variation for being undefined.
|
|
57
|
+
if (!Object.keys(variations).length) {
|
|
58
|
+
this.warnOnce("no-runtime-config", "no variations available from the runtime config, so origin fetches carry the base headers only — the cached document may differ from production. Are you logged in?");
|
|
139
59
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* named capture group from a regex match. Used by `prepareOriginRequest`
|
|
144
|
-
* to materialize parameterized variation headers — e.g. given:
|
|
145
|
-
*
|
|
146
|
-
* headers = { cookie: "fmarktcookie=${storeId}" }
|
|
147
|
-
* groups = { storeId: "2879130" }
|
|
148
|
-
*
|
|
149
|
-
* the result is `{ cookie: "fmarktcookie=2879130" }`.
|
|
150
|
-
*
|
|
151
|
-
* Placeholders that don't match a captured group are left intact rather
|
|
152
|
-
* than replaced with `undefined`, so a misconfigured variation still
|
|
153
|
-
* produces a recognisable header value instead of a silent corruption.
|
|
154
|
-
* Non-string header values are passed through unchanged.
|
|
155
|
-
*/
|
|
156
|
-
substituteNamedGroups(headers, groups) {
|
|
157
|
-
if (!headers) {
|
|
158
|
-
return {};
|
|
60
|
+
let resolved;
|
|
61
|
+
try {
|
|
62
|
+
resolved = resolveRequest(variations, originUrl, variationParameter);
|
|
159
63
|
}
|
|
160
|
-
|
|
161
|
-
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (!(error instanceof UndefinedVariationError))
|
|
66
|
+
throw error;
|
|
67
|
+
this.warnOnce(`undefined-variation:${variationParameter}`, `${error.message} Falling back to the base headers.`);
|
|
68
|
+
resolved = resolveRequest({}, originUrl);
|
|
162
69
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
70
|
+
const init = { headers: { ...resolved.headers } };
|
|
71
|
+
// Only a request that matched no specific variation falls back to origin basic auth. The
|
|
72
|
+
// wildcard applies to every request, so it does not count as a match.
|
|
73
|
+
const matchedSpecificVariation = resolved.appliedKeys.some((key) => key !== "*" && !key.startsWith("device-default:"));
|
|
74
|
+
if (!matchedSpecificVariation) {
|
|
75
|
+
const authentication = this.prepareAuthRequest(originUrl);
|
|
76
|
+
if (authentication)
|
|
77
|
+
init.headers["Authorization"] = authentication;
|
|
169
78
|
}
|
|
170
|
-
return
|
|
79
|
+
return { url: resolved.url, init };
|
|
171
80
|
}
|
|
172
81
|
prepareAuthRequest(originUrl) {
|
|
173
82
|
if (!this.speedKitServerConfig ||
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of the server's variation resolution, so a request can be reproduced locally with the exact
|
|
3
|
+
* headers Speed Kit would send.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors two pieces of server behaviour:
|
|
6
|
+
* - `AssetVariations.getVariations()` — which variation configs apply and in which order
|
|
7
|
+
* (orestes: `orestes-common/.../typesystem/AssetVariations.kt`)
|
|
8
|
+
* - `AssetClient.newRequest()` / `createRequest()` — base headers, last-writer-wins merging,
|
|
9
|
+
* urlPrefix and queryParams (orestes: `orestes-server/.../asset/AssetClient.kt`)
|
|
10
|
+
*
|
|
11
|
+
* Keep this file free of I/O so the resolution stays unit-testable.
|
|
12
|
+
*/
|
|
13
|
+
/** A single variation entry of the runtime config's `speedKit.variations`. */
|
|
14
|
+
export interface AssetVariation {
|
|
15
|
+
headers?: Record<string, string | null>;
|
|
16
|
+
urlPrefix?: string;
|
|
17
|
+
encodePrefixedUrl?: boolean;
|
|
18
|
+
queryParams?: string[];
|
|
19
|
+
regex?: string | null;
|
|
20
|
+
/** Which config key produced this entry. Set during resolution, for reporting. */
|
|
21
|
+
matchedKey?: string;
|
|
22
|
+
}
|
|
23
|
+
export type AssetVariationMap = Record<string, AssetVariation>;
|
|
24
|
+
export interface ResolvedRequest {
|
|
25
|
+
/** Keys of the applied configs, in the order the server applies them. */
|
|
26
|
+
appliedKeys: string[];
|
|
27
|
+
/** `template` when a regex entry matched, otherwise `named`. */
|
|
28
|
+
resolutionPath: "template" | "named";
|
|
29
|
+
url: string;
|
|
30
|
+
headers: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
export declare const DEFAULT_TYPE = "default";
|
|
33
|
+
/** User agents the server falls back to. Keep in sync with AssetVariations.kt. */
|
|
34
|
+
export declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15 (compatible; SpeedKit/1.0)";
|
|
35
|
+
/** Thrown where the server would throw UndefinedVariationException. */
|
|
36
|
+
export declare class UndefinedVariationError extends Error {
|
|
37
|
+
constructor(variationType: string);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolves which variation configs apply, in server order. The template path returns
|
|
41
|
+
* `[wildcard, template]` and skips the device defaults; otherwise `[default, wildcard, named]`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getVariations(custom: AssetVariationMap, variationType?: string): AssetVariation[];
|
|
44
|
+
/** Resolves a variation into the concrete request the server would send. */
|
|
45
|
+
export declare function resolveRequest(custom: AssetVariationMap, url: string, variationType?: string, hostOverride?: string): ResolvedRequest;
|
|
46
|
+
/** Renders the resolved request as a curl command, for pasting into a bug report. */
|
|
47
|
+
export declare function toCurl(url: string, headers: Record<string, string>): string;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of the server's variation resolution, so a request can be reproduced locally with the exact
|
|
3
|
+
* headers Speed Kit would send.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors two pieces of server behaviour:
|
|
6
|
+
* - `AssetVariations.getVariations()` — which variation configs apply and in which order
|
|
7
|
+
* (orestes: `orestes-common/.../typesystem/AssetVariations.kt`)
|
|
8
|
+
* - `AssetClient.newRequest()` / `createRequest()` — base headers, last-writer-wins merging,
|
|
9
|
+
* urlPrefix and queryParams (orestes: `orestes-server/.../asset/AssetClient.kt`)
|
|
10
|
+
*
|
|
11
|
+
* Keep this file free of I/O so the resolution stays unit-testable.
|
|
12
|
+
*/
|
|
13
|
+
import { javaRegexToJs } from "../../helpers/java-regex.js";
|
|
14
|
+
export const DEFAULT_TYPE = "default";
|
|
15
|
+
const WILD_CARD = "*";
|
|
16
|
+
/** User agents the server falls back to. Keep in sync with AssetVariations.kt. */
|
|
17
|
+
export const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15 (compatible; SpeedKit/1.0)";
|
|
18
|
+
const MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1 (compatible; SpeedKit/1.0)";
|
|
19
|
+
const TABLET_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 (compatible; SpeedKit/1.0)";
|
|
20
|
+
const DEVICE_DEFAULTS = {
|
|
21
|
+
[DEFAULT_TYPE]: { headers: { "user-agent": DEFAULT_USER_AGENT } },
|
|
22
|
+
mobile: { headers: { "user-agent": MOBILE_USER_AGENT } },
|
|
23
|
+
tablet: { headers: { "user-agent": TABLET_USER_AGENT } },
|
|
24
|
+
};
|
|
25
|
+
/** Group names a template captures, e.g. `storeId` from `(?<storeId>.*)`. */
|
|
26
|
+
const CAPTURING_TEMPLATES = /<(\w+)>/g;
|
|
27
|
+
/** Thrown where the server would throw UndefinedVariationException. */
|
|
28
|
+
export class UndefinedVariationError extends Error {
|
|
29
|
+
constructor(variationType) {
|
|
30
|
+
super(`Variation ${variationType} not found — the server would reject this request with UndefinedVariationException.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Matches a template pattern against a variation. The server uses Java's `matches()`, which requires
|
|
35
|
+
* the pattern to cover the whole input, so a partial match is not a match here either.
|
|
36
|
+
*/
|
|
37
|
+
function matchWholeVariation(pattern, variationType) {
|
|
38
|
+
const regex = javaRegexToJs(pattern);
|
|
39
|
+
// An unsupported Java construct leaves the entry unmatched rather than crashing the caller.
|
|
40
|
+
if (!regex)
|
|
41
|
+
return undefined;
|
|
42
|
+
const match = regex.exec(variationType);
|
|
43
|
+
return match?.[0] === variationType ? match : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Substitutes `${group}` in every header, urlPrefix and queryParam of a template entry. */
|
|
46
|
+
function applyTemplate(entry, variationType) {
|
|
47
|
+
const pattern = entry.regex;
|
|
48
|
+
if (!pattern)
|
|
49
|
+
return undefined;
|
|
50
|
+
const match = matchWholeVariation(pattern, variationType);
|
|
51
|
+
if (!match)
|
|
52
|
+
return undefined;
|
|
53
|
+
const replacements = new Map();
|
|
54
|
+
for (const [, groupName] of pattern.matchAll(CAPTURING_TEMPLATES)) {
|
|
55
|
+
const value = match.groups?.[groupName];
|
|
56
|
+
if (value !== undefined)
|
|
57
|
+
replacements.set(groupName, value);
|
|
58
|
+
}
|
|
59
|
+
const substitute = (text) => {
|
|
60
|
+
let out = text;
|
|
61
|
+
for (const [name, value] of replacements)
|
|
62
|
+
out = out.split(`\${${name}}`).join(value);
|
|
63
|
+
return out;
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
matchedKey: entry.matchedKey,
|
|
67
|
+
urlPrefix: entry.urlPrefix ? substitute(entry.urlPrefix) : entry.urlPrefix,
|
|
68
|
+
encodePrefixedUrl: entry.encodePrefixedUrl,
|
|
69
|
+
queryParams: (entry.queryParams ?? []).map(substitute),
|
|
70
|
+
headers: Object.fromEntries(Object.entries(entry.headers ?? {}).map(([key, value]) => [
|
|
71
|
+
key,
|
|
72
|
+
value === null ? null : substitute(String(value)),
|
|
73
|
+
])),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolves which variation configs apply, in server order. The template path returns
|
|
78
|
+
* `[wildcard, template]` and skips the device defaults; otherwise `[default, wildcard, named]`.
|
|
79
|
+
*/
|
|
80
|
+
export function getVariations(custom, variationType) {
|
|
81
|
+
const entries = Object.entries(custom ?? {}).map(([key, entry]) => ({
|
|
82
|
+
...entry,
|
|
83
|
+
matchedKey: key,
|
|
84
|
+
}));
|
|
85
|
+
const wildcard = entries.find((entry) => entry.matchedKey === WILD_CARD);
|
|
86
|
+
const type = variationType ?? DEFAULT_TYPE;
|
|
87
|
+
if (variationType &&
|
|
88
|
+
!custom[variationType] &&
|
|
89
|
+
!custom[variationType.toUpperCase()]) {
|
|
90
|
+
const template = entries
|
|
91
|
+
.filter((entry) => entry.regex)
|
|
92
|
+
.map((entry) => applyTemplate(entry, variationType))
|
|
93
|
+
.find(Boolean);
|
|
94
|
+
if (template)
|
|
95
|
+
return [wildcard, template].filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
const deviceDefault = DEVICE_DEFAULTS[type] ?? DEVICE_DEFAULTS[type.toLowerCase()];
|
|
98
|
+
const named = custom[type] ?? custom[type.toUpperCase()];
|
|
99
|
+
if (!deviceDefault && !named)
|
|
100
|
+
throw new UndefinedVariationError(type);
|
|
101
|
+
return [
|
|
102
|
+
deviceDefault && { ...deviceDefault, matchedKey: `device-default:${type}` },
|
|
103
|
+
wildcard,
|
|
104
|
+
named && { ...named, matchedKey: type },
|
|
105
|
+
].filter(Boolean);
|
|
106
|
+
}
|
|
107
|
+
/** Applies each config's urlPrefix and queryParams in order, as AssetClient.createRequest does. */
|
|
108
|
+
function buildUrl(url, chain) {
|
|
109
|
+
let result = url;
|
|
110
|
+
for (const config of chain) {
|
|
111
|
+
if (config.urlPrefix) {
|
|
112
|
+
result =
|
|
113
|
+
config.urlPrefix +
|
|
114
|
+
(config.encodePrefixedUrl ? encodeURIComponent(result) : result);
|
|
115
|
+
}
|
|
116
|
+
if (config.queryParams?.length) {
|
|
117
|
+
result +=
|
|
118
|
+
(result.includes("?") ? "&" : "?") + config.queryParams.join("&");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Base headers from AssetClient.newRequest, then each config's headers with the last writer
|
|
125
|
+
* winning. A `null` value removes the header, which is how a config drops Accept-Language.
|
|
126
|
+
*/
|
|
127
|
+
function buildHeaders(chain, hostOverride) {
|
|
128
|
+
const headers = new Map([
|
|
129
|
+
[
|
|
130
|
+
"accept",
|
|
131
|
+
"text/html, application/xhtml+xml, application/xml;q=0.9, image/avif, image/webp, */*;q=0.8",
|
|
132
|
+
],
|
|
133
|
+
["accept-encoding", "gzip"],
|
|
134
|
+
["accept-language", "*"],
|
|
135
|
+
["user-agent", DEFAULT_USER_AGENT],
|
|
136
|
+
]);
|
|
137
|
+
for (const config of chain) {
|
|
138
|
+
for (const [key, value] of Object.entries(config.headers ?? {})) {
|
|
139
|
+
if (value === null)
|
|
140
|
+
headers.delete(key.toLowerCase());
|
|
141
|
+
else
|
|
142
|
+
headers.set(key.toLowerCase(), String(value));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (hostOverride)
|
|
146
|
+
headers.set("host", hostOverride);
|
|
147
|
+
return Object.fromEntries(headers);
|
|
148
|
+
}
|
|
149
|
+
/** Resolves a variation into the concrete request the server would send. */
|
|
150
|
+
export function resolveRequest(custom, url, variationType, hostOverride) {
|
|
151
|
+
const chain = getVariations(custom, variationType);
|
|
152
|
+
const isTemplate = chain.some((config) => config.matchedKey !== WILD_CARD &&
|
|
153
|
+
!DEVICE_DEFAULTS[config.matchedKey ?? ""]) &&
|
|
154
|
+
!chain.some((config) => config.matchedKey?.startsWith("device-default:"));
|
|
155
|
+
return {
|
|
156
|
+
appliedKeys: chain.map((config) => config.matchedKey ?? "?"),
|
|
157
|
+
resolutionPath: isTemplate ? "template" : "named",
|
|
158
|
+
url: buildUrl(url, chain),
|
|
159
|
+
headers: buildHeaders(chain, hostOverride),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Renders the resolved request as a curl command, for pasting into a bug report. */
|
|
163
|
+
export function toCurl(url, headers) {
|
|
164
|
+
const parts = ["curl -sSD - -o /dev/null --compressed"];
|
|
165
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
166
|
+
parts.push(` -H ${JSON.stringify(`${key}: ${value}`)}`);
|
|
167
|
+
}
|
|
168
|
+
parts.push(` ${JSON.stringify(url)}`);
|
|
169
|
+
return parts.join(" \\\n");
|
|
170
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { getVariations, resolveRequest, UndefinedVariationError, } from "./asset-variations.js";
|
|
4
|
+
/**
|
|
5
|
+
* These cases mirror the server's own expectations, so a change here means the simulation and the
|
|
6
|
+
* server have drifted apart. The reference tests live in orestes:
|
|
7
|
+
* `orestes-common/src/test/kotlin/info/orestes/common/typesystem/WebBotAuthSignatureMismatchTest.kt`.
|
|
8
|
+
*/
|
|
9
|
+
describe("asset variations", () => {
|
|
10
|
+
const storeTemplate = {
|
|
11
|
+
"*": { headers: { cookie: "fmarktcookie=e_2879130" } },
|
|
12
|
+
"desktop-fmarkt-variant": {
|
|
13
|
+
headers: { cookie: "fmarktcookie=${storeId}" },
|
|
14
|
+
regex: "(?i)desktop-fmarkt-variant-(?<storeId>.*)",
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
it("substitutes a captured group into the headers", () => {
|
|
18
|
+
const { headers } = resolveRequest(storeTemplate, "https://www.example.com/plp", "desktop-fmarkt-variant-e_1907090");
|
|
19
|
+
expect(headers.cookie).to.equal("fmarktcookie=e_1907090");
|
|
20
|
+
});
|
|
21
|
+
it("lets the template override the wildcard, because the server applies it last", () => {
|
|
22
|
+
const { appliedKeys, resolutionPath } = resolveRequest(storeTemplate, "https://www.example.com/plp", "desktop-fmarkt-variant-e_1907090");
|
|
23
|
+
expect(resolutionPath).to.equal("template");
|
|
24
|
+
expect(appliedKeys).to.deep.equal(["*", "desktop-fmarkt-variant"]);
|
|
25
|
+
});
|
|
26
|
+
it("matches a template only against the whole variation, and rejects what matches nothing", () => {
|
|
27
|
+
// The desktop template cannot match a mobile variation, and the server does not fall back to
|
|
28
|
+
// the "mobile" device default by substring — that is the pre-renderer's rule, not the server's.
|
|
29
|
+
expect(() => getVariations(storeTemplate, "mobile-fmarkt-variant-e_1907090")).to.throw(UndefinedVariationError);
|
|
30
|
+
});
|
|
31
|
+
it("resolves a mobile variation through its own template", () => {
|
|
32
|
+
const custom = {
|
|
33
|
+
...storeTemplate,
|
|
34
|
+
"mobile-fmarkt-variant": {
|
|
35
|
+
headers: { cookie: "fmarktcookie=${storeId}" },
|
|
36
|
+
regex: "(?i)mobile-fmarkt-variant-(?<storeId>.*)",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const { headers, appliedKeys } = resolveRequest(custom, "https://www.example.com/", "mobile-fmarkt-variant-e_1");
|
|
40
|
+
expect(headers.cookie).to.equal("fmarktcookie=e_1");
|
|
41
|
+
expect(appliedKeys).to.deep.equal(["*", "mobile-fmarkt-variant"]);
|
|
42
|
+
});
|
|
43
|
+
it("prefers an exact key over a template", () => {
|
|
44
|
+
const custom = {
|
|
45
|
+
...storeTemplate,
|
|
46
|
+
"desktop-fmarkt-variant-e_1": { headers: { cookie: "exact" } },
|
|
47
|
+
};
|
|
48
|
+
const { headers } = resolveRequest(custom, "https://www.example.com/", "desktop-fmarkt-variant-e_1");
|
|
49
|
+
expect(headers.cookie).to.equal("exact");
|
|
50
|
+
});
|
|
51
|
+
it("keeps the base headers a request always carries", () => {
|
|
52
|
+
const { headers } = resolveRequest({}, "https://www.example.com/");
|
|
53
|
+
expect(headers["accept-encoding"]).to.equal("gzip");
|
|
54
|
+
expect(headers["accept-language"]).to.equal("*");
|
|
55
|
+
expect(headers["user-agent"]).to.contain("SpeedKit/1.0");
|
|
56
|
+
});
|
|
57
|
+
it("drops a header a variation sets to null", () => {
|
|
58
|
+
const { headers } = resolveRequest({ "*": { headers: { "accept-language": null } } }, "https://www.example.com/");
|
|
59
|
+
expect(headers).to.not.have.property("accept-language");
|
|
60
|
+
});
|
|
61
|
+
it("applies urlPrefix and queryParams like the server", () => {
|
|
62
|
+
const { url } = resolveRequest({
|
|
63
|
+
"*": {
|
|
64
|
+
urlPrefix: "https://proxy.example.com/?target=",
|
|
65
|
+
encodePrefixedUrl: true,
|
|
66
|
+
queryParams: ["bq=1"],
|
|
67
|
+
},
|
|
68
|
+
}, "https://www.example.com/plp?page=2");
|
|
69
|
+
expect(url).to.equal("https://proxy.example.com/?target=https%3A%2F%2Fwww.example.com%2Fplp%3Fpage%3D2&bq=1");
|
|
70
|
+
});
|
|
71
|
+
it("reports an unknown variation instead of guessing", () => {
|
|
72
|
+
expect(() => getVariations({}, "b2b-unknown")).to.throw(UndefinedVariationError);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Inputs for `sk origin-request`. No logic. */
|
|
2
|
+
export declare class OriginRequestContext {
|
|
3
|
+
readonly customerPath: string;
|
|
4
|
+
readonly configName: string;
|
|
5
|
+
readonly pageUrl: string;
|
|
6
|
+
readonly variation?: string;
|
|
7
|
+
readonly host?: string;
|
|
8
|
+
readonly bodyFile?: string;
|
|
9
|
+
readonly grep?: string;
|
|
10
|
+
readonly json?: boolean;
|
|
11
|
+
readonly variationsFile?: string;
|
|
12
|
+
constructor(customerPath: string, configName: string, pageUrl: string, variation?: string, host?: string, bodyFile?: string, grep?: string, json?: boolean, variationsFile?: string);
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Inputs for `sk origin-request`. No logic. */
|
|
2
|
+
export class OriginRequestContext {
|
|
3
|
+
customerPath;
|
|
4
|
+
configName;
|
|
5
|
+
pageUrl;
|
|
6
|
+
variation;
|
|
7
|
+
host;
|
|
8
|
+
bodyFile;
|
|
9
|
+
grep;
|
|
10
|
+
json;
|
|
11
|
+
variationsFile;
|
|
12
|
+
constructor(customerPath, configName, pageUrl, variation, host, bodyFile, grep, json, variationsFile) {
|
|
13
|
+
this.customerPath = customerPath;
|
|
14
|
+
this.configName = configName;
|
|
15
|
+
this.pageUrl = pageUrl;
|
|
16
|
+
this.variation = variation;
|
|
17
|
+
this.host = host;
|
|
18
|
+
this.bodyFile = bodyFile;
|
|
19
|
+
this.grep = grep;
|
|
20
|
+
this.json = json;
|
|
21
|
+
this.variationsFile = variationsFile;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { UserCliConfig } from "../../helpers/cli-config.js";
|
|
2
|
+
import { OriginRequestContext } from "./origin-request-context.js";
|
|
3
|
+
import { OriginRequestService } from "./origin-request-service.js";
|
|
4
|
+
export declare class OriginRequestServiceFactory {
|
|
5
|
+
private readonly context;
|
|
6
|
+
private readonly userConfig;
|
|
7
|
+
private service?;
|
|
8
|
+
constructor(context: OriginRequestContext, userConfig: UserCliConfig);
|
|
9
|
+
buildService(): Promise<OriginRequestService>;
|
|
10
|
+
private build;
|
|
11
|
+
private getIntegrationFiles;
|
|
12
|
+
private getConfigApi;
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CliServiceFactory } from "../cli/index.js";
|
|
2
|
+
import { ConfigApiContext, ConfigApiServiceFactory, } from "../config-api/index.js";
|
|
3
|
+
import { IntegrationApiContext, IntegrationApiFactory, } from "../integration-api/index.js";
|
|
4
|
+
import { OriginRequestService } from "./origin-request-service.js";
|
|
5
|
+
export class OriginRequestServiceFactory {
|
|
6
|
+
context;
|
|
7
|
+
userConfig;
|
|
8
|
+
service = undefined;
|
|
9
|
+
constructor(context, userConfig) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.userConfig = userConfig;
|
|
12
|
+
}
|
|
13
|
+
async buildService() {
|
|
14
|
+
if (!(this.service instanceof OriginRequestService)) {
|
|
15
|
+
await this.build();
|
|
16
|
+
}
|
|
17
|
+
return this.service;
|
|
18
|
+
}
|
|
19
|
+
async build() {
|
|
20
|
+
const files = await this.getIntegrationFiles();
|
|
21
|
+
const customer = files.getCustomerConfig().config;
|
|
22
|
+
this.service = new OriginRequestService(this.context, new CliServiceFactory().getService(), this.getConfigApi(customer.app));
|
|
23
|
+
}
|
|
24
|
+
async getIntegrationFiles() {
|
|
25
|
+
const integrationApiContext = new IntegrationApiContext(this.context.customerPath, this.context.configName, []);
|
|
26
|
+
const integrationApi = new IntegrationApiFactory(integrationApiContext, this.userConfig).buildService();
|
|
27
|
+
return await integrationApi.run();
|
|
28
|
+
}
|
|
29
|
+
getConfigApi(app) {
|
|
30
|
+
return new ConfigApiServiceFactory(new ConfigApiContext(app)).getService();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CliService } from "../cli/index.js";
|
|
2
|
+
import { ConfigApiService } from "../config-api/index.js";
|
|
3
|
+
import { OriginRequestContext } from "./origin-request-context.js";
|
|
4
|
+
/**
|
|
5
|
+
* Fetches a page from origin with exactly the headers Speed Kit's server would send for a cache
|
|
6
|
+
* variation, then reports what the origin answered.
|
|
7
|
+
*
|
|
8
|
+
* Use it to check whether the origin resolves a variation's identity — a store, a locale, a segment
|
|
9
|
+
* — without waiting for a render or reading backend logs. The variations come from the app's
|
|
10
|
+
* deployed runtime config, so the simulation follows production rather than a local file.
|
|
11
|
+
*
|
|
12
|
+
* Known deviations from the server, all deliberate:
|
|
13
|
+
* - The origin map (origin rewriting plus its extra headers) lives outside the runtime config;
|
|
14
|
+
* use `--host` for the Host part.
|
|
15
|
+
* - Revalidation headers (If-None-Match, If-Modified-Since) apply only when refreshing a cached
|
|
16
|
+
* entry, so they are out of scope.
|
|
17
|
+
* - Header order and the HTTP version come from undici, not Jetty.
|
|
18
|
+
*/
|
|
19
|
+
export declare class OriginRequestService {
|
|
20
|
+
private readonly context;
|
|
21
|
+
private readonly cli;
|
|
22
|
+
private readonly configApi;
|
|
23
|
+
constructor(context: OriginRequestContext, cli: CliService, configApi: ConfigApiService);
|
|
24
|
+
run(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Reads `speedKit.variations` from the app's deployed runtime config, or from a local file when
|
|
27
|
+
* `--variations` is given. Returns undefined when neither source is usable.
|
|
28
|
+
*/
|
|
29
|
+
private loadVariations;
|
|
30
|
+
private fetchRuntimeConfig;
|
|
31
|
+
private readVariationsFile;
|
|
32
|
+
private report;
|
|
33
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { safe } from "../../helpers/safe.js";
|
|
3
|
+
import { DEFAULT_TYPE, resolveRequest, toCurl, UndefinedVariationError, } from "./asset-variations.js";
|
|
4
|
+
/**
|
|
5
|
+
* Fetches a page from origin with exactly the headers Speed Kit's server would send for a cache
|
|
6
|
+
* variation, then reports what the origin answered.
|
|
7
|
+
*
|
|
8
|
+
* Use it to check whether the origin resolves a variation's identity — a store, a locale, a segment
|
|
9
|
+
* — without waiting for a render or reading backend logs. The variations come from the app's
|
|
10
|
+
* deployed runtime config, so the simulation follows production rather than a local file.
|
|
11
|
+
*
|
|
12
|
+
* Known deviations from the server, all deliberate:
|
|
13
|
+
* - The origin map (origin rewriting plus its extra headers) lives outside the runtime config;
|
|
14
|
+
* use `--host` for the Host part.
|
|
15
|
+
* - Revalidation headers (If-None-Match, If-Modified-Since) apply only when refreshing a cached
|
|
16
|
+
* entry, so they are out of scope.
|
|
17
|
+
* - Header order and the HTTP version come from undici, not Jetty.
|
|
18
|
+
*/
|
|
19
|
+
export class OriginRequestService {
|
|
20
|
+
context;
|
|
21
|
+
cli;
|
|
22
|
+
configApi;
|
|
23
|
+
constructor(context, cli, configApi) {
|
|
24
|
+
this.context = context;
|
|
25
|
+
this.cli = cli;
|
|
26
|
+
this.configApi = configApi;
|
|
27
|
+
}
|
|
28
|
+
async run() {
|
|
29
|
+
const variations = await this.loadVariations();
|
|
30
|
+
// Without variations every request would resolve to the bare defaults, so the report would
|
|
31
|
+
// describe a request the server never sends. Stop instead of simulating something wrong.
|
|
32
|
+
if (!variations)
|
|
33
|
+
return;
|
|
34
|
+
const variation = this.context.variation ?? DEFAULT_TYPE;
|
|
35
|
+
let resolved;
|
|
36
|
+
try {
|
|
37
|
+
resolved = resolveRequest(variations, this.context.pageUrl, this.context.variation, this.context.host);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof UndefinedVariationError) {
|
|
41
|
+
this.cli.writeError(error.message);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const response = await fetch(resolved.url, {
|
|
47
|
+
headers: resolved.headers,
|
|
48
|
+
redirect: "manual",
|
|
49
|
+
});
|
|
50
|
+
const body = await response.text();
|
|
51
|
+
const setCookies = response.headers.getSetCookie?.() ?? [];
|
|
52
|
+
if (this.context.bodyFile)
|
|
53
|
+
await writeFile(this.context.bodyFile, body);
|
|
54
|
+
if (this.context.json) {
|
|
55
|
+
this.cli.write(JSON.stringify({
|
|
56
|
+
variation,
|
|
57
|
+
resolutionPath: resolved.resolutionPath,
|
|
58
|
+
appliedConfigs: resolved.appliedKeys,
|
|
59
|
+
requestUrl: resolved.url,
|
|
60
|
+
requestHeaders: resolved.headers,
|
|
61
|
+
status: response.status,
|
|
62
|
+
responseHeaders: Object.fromEntries(response.headers),
|
|
63
|
+
setCookie: setCookies,
|
|
64
|
+
bodyLength: body.length,
|
|
65
|
+
}, null, 2));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.report(variation, resolved, response, setCookies, body);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Reads `speedKit.variations` from the app's deployed runtime config, or from a local file when
|
|
72
|
+
* `--variations` is given. Returns undefined when neither source is usable.
|
|
73
|
+
*/
|
|
74
|
+
async loadVariations() {
|
|
75
|
+
const raw = this.context.variationsFile
|
|
76
|
+
? await this.readVariationsFile(this.context.variationsFile)
|
|
77
|
+
: await this.fetchRuntimeConfig();
|
|
78
|
+
if (!raw)
|
|
79
|
+
return undefined;
|
|
80
|
+
const parsed = JSON.parse(raw);
|
|
81
|
+
const variations = parsed.speedKit?.variations ?? parsed.variations;
|
|
82
|
+
if (!variations) {
|
|
83
|
+
this.cli.writeError("the config carries no speedKit.variations — nothing to simulate");
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
this.cli.writeSuccess(`variations: ${Object.keys(variations).join(", ")}`);
|
|
87
|
+
return variations;
|
|
88
|
+
}
|
|
89
|
+
async fetchRuntimeConfig() {
|
|
90
|
+
this.cli.startAction("ORIGIN_REQUEST:FETCH:RUNTIME_CONFIG", "load variations from the app");
|
|
91
|
+
const serverConfig = await safe(this.configApi.getServerConfig());
|
|
92
|
+
if (serverConfig.success === false) {
|
|
93
|
+
this.cli.failAction("ORIGIN_REQUEST:FETCH:RUNTIME_CONFIG");
|
|
94
|
+
this.cli.writeError(`unable to load the runtime config: ${serverConfig.error}. Run "sk login", or pass --variations <file> to simulate against a config you already have.`);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
this.cli.successAction("ORIGIN_REQUEST:FETCH:RUNTIME_CONFIG");
|
|
98
|
+
return serverConfig.data;
|
|
99
|
+
}
|
|
100
|
+
async readVariationsFile(file) {
|
|
101
|
+
const content = await safe(readFile(file, "utf8"));
|
|
102
|
+
if (content.success === false) {
|
|
103
|
+
this.cli.writeError(`unable to read ${file}: ${content.error}`);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
this.cli.writeSuccess(`variations from ${file}`);
|
|
107
|
+
return content.data;
|
|
108
|
+
}
|
|
109
|
+
report(variation, resolved, response, setCookies, body) {
|
|
110
|
+
this.cli.write(`variation ${variation}`);
|
|
111
|
+
this.cli.write(`resolution ${resolved.resolutionPath} → applied: ${resolved.appliedKeys.join(" then ")}`);
|
|
112
|
+
this.cli.write(`request url ${resolved.url}`);
|
|
113
|
+
this.cli.write("request headers");
|
|
114
|
+
for (const [key, value] of Object.entries(resolved.headers))
|
|
115
|
+
this.cli.write(` ${key}: ${value}`);
|
|
116
|
+
this.cli.write(`\nstatus ${response.status} ${response.statusText}`);
|
|
117
|
+
const location = response.headers.get("location");
|
|
118
|
+
if (location)
|
|
119
|
+
this.cli.write(`location ${location}`);
|
|
120
|
+
if (setCookies.length) {
|
|
121
|
+
this.cli.write("set-cookie");
|
|
122
|
+
for (const cookie of setCookies)
|
|
123
|
+
this.cli.write(` ${cookie}`);
|
|
124
|
+
}
|
|
125
|
+
this.cli.write(`body ${body.length} bytes${this.context.bodyFile ? ` → ${this.context.bodyFile}` : ""}`);
|
|
126
|
+
this.cli.write(`\nequivalent curl:\n${toCurl(resolved.url, resolved.headers)}`);
|
|
127
|
+
if (this.context.grep) {
|
|
128
|
+
const pattern = new RegExp(this.context.grep, "i");
|
|
129
|
+
const hits = body.split("\n").filter((line) => pattern.test(line));
|
|
130
|
+
this.cli.write(`\ngrep "${this.context.grep}" → ${hits.length} line(s)`);
|
|
131
|
+
for (const line of hits.slice(0, 10))
|
|
132
|
+
this.cli.write(` ${line.trim().slice(0, 300)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -651,6 +651,95 @@
|
|
|
651
651
|
"onboarding.js"
|
|
652
652
|
]
|
|
653
653
|
},
|
|
654
|
+
"origin-request": {
|
|
655
|
+
"aliases": [],
|
|
656
|
+
"args": {
|
|
657
|
+
"customerPath": {
|
|
658
|
+
"description": "The customer config path",
|
|
659
|
+
"name": "customerPath",
|
|
660
|
+
"required": true
|
|
661
|
+
},
|
|
662
|
+
"pageUrl": {
|
|
663
|
+
"description": "Page to request from origin",
|
|
664
|
+
"name": "pageUrl",
|
|
665
|
+
"required": true
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
"description": "Request a page from origin with the exact headers Speed Kit's server sends for a cache variation, using the app's deployed runtime config",
|
|
669
|
+
"examples": [
|
|
670
|
+
"$ sk origin-request <customerPath> <pageUrl>",
|
|
671
|
+
"$ sk origin-request customers/decathlon.de https://www.decathlon.de/ -v mobile",
|
|
672
|
+
"$ sk origin-request customers/decathlon.de https://www.decathlon.de/ --grep \"storeId\" --body /tmp/origin.html"
|
|
673
|
+
],
|
|
674
|
+
"flags": {
|
|
675
|
+
"configName": {
|
|
676
|
+
"char": "c",
|
|
677
|
+
"description": "The costumer config name",
|
|
678
|
+
"name": "configName",
|
|
679
|
+
"required": false,
|
|
680
|
+
"default": "production",
|
|
681
|
+
"hasDynamicHelp": false,
|
|
682
|
+
"multiple": false,
|
|
683
|
+
"type": "option"
|
|
684
|
+
},
|
|
685
|
+
"variation": {
|
|
686
|
+
"char": "v",
|
|
687
|
+
"description": "Cache variation to simulate, e.g. desktop-fmarkt-variant-e_1907090. Omitted means the default variation",
|
|
688
|
+
"name": "variation",
|
|
689
|
+
"hasDynamicHelp": false,
|
|
690
|
+
"multiple": false,
|
|
691
|
+
"type": "option"
|
|
692
|
+
},
|
|
693
|
+
"host": {
|
|
694
|
+
"description": "Send this Host header, emulating an origin-map host override",
|
|
695
|
+
"name": "host",
|
|
696
|
+
"hasDynamicHelp": false,
|
|
697
|
+
"multiple": false,
|
|
698
|
+
"type": "option"
|
|
699
|
+
},
|
|
700
|
+
"body": {
|
|
701
|
+
"description": "Write the response body to this file",
|
|
702
|
+
"name": "body",
|
|
703
|
+
"hasDynamicHelp": false,
|
|
704
|
+
"multiple": false,
|
|
705
|
+
"type": "option"
|
|
706
|
+
},
|
|
707
|
+
"grep": {
|
|
708
|
+
"description": "Print body lines matching this regex, case-insensitive",
|
|
709
|
+
"name": "grep",
|
|
710
|
+
"hasDynamicHelp": false,
|
|
711
|
+
"multiple": false,
|
|
712
|
+
"type": "option"
|
|
713
|
+
},
|
|
714
|
+
"json": {
|
|
715
|
+
"description": "Machine-readable output",
|
|
716
|
+
"name": "json",
|
|
717
|
+
"allowNo": false,
|
|
718
|
+
"type": "boolean"
|
|
719
|
+
},
|
|
720
|
+
"variations": {
|
|
721
|
+
"description": "Simulate against a runtime config in a local file instead of the app's deployed one",
|
|
722
|
+
"name": "variations",
|
|
723
|
+
"hasDynamicHelp": false,
|
|
724
|
+
"multiple": false,
|
|
725
|
+
"type": "option"
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
"hasDynamicHelp": false,
|
|
729
|
+
"hiddenAliases": [],
|
|
730
|
+
"id": "origin-request",
|
|
731
|
+
"pluginAlias": "@speedkit/cli",
|
|
732
|
+
"pluginName": "@speedkit/cli",
|
|
733
|
+
"pluginType": "core",
|
|
734
|
+
"strict": true,
|
|
735
|
+
"enableJsonFlag": false,
|
|
736
|
+
"isESM": true,
|
|
737
|
+
"relativePath": [
|
|
738
|
+
"dist",
|
|
739
|
+
"commands",
|
|
740
|
+
"origin-request.js"
|
|
741
|
+
]
|
|
742
|
+
},
|
|
654
743
|
"prewarm": {
|
|
655
744
|
"aliases": [],
|
|
656
745
|
"args": {
|
|
@@ -1131,5 +1220,5 @@
|
|
|
1131
1220
|
]
|
|
1132
1221
|
}
|
|
1133
1222
|
},
|
|
1134
|
-
"version": "4.
|
|
1223
|
+
"version": "4.23.0"
|
|
1135
1224
|
}
|