@speedkit/cli 4.26.0 → 4.27.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 +10 -0
- package/README.md +186 -15
- package/dist/commands/auto-prewarm.d.ts +8 -1
- package/dist/commands/auto-prewarm.js +18 -7
- package/dist/commands/generate-pop-config.d.ts +1 -0
- package/dist/commands/generate-pop-config.js +9 -3
- package/dist/commands/prewarm.js +3 -1
- package/dist/commands/query/parameter.d.ts +26 -0
- package/dist/commands/query/parameter.js +27 -0
- package/dist/commands/query/prewarm.d.ts +31 -0
- package/dist/commands/query/prewarm.js +31 -0
- package/dist/helpers/clipboard.d.ts +19 -0
- package/dist/helpers/clipboard.js +70 -0
- package/dist/helpers/evaluate-speed-kit-config.d.ts +8 -18
- package/dist/helpers/evaluate-speed-kit-config.js +8 -6
- package/dist/helpers/evaluate-speed-kit-config.spec.d.ts +1 -0
- package/dist/helpers/evaluate-speed-kit-config.spec.js +78 -0
- package/dist/models/cli-parameters.d.ts +73 -0
- package/dist/models/cli-parameters.js +151 -0
- package/dist/models/cli-parameters.spec.d.ts +1 -0
- package/dist/models/cli-parameters.spec.js +47 -0
- package/dist/services/athena/athena-service.d.ts +2 -0
- package/dist/services/athena/athena-service.js +15 -4
- package/dist/services/athena/athena-service.spec.d.ts +1 -0
- package/dist/services/athena/athena-service.spec.js +74 -0
- package/dist/services/deploy/handler/install-resource-handler.js +3 -4
- package/dist/services/onboarding/onboarding-service-factory.js +2 -2
- package/dist/services/pop-config/pop-config-service.d.ts +8 -1
- package/dist/services/pop-config/pop-config-service.js +16 -28
- package/dist/services/prewarm/assets/asset-api-client.d.ts +1 -1
- package/dist/services/prewarm/assets/asset-api-client.js +5 -4
- package/dist/services/prewarm/auto-pre-warm-factory.d.ts +12 -2
- package/dist/services/prewarm/auto-pre-warm-factory.js +20 -6
- package/dist/services/prewarm/csv-reader.d.ts +21 -2
- package/dist/services/prewarm/csv-reader.js +71 -9
- package/dist/services/prewarm/csv-reader.spec.d.ts +1 -0
- package/dist/services/prewarm/csv-reader.spec.js +75 -0
- package/dist/services/prewarm/index.d.ts +1 -0
- package/dist/services/prewarm/index.js +1 -0
- package/dist/services/prewarm/pre-warm-factory.d.ts +0 -2
- package/dist/services/prewarm/pre-warm-factory.js +8 -11
- package/dist/services/prewarm/pre-warm-model.d.ts +14 -1
- package/dist/services/prewarm/pre-warm-model.js +0 -1
- package/dist/services/prewarm/pre-warm-service.d.ts +9 -4
- package/dist/services/prewarm/pre-warm-service.js +18 -14
- package/dist/services/prewarm/prewarm-targets.d.ts +12 -0
- package/dist/services/prewarm/prewarm-targets.js +18 -0
- package/dist/services/prewarm/prewarm-targets.spec.d.ts +1 -0
- package/dist/services/prewarm/prewarm-targets.spec.js +29 -0
- package/dist/services/query-builder/error/match-matches-nothing-error.d.ts +11 -0
- package/dist/services/query-builder/error/match-matches-nothing-error.js +17 -0
- package/dist/services/query-builder/queries/device.d.ts +21 -0
- package/dist/services/query-builder/queries/device.js +26 -0
- package/dist/services/query-builder/queries/index.d.ts +6 -0
- package/dist/services/query-builder/queries/index.js +6 -0
- package/dist/services/query-builder/queries/page-filter.d.ts +39 -0
- package/dist/services/query-builder/queries/page-filter.js +135 -0
- package/dist/services/query-builder/queries/page-source.d.ts +30 -0
- package/dist/services/query-builder/queries/page-source.js +79 -0
- package/dist/services/query-builder/queries/parameter-query.d.ts +14 -0
- package/dist/services/query-builder/queries/parameter-query.js +142 -0
- package/dist/services/query-builder/queries/pops-query.d.ts +27 -0
- package/dist/services/query-builder/queries/pops-query.js +65 -0
- package/dist/services/query-builder/queries/prewarm-query.d.ts +18 -0
- package/dist/services/query-builder/queries/prewarm-query.js +227 -0
- package/dist/services/query-builder/queries/query-model.d.ts +97 -0
- package/dist/services/query-builder/queries/query-model.js +19 -0
- package/dist/services/query-builder/queries/spec/golden-configs.d.ts +18 -0
- package/dist/services/query-builder/queries/spec/golden-configs.js +116 -0
- package/dist/services/query-builder/queries/spec/golden.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/golden.spec.js +161 -0
- package/dist/services/query-builder/queries/spec/pops-query.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/pops-query.spec.js +58 -0
- package/dist/services/query-builder/queries/spec/queries.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/queries.spec.js +312 -0
- package/dist/services/query-builder/queries/spec/reported-bugs.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/reported-bugs.spec.js +86 -0
- package/dist/services/query-builder/query-builder-factory.d.ts +2 -2
- package/dist/services/query-builder/query-builder-factory.js +35 -7
- package/dist/services/query-builder/query-builder-model.d.ts +31 -19
- package/dist/services/query-builder/query-builder-model.js +5 -5
- package/dist/services/query-builder/query-builder-service.d.ts +26 -5
- package/dist/services/query-builder/query-builder-service.js +157 -44
- package/dist/services/query-builder/query-command.d.ts +26 -0
- package/dist/services/query-builder/query-command.js +35 -0
- package/dist/services/query-builder/rules/index.d.ts +8 -0
- package/dist/services/query-builder/rules/index.js +8 -0
- package/dist/services/query-builder/rules/match-pattern.d.ts +51 -0
- package/dist/services/query-builder/rules/match-pattern.js +141 -0
- package/dist/services/query-builder/rules/pattern-language.d.ts +59 -0
- package/dist/services/query-builder/rules/pattern-language.js +159 -0
- package/dist/services/query-builder/rules/rule-model.d.ts +107 -0
- package/dist/services/query-builder/rules/rule-model.js +54 -0
- package/dist/services/query-builder/rules/rule-parser.d.ts +33 -0
- package/dist/services/query-builder/rules/rule-parser.js +189 -0
- package/dist/services/query-builder/rules/rule-reducer.d.ts +71 -0
- package/dist/services/query-builder/rules/rule-reducer.js +383 -0
- package/dist/services/query-builder/rules/rule-relevance.d.ts +43 -0
- package/dist/services/query-builder/rules/rule-relevance.js +69 -0
- package/dist/services/query-builder/rules/rule-to-sql.d.ts +51 -0
- package/dist/services/query-builder/rules/rule-to-sql.js +75 -0
- package/dist/services/query-builder/rules/spec/pattern-language.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/pattern-language.spec.js +90 -0
- package/dist/services/query-builder/rules/spec/rule-parser.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-parser.spec.js +150 -0
- package/dist/services/query-builder/rules/spec/rule-reducer.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-reducer.spec.js +169 -0
- package/dist/services/query-builder/rules/spec/rule-relevance.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-relevance.spec.js +88 -0
- package/dist/services/query-builder/rules/spec/rule-to-sql.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-to-sql.spec.js +110 -0
- package/dist/services/query-builder/rules/spec/runtime-semantics.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/runtime-semantics.spec.js +55 -0
- package/dist/services/query-builder/rules/spec/strip-parameters.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/strip-parameters.spec.js +85 -0
- package/dist/services/query-builder/rules/strip-parameters.d.ts +39 -0
- package/dist/services/query-builder/rules/strip-parameters.js +197 -0
- package/dist/services/query-builder/sql/expression.d.ts +73 -0
- package/dist/services/query-builder/sql/expression.js +133 -0
- package/dist/services/query-builder/sql/format.d.ts +14 -0
- package/dist/services/query-builder/sql/format.js +48 -0
- package/dist/services/query-builder/sql/index.d.ts +4 -0
- package/dist/services/query-builder/sql/index.js +4 -0
- package/dist/services/query-builder/sql/predicate.d.ts +66 -0
- package/dist/services/query-builder/sql/predicate.js +124 -0
- package/dist/services/query-builder/sql/regex.d.ts +28 -0
- package/dist/services/query-builder/sql/regex.js +81 -0
- package/dist/services/query-builder/sql/spec/expression.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/expression.spec.js +75 -0
- package/dist/services/query-builder/sql/spec/format.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/format.spec.js +37 -0
- package/dist/services/query-builder/sql/spec/predicate.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/predicate.spec.js +78 -0
- package/dist/services/query-builder/sql/spec/regex.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/regex.spec.js +62 -0
- package/oclif.manifest.json +350 -100
- package/package.json +11 -2
- package/dist/commands/build-parameter-query.d.ts +0 -14
- package/dist/commands/build-parameter-query.js +0 -25
- package/dist/commands/build-prewarm-query.d.ts +0 -16
- package/dist/commands/build-prewarm-query.js +0 -37
- package/dist/helpers/build-query-helper.d.ts +0 -32
- package/dist/helpers/build-query-helper.js +0 -223
- package/dist/helpers/get-parsed-config.d.ts +0 -15
- package/dist/helpers/get-parsed-config.js +0 -62
- package/dist/services/onboarding/dashboard/parameter-query-builder.d.ts +0 -8
- package/dist/services/onboarding/dashboard/parameter-query-builder.js +0 -63
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import clipboardy from "clipboardy";
|
|
3
|
+
/** How long a clipboard tool may take before the CLI stops waiting for it. */
|
|
4
|
+
const TIMEOUT_IN_MS = 2000;
|
|
5
|
+
/**
|
|
6
|
+
* Puts text on the system clipboard, and never holds up the CLI.
|
|
7
|
+
*
|
|
8
|
+
* `clipboardy.writeSync` cannot be used on Wayland. It runs `wl-copy` through
|
|
9
|
+
* `execaSync`, and `wl-copy` has to *stay alive* to own the selection — there
|
|
10
|
+
* is no clipboard daemon on Wayland, the source process serves the data. It
|
|
11
|
+
* forks into the background but inherits the pipes `execa` created, so the
|
|
12
|
+
* synchronous wait never returns: the command hangs after the work is done,
|
|
13
|
+
* with the output still unwritten, until the user interrupts it.
|
|
14
|
+
*
|
|
15
|
+
* On Wayland the tool is therefore started the way it wants to be — detached,
|
|
16
|
+
* with its output going nowhere, and unreferenced so Node can exit while it
|
|
17
|
+
* keeps serving the clipboard. Everywhere else `clipboardy` is used as before,
|
|
18
|
+
* behind a timeout, so no clipboard tool can ever be the reason a command
|
|
19
|
+
* appears to do nothing.
|
|
20
|
+
*
|
|
21
|
+
* @returns whether the text reached the clipboard.
|
|
22
|
+
*/
|
|
23
|
+
export async function copyToClipboard(text) {
|
|
24
|
+
if (isWayland()) {
|
|
25
|
+
const copied = await copyWithWaylandClipboard(text);
|
|
26
|
+
if (copied) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return await withTimeout(clipboardy.write(text).then(() => true));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isWayland() {
|
|
38
|
+
return process.platform === "linux" && !!process.env.WAYLAND_DISPLAY;
|
|
39
|
+
}
|
|
40
|
+
/** Hands the text to `wl-copy` and leaves it running to serve the selection. */
|
|
41
|
+
function copyWithWaylandClipboard(text) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const copy = spawn("wl-copy", ["--type", "text/plain"], {
|
|
44
|
+
detached: true,
|
|
45
|
+
// Anything but `ignore` here is what makes the call hang.
|
|
46
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
47
|
+
});
|
|
48
|
+
copy.on("error", () => resolve(false));
|
|
49
|
+
copy.on("spawn", () => {
|
|
50
|
+
copy.stdin.end(text, () => {
|
|
51
|
+
copy.unref();
|
|
52
|
+
resolve(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function withTimeout(copied) {
|
|
58
|
+
let timer;
|
|
59
|
+
const expired = new Promise((resolve) => {
|
|
60
|
+
timer = setTimeout(() => resolve(false), TIMEOUT_IN_MS);
|
|
61
|
+
// A pending timer must not keep the process alive on its own.
|
|
62
|
+
timer.unref();
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
return await Promise.race([copied, expired]);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { InstallResourceFile } from "../services/integration-api/index.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The `window.speedKit` a config assigned — whatever shape the config wrote,
|
|
4
|
+
* not the seed object the browser mock starts from.
|
|
5
|
+
*/
|
|
6
|
+
export type EvaluatedSpeedKitConfig = Record<string, any>;
|
|
7
|
+
export declare function evaluateLocalSpeedKitConfig(app: string, origin: string, speedKitConfigFile: InstallResourceFile): Promise<EvaluatedSpeedKitConfig>;
|
|
8
8
|
export declare function evaluateLocalAndRemoteSpeedKit(app: string, origin: string, speedKitConfigFile: InstallResourceFile, activeSpeedKitConfig: string): Promise<{
|
|
9
|
-
localSpeedKit:
|
|
10
|
-
|
|
11
|
-
splitTestId: any;
|
|
12
|
-
detectDevice: () => void;
|
|
13
|
-
customVariation: any[];
|
|
14
|
-
};
|
|
15
|
-
remoteSpeedKit: {
|
|
16
|
-
split: any;
|
|
17
|
-
splitTestId: any;
|
|
18
|
-
detectDevice: () => void;
|
|
19
|
-
customVariation: any[];
|
|
20
|
-
};
|
|
9
|
+
localSpeedKit: EvaluatedSpeedKitConfig;
|
|
10
|
+
remoteSpeedKit: EvaluatedSpeedKitConfig;
|
|
21
11
|
}>;
|
|
@@ -78,14 +78,16 @@ function buildBrowserMock(origin, app) {
|
|
|
78
78
|
top: {},
|
|
79
79
|
};
|
|
80
80
|
window.top = window;
|
|
81
|
+
// A config may read the DOM while it builds itself — `precache` is
|
|
82
|
+
// frequently a list of script and link hrefs. There is no document here, so
|
|
83
|
+
// these answer the way the DOM answers for a page without matches: an empty
|
|
84
|
+
// list, not `undefined`, which `Array.from` and `forEach` would choke on.
|
|
81
85
|
const document = {
|
|
82
86
|
cookie: "",
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
querySelectorAll: () =>
|
|
87
|
-
/* empty */
|
|
88
|
-
},
|
|
87
|
+
referrer: "",
|
|
88
|
+
getElementById: () => null,
|
|
89
|
+
querySelector: () => null,
|
|
90
|
+
querySelectorAll: () => [],
|
|
89
91
|
createElement: () => {
|
|
90
92
|
/* empty */
|
|
91
93
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { evaluateLocalSpeedKitConfig } from "./evaluate-speed-kit-config.js";
|
|
4
|
+
function configFile(source) {
|
|
5
|
+
return {
|
|
6
|
+
getContent: () => source,
|
|
7
|
+
path: "config_SpeedKit.js",
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function evaluate(source) {
|
|
11
|
+
return evaluateLocalSpeedKitConfig("shop-de", "https://www.shop.de", configFile(source));
|
|
12
|
+
}
|
|
13
|
+
describe("evaluateLocalSpeedKitConfig", () => {
|
|
14
|
+
it("should read the app and the origin the config is evaluated for", async () => {
|
|
15
|
+
const speedKit = await evaluate(`
|
|
16
|
+
window.speedKit = { appName: APP, host: window.location.host };
|
|
17
|
+
`);
|
|
18
|
+
expect(speedKit.appName).to.equal("shop-de");
|
|
19
|
+
expect(speedKit.host).to.equal("www.shop.de");
|
|
20
|
+
});
|
|
21
|
+
it("should evaluate a config that reads the dom while it builds itself", async () => {
|
|
22
|
+
// A real shape: `precache` is collected from the document's own script and
|
|
23
|
+
// link tags. A `querySelectorAll` returning undefined would throw here,
|
|
24
|
+
// and the whole config — every rule in it — would be unreadable.
|
|
25
|
+
const speedKit = await evaluate(`
|
|
26
|
+
window.speedKit = {
|
|
27
|
+
appName: APP,
|
|
28
|
+
precache: Array.from(
|
|
29
|
+
document.querySelectorAll('script[src], link[rel="stylesheet"]'),
|
|
30
|
+
).map(function (element) {
|
|
31
|
+
return element.getAttribute('src');
|
|
32
|
+
}),
|
|
33
|
+
enabledSites: [{ pathname: [/^\\/p\\//i] }],
|
|
34
|
+
};
|
|
35
|
+
`);
|
|
36
|
+
expect(speedKit.precache).to.deep.equal([]);
|
|
37
|
+
expect(speedKit.enabledSites).to.have.lengthOf(1);
|
|
38
|
+
});
|
|
39
|
+
it("should expose the location a config branches on (ED-409)", async () => {
|
|
40
|
+
// nbb reads `window.location.hostname` to pick its split, and read
|
|
41
|
+
// `document.referrer` to tell a new visitor from a returning one. When
|
|
42
|
+
// `window.location` was undefined this failed with
|
|
43
|
+
// "Cannot read properties of undefined (reading 'hostname')".
|
|
44
|
+
const speedKit = await evaluate(`
|
|
45
|
+
var hostname = window.location.hostname;
|
|
46
|
+
var isNewUser = !document.referrer;
|
|
47
|
+
window.speedKit = {
|
|
48
|
+
appName: APP,
|
|
49
|
+
hostname: hostname,
|
|
50
|
+
alsoHostname: location.hostname,
|
|
51
|
+
isNewUser: isNewUser,
|
|
52
|
+
};
|
|
53
|
+
`);
|
|
54
|
+
expect(speedKit.hostname).to.equal("www.shop.de");
|
|
55
|
+
expect(speedKit.alsoHostname).to.equal("www.shop.de");
|
|
56
|
+
expect(speedKit.isNewUser).to.equal(true);
|
|
57
|
+
});
|
|
58
|
+
it("should answer a missing element with null, the way the dom does", async () => {
|
|
59
|
+
const speedKit = await evaluate(`
|
|
60
|
+
window.speedKit = {
|
|
61
|
+
appName: APP,
|
|
62
|
+
hasMarker: document.querySelector('.marker') !== null,
|
|
63
|
+
byId: document.getElementById('marker'),
|
|
64
|
+
};
|
|
65
|
+
`);
|
|
66
|
+
expect(speedKit.hasMarker).to.equal(false);
|
|
67
|
+
expect(speedKit.byId).to.equal(null);
|
|
68
|
+
});
|
|
69
|
+
it("should report what the config itself throws", async () => {
|
|
70
|
+
try {
|
|
71
|
+
await evaluate(`window.speedKit = { appName: nothingHere.atAll };`);
|
|
72
|
+
expect.fail("should have thrown");
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
expect(String(error)).to.contain("config_Speedkit.js");
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -4,16 +4,30 @@ export declare enum CLIParameters {
|
|
|
4
4
|
ConfigName = "configName",
|
|
5
5
|
ContentTypes = "contentTypes",
|
|
6
6
|
Cookies = "cookies",
|
|
7
|
+
Count = "count",
|
|
7
8
|
CreateTests = "CreateTests",
|
|
8
9
|
CustomerPath = "customerPath",
|
|
9
10
|
CustomersDirectory = "customersDirectory",
|
|
11
|
+
Days = "days",
|
|
10
12
|
Delay = "delay",
|
|
13
|
+
Device = "device",
|
|
11
14
|
Domain = "domain",
|
|
12
15
|
DryRun = "dryRun",
|
|
16
|
+
Execute = "execute",
|
|
17
|
+
BySet = "bySet",
|
|
18
|
+
ExcludeNoindex = "excludeNoindex",
|
|
19
|
+
ExcludeNotFound = "excludeNotFound",
|
|
20
|
+
ExcludeSuspicious = "excludeSuspicious",
|
|
21
|
+
Explain = "explain",
|
|
13
22
|
Group = "group",
|
|
23
|
+
HardNavigationsOnly = "hardNavigationsOnly",
|
|
14
24
|
HideDiff = "hideDiff",
|
|
15
25
|
Ips = "ips",
|
|
16
26
|
KeepParameterSorting = "keepParameterSorting",
|
|
27
|
+
Limit = "limit",
|
|
28
|
+
Match = "match",
|
|
29
|
+
MinHits = "minHits",
|
|
30
|
+
MinImpressions = "minImpressions",
|
|
17
31
|
Quiet = "quiet",
|
|
18
32
|
Mobile = "mobile",
|
|
19
33
|
PageUrl = "pageUrl",
|
|
@@ -35,9 +49,11 @@ export declare enum CLIParametersChar {
|
|
|
35
49
|
CreateTests = "t",
|
|
36
50
|
CustomerPath = "p",
|
|
37
51
|
CustomersDirectory = "d",
|
|
52
|
+
Days = "d",
|
|
38
53
|
Delay = "d",
|
|
39
54
|
Domain = "d",
|
|
40
55
|
DryRun = "d",
|
|
56
|
+
Execute = "e",
|
|
41
57
|
Group = "g",
|
|
42
58
|
HideDiff = "h",
|
|
43
59
|
Ips = "i",
|
|
@@ -59,9 +75,11 @@ export declare enum CLIParametersExample {
|
|
|
59
75
|
CustomerPath = "customers/decathlon.de",
|
|
60
76
|
CustomersDirectory = "customers",
|
|
61
77
|
Delay = "1200",
|
|
78
|
+
Devices = "desktop,mobile",
|
|
62
79
|
Domain = "www.decathlon.de",
|
|
63
80
|
Group = "B",
|
|
64
81
|
Ips = "151.101.120.194",
|
|
82
|
+
Match = "^/p/",
|
|
65
83
|
Mobile = "Y",
|
|
66
84
|
PageUrl = "https://www.decathlon.de/",
|
|
67
85
|
Path = "C:\\Users\\testUser\\file.csv",
|
|
@@ -78,6 +96,61 @@ export declare const CLI_CONFIG_NAME: {
|
|
|
78
96
|
export declare const CLI_CONFIG_IS_TEST: {
|
|
79
97
|
useTestConfig: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
80
98
|
};
|
|
99
|
+
/** Shared wording, so every query command describes the window the same way. */
|
|
100
|
+
export declare const CLI_QUERY_DAYS_DESCRIPTION = "Number of days the query looks back";
|
|
101
|
+
/**
|
|
102
|
+
* How far back a query looks. Its own object because `generate-pop-config`
|
|
103
|
+
* takes it without the `-d` shorthand, which `--deploy` holds there.
|
|
104
|
+
*/
|
|
105
|
+
export declare const CLI_QUERY_DAYS: {
|
|
106
|
+
days: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* What a query command does with the query it built. Both commands take them,
|
|
110
|
+
* so a query can be piped or run without a detour through the clipboard.
|
|
111
|
+
*/
|
|
112
|
+
export declare const CLI_QUERY_OUTPUT: {
|
|
113
|
+
execute: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
114
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Which page impressions a query leaves out, and whether to account for every
|
|
118
|
+
* rule it does not carry over literally. Bots are always excluded.
|
|
119
|
+
*/
|
|
120
|
+
export declare const CLI_QUERY_FILTERS: {
|
|
121
|
+
excludeNoindex: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
122
|
+
excludeSuspicious: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
123
|
+
explain: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
124
|
+
hardNavigationsOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Which variation the pre-warm list carries per url, instead of leaving the
|
|
128
|
+
* pre-warmer to request every url in every variation.
|
|
129
|
+
*
|
|
130
|
+
* The two flags answer the same question from two different sources — the
|
|
131
|
+
* traffic, or the operator — so combining them would cross-produce the list
|
|
132
|
+
* again, which is what they are there to stop. A command that also reads a
|
|
133
|
+
* variations file from disk excludes that one on its own side.
|
|
134
|
+
*/
|
|
135
|
+
export declare const CLI_QUERY_VARIATIONS: {
|
|
136
|
+
device: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
137
|
+
variation: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
138
|
+
};
|
|
139
|
+
/** What the pre-warm query keeps of a long tail that is mostly seen once. */
|
|
140
|
+
export declare const CLI_QUERY_PREWARM: {
|
|
141
|
+
count: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
142
|
+
excludeNotFound: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
143
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
144
|
+
match: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
145
|
+
minHits: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
146
|
+
device: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
147
|
+
variation: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
148
|
+
};
|
|
149
|
+
/** How the parameter query presents what it found. */
|
|
150
|
+
export declare const CLI_QUERY_PARAMETER: {
|
|
151
|
+
bySet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
152
|
+
minImpressions: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
153
|
+
};
|
|
81
154
|
export declare const CLI_CUSTOMER_CONFIG: {
|
|
82
155
|
customerPath: import("@oclif/core/interfaces").Arg<string, {
|
|
83
156
|
exists?: boolean;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { Args, Flags } from "@oclif/core";
|
|
2
|
+
// Imported from the modules rather than from the query-builder barrels: this
|
|
3
|
+
// file is loaded by every command, and the rules barrel pulls in the automata
|
|
4
|
+
// library.
|
|
5
|
+
import { parseMatchPattern } from "../services/query-builder/rules/match-pattern.js";
|
|
6
|
+
import { DEVICE_VARIATIONS } from "../services/query-builder/queries/device.js";
|
|
2
7
|
export var CLIParameters;
|
|
3
8
|
(function (CLIParameters) {
|
|
4
9
|
CLIParameters["AppName"] = "app";
|
|
@@ -6,16 +11,30 @@ export var CLIParameters;
|
|
|
6
11
|
CLIParameters["ConfigName"] = "configName";
|
|
7
12
|
CLIParameters["ContentTypes"] = "contentTypes";
|
|
8
13
|
CLIParameters["Cookies"] = "cookies";
|
|
14
|
+
CLIParameters["Count"] = "count";
|
|
9
15
|
CLIParameters["CreateTests"] = "CreateTests";
|
|
10
16
|
CLIParameters["CustomerPath"] = "customerPath";
|
|
11
17
|
CLIParameters["CustomersDirectory"] = "customersDirectory";
|
|
18
|
+
CLIParameters["Days"] = "days";
|
|
12
19
|
CLIParameters["Delay"] = "delay";
|
|
20
|
+
CLIParameters["Device"] = "device";
|
|
13
21
|
CLIParameters["Domain"] = "domain";
|
|
14
22
|
CLIParameters["DryRun"] = "dryRun";
|
|
23
|
+
CLIParameters["Execute"] = "execute";
|
|
24
|
+
CLIParameters["BySet"] = "bySet";
|
|
25
|
+
CLIParameters["ExcludeNoindex"] = "excludeNoindex";
|
|
26
|
+
CLIParameters["ExcludeNotFound"] = "excludeNotFound";
|
|
27
|
+
CLIParameters["ExcludeSuspicious"] = "excludeSuspicious";
|
|
28
|
+
CLIParameters["Explain"] = "explain";
|
|
15
29
|
CLIParameters["Group"] = "group";
|
|
30
|
+
CLIParameters["HardNavigationsOnly"] = "hardNavigationsOnly";
|
|
16
31
|
CLIParameters["HideDiff"] = "hideDiff";
|
|
17
32
|
CLIParameters["Ips"] = "ips";
|
|
18
33
|
CLIParameters["KeepParameterSorting"] = "keepParameterSorting";
|
|
34
|
+
CLIParameters["Limit"] = "limit";
|
|
35
|
+
CLIParameters["Match"] = "match";
|
|
36
|
+
CLIParameters["MinHits"] = "minHits";
|
|
37
|
+
CLIParameters["MinImpressions"] = "minImpressions";
|
|
19
38
|
CLIParameters["Quiet"] = "quiet";
|
|
20
39
|
CLIParameters["Mobile"] = "mobile";
|
|
21
40
|
CLIParameters["PageUrl"] = "pageUrl";
|
|
@@ -39,9 +58,11 @@ export var CLIParametersChar;
|
|
|
39
58
|
CLIParametersChar["CreateTests"] = "t";
|
|
40
59
|
CLIParametersChar["CustomerPath"] = "p";
|
|
41
60
|
CLIParametersChar["CustomersDirectory"] = "d";
|
|
61
|
+
CLIParametersChar["Days"] = "d";
|
|
42
62
|
CLIParametersChar["Delay"] = "d";
|
|
43
63
|
CLIParametersChar["Domain"] = "d";
|
|
44
64
|
CLIParametersChar["DryRun"] = "d";
|
|
65
|
+
CLIParametersChar["Execute"] = "e";
|
|
45
66
|
CLIParametersChar["Group"] = "g";
|
|
46
67
|
CLIParametersChar["HideDiff"] = "h";
|
|
47
68
|
CLIParametersChar["Ips"] = "i";
|
|
@@ -65,9 +86,11 @@ export var CLIParametersExample;
|
|
|
65
86
|
CLIParametersExample["CustomerPath"] = "customers/decathlon.de";
|
|
66
87
|
CLIParametersExample["CustomersDirectory"] = "customers";
|
|
67
88
|
CLIParametersExample["Delay"] = "1200";
|
|
89
|
+
CLIParametersExample["Devices"] = "desktop,mobile";
|
|
68
90
|
CLIParametersExample["Domain"] = "www.decathlon.de";
|
|
69
91
|
CLIParametersExample["Group"] = "B";
|
|
70
92
|
CLIParametersExample["Ips"] = "151.101.120.194";
|
|
93
|
+
CLIParametersExample["Match"] = "^/p/";
|
|
71
94
|
CLIParametersExample["Mobile"] = "Y";
|
|
72
95
|
CLIParametersExample["PageUrl"] = "https://www.decathlon.de/";
|
|
73
96
|
CLIParametersExample["Path"] = "C:\\Users\\testUser\\file.csv";
|
|
@@ -95,6 +118,134 @@ export const CLI_CONFIG_IS_TEST = {
|
|
|
95
118
|
description: "Override app with configured testApp",
|
|
96
119
|
}),
|
|
97
120
|
};
|
|
121
|
+
/** Shared wording, so every query command describes the window the same way. */
|
|
122
|
+
export const CLI_QUERY_DAYS_DESCRIPTION = "Number of days the query looks back";
|
|
123
|
+
/**
|
|
124
|
+
* How far back a query looks. Its own object because `generate-pop-config`
|
|
125
|
+
* takes it without the `-d` shorthand, which `--deploy` holds there.
|
|
126
|
+
*/
|
|
127
|
+
export const CLI_QUERY_DAYS = {
|
|
128
|
+
[CLIParameters.Days]: Flags.integer({
|
|
129
|
+
char: CLIParametersChar.Days,
|
|
130
|
+
description: CLI_QUERY_DAYS_DESCRIPTION,
|
|
131
|
+
min: 1,
|
|
132
|
+
required: false,
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* What a query command does with the query it built. Both commands take them,
|
|
137
|
+
* so a query can be piped or run without a detour through the clipboard.
|
|
138
|
+
*/
|
|
139
|
+
export const CLI_QUERY_OUTPUT = {
|
|
140
|
+
[CLIParameters.Execute]: Flags.boolean({
|
|
141
|
+
char: CLIParametersChar.Execute,
|
|
142
|
+
default: false,
|
|
143
|
+
description: "Execute the query directly and write the result to a <app>-<date>-<query>.csv file",
|
|
144
|
+
}),
|
|
145
|
+
[CLIParameters.Quiet]: Flags.boolean({
|
|
146
|
+
char: CLIParametersChar.Quiet,
|
|
147
|
+
default: false,
|
|
148
|
+
description: "Output only the raw query without clipboard copy or styling",
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Which page impressions a query leaves out, and whether to account for every
|
|
153
|
+
* rule it does not carry over literally. Bots are always excluded.
|
|
154
|
+
*/
|
|
155
|
+
export const CLI_QUERY_FILTERS = {
|
|
156
|
+
[CLIParameters.ExcludeNoindex]: Flags.boolean({
|
|
157
|
+
default: false,
|
|
158
|
+
description: "Exclude page impressions of pages marked noindex",
|
|
159
|
+
}),
|
|
160
|
+
[CLIParameters.ExcludeSuspicious]: Flags.boolean({
|
|
161
|
+
default: false,
|
|
162
|
+
description: "Exclude page impressions Speed Kit classified as suspicious",
|
|
163
|
+
}),
|
|
164
|
+
[CLIParameters.Explain]: Flags.boolean({
|
|
165
|
+
default: false,
|
|
166
|
+
description: "Account for every rule the query leaves out, reduces or approximates",
|
|
167
|
+
}),
|
|
168
|
+
[CLIParameters.HardNavigationsOnly]: Flags.boolean({
|
|
169
|
+
default: false,
|
|
170
|
+
description: "Exclude soft (SPA) navigations",
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Which variation the pre-warm list carries per url, instead of leaving the
|
|
175
|
+
* pre-warmer to request every url in every variation.
|
|
176
|
+
*
|
|
177
|
+
* The two flags answer the same question from two different sources — the
|
|
178
|
+
* traffic, or the operator — so combining them would cross-produce the list
|
|
179
|
+
* again, which is what they are there to stop. A command that also reads a
|
|
180
|
+
* variations file from disk excludes that one on its own side.
|
|
181
|
+
*/
|
|
182
|
+
export const CLI_QUERY_VARIATIONS = {
|
|
183
|
+
[CLIParameters.Device]: Flags.string({
|
|
184
|
+
delimiter: ",",
|
|
185
|
+
description: "Split the list by device class, so every url is warmed in the variations it was actually requested in. A class not named is left out",
|
|
186
|
+
exclusive: [CLIParameters.Variation],
|
|
187
|
+
multiple: true,
|
|
188
|
+
options: [...DEVICE_VARIATIONS],
|
|
189
|
+
required: false,
|
|
190
|
+
}),
|
|
191
|
+
[CLIParameters.Variation]: Flags.string({
|
|
192
|
+
char: CLIParametersChar.Variation,
|
|
193
|
+
delimiter: ",",
|
|
194
|
+
description: "Pair every url of the list with each of these variations. For the custom variations no traffic can reveal, e.g. 'desktop-pdp-a,mobile-pdp-a'",
|
|
195
|
+
exclusive: [CLIParameters.Device],
|
|
196
|
+
multiple: true,
|
|
197
|
+
required: false,
|
|
198
|
+
}),
|
|
199
|
+
};
|
|
200
|
+
/** What the pre-warm query keeps of a long tail that is mostly seen once. */
|
|
201
|
+
export const CLI_QUERY_PREWARM = {
|
|
202
|
+
...CLI_QUERY_VARIATIONS,
|
|
203
|
+
[CLIParameters.Count]: Flags.boolean({
|
|
204
|
+
default: false,
|
|
205
|
+
description: "Run the query and report how large the list is instead of what is in it: urls, the impressions they stand for, and a row per variation where the list carries variations. Every other flag still applies, so this counts the list the same command would produce",
|
|
206
|
+
exclusive: [CLIParameters.Execute],
|
|
207
|
+
}),
|
|
208
|
+
[CLIParameters.ExcludeNotFound]: Flags.boolean({
|
|
209
|
+
default: false,
|
|
210
|
+
description: "Leave out urls the origin answers with a 404 more often than not",
|
|
211
|
+
}),
|
|
212
|
+
[CLIParameters.Limit]: Flags.integer({
|
|
213
|
+
description: "Keep only the most requested urls",
|
|
214
|
+
min: 1,
|
|
215
|
+
required: false,
|
|
216
|
+
}),
|
|
217
|
+
[CLIParameters.Match]: Flags.string({
|
|
218
|
+
description: "Keep only urls whose path matches this regular expression, e.g. '^/p/'. The path is matched with its query string, the way a version 1 pathname rule matches it, so '[?&]size=' works too. Repeat the flag to keep the urls matching any of several patterns, and start a pattern with (?i) to match case-insensitively",
|
|
219
|
+
multiple: true,
|
|
220
|
+
parse: async (input) => {
|
|
221
|
+
try {
|
|
222
|
+
parseMatchPattern(input);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw new Error(`--match ${input} is not a regular expression: ${error.message}`, { cause: error });
|
|
226
|
+
}
|
|
227
|
+
return input;
|
|
228
|
+
},
|
|
229
|
+
required: false,
|
|
230
|
+
}),
|
|
231
|
+
[CLIParameters.MinHits]: Flags.integer({
|
|
232
|
+
description: "Keep only urls requested at least this often in the window",
|
|
233
|
+
min: 1,
|
|
234
|
+
required: false,
|
|
235
|
+
}),
|
|
236
|
+
};
|
|
237
|
+
/** How the parameter query presents what it found. */
|
|
238
|
+
export const CLI_QUERY_PARAMETER = {
|
|
239
|
+
[CLIParameters.BySet]: Flags.boolean({
|
|
240
|
+
default: false,
|
|
241
|
+
description: "Rank whole sets of parameters instead of single parameters, to see which ones travel together",
|
|
242
|
+
}),
|
|
243
|
+
[CLIParameters.MinImpressions]: Flags.integer({
|
|
244
|
+
description: "Leave out parameters seen on fewer page impressions than this",
|
|
245
|
+
min: 1,
|
|
246
|
+
required: false,
|
|
247
|
+
}),
|
|
248
|
+
};
|
|
98
249
|
export const CLI_CUSTOMER_CONFIG = {
|
|
99
250
|
[CLIParameters.CustomerPath]: Args.directory({
|
|
100
251
|
name: CLIParameters.CustomerPath,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parse } from "@oclif/core/parser";
|
|
2
|
+
import { expect } from "chai";
|
|
3
|
+
import { describe, it } from "mocha";
|
|
4
|
+
import { CLI_QUERY_VARIATIONS } from "./cli-parameters.js";
|
|
5
|
+
async function parseFlags(argv) {
|
|
6
|
+
const { flags } = await parse(argv, { flags: CLI_QUERY_VARIATIONS });
|
|
7
|
+
return flags;
|
|
8
|
+
}
|
|
9
|
+
/** The message the parser refused the arguments with. */
|
|
10
|
+
async function refusal(argv) {
|
|
11
|
+
try {
|
|
12
|
+
await parseFlags(argv);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return error.message;
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Expected ${argv.join(" ")} to be refused`);
|
|
18
|
+
}
|
|
19
|
+
/** What the parser has to catch before Athena is asked anything. */
|
|
20
|
+
describe("the --device and --variation flags", () => {
|
|
21
|
+
it("should read a comma separated list of devices", async () => {
|
|
22
|
+
expect(await parseFlags(["--device", "desktop,mobile"])).to.deep.equal({
|
|
23
|
+
device: ["desktop", "mobile"],
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
it("should read a comma separated list of variations", async () => {
|
|
27
|
+
expect(await parseFlags(["-v", "men,women"])).to.deep.equal({
|
|
28
|
+
variation: ["men", "women"],
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
it("should refuse a device class that does not exist", async () => {
|
|
32
|
+
// A typo has to fail here rather than produce a query that scans a whole
|
|
33
|
+
// window and returns nothing.
|
|
34
|
+
expect(await refusal(["--device", "phone"])).to.contain("Expected --device=phone to be one of: desktop, mobile, tablet");
|
|
35
|
+
});
|
|
36
|
+
it("should refuse a device class hidden among valid ones", async () => {
|
|
37
|
+
expect(await refusal(["--device", "desktop,tv"])).to.contain("to be one of");
|
|
38
|
+
});
|
|
39
|
+
it("should refuse the two flags together", async () => {
|
|
40
|
+
// They answer the same question from two sources; together they would
|
|
41
|
+
// cross-produce the list again.
|
|
42
|
+
expect(await refusal(["--device", "desktop", "--variation", "men"])).to.contain("cannot also be provided");
|
|
43
|
+
});
|
|
44
|
+
it("should take neither by default", async () => {
|
|
45
|
+
expect(await parseFlags([])).to.deep.equal({});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -12,6 +12,8 @@ export declare class AwsAthenaService implements AthenaService {
|
|
|
12
12
|
private cli;
|
|
13
13
|
constructor(client: AthenaClient, cli: CliServiceInterface);
|
|
14
14
|
getResult<T>(query: string, parameters?: string[], maxAgeInMinutes?: number): Promise<T[]>;
|
|
15
|
+
/** The query, from sending it off to the rows it produced. */
|
|
16
|
+
private runAndFetch;
|
|
15
17
|
private castValueToType;
|
|
16
18
|
/**
|
|
17
19
|
* Fetch the query result of the given query id, if the result is still available
|
|
@@ -17,15 +17,26 @@ export class AwsAthenaService {
|
|
|
17
17
|
}
|
|
18
18
|
async getResult(query, parameters = [], maxAgeInMinutes = DEFAULT_MAX_AGE_IN_MINUTES) {
|
|
19
19
|
this.cli.startAction("ATHENA:QUERY", "execute athena query");
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Everything the action covers is inside the guard, running the query
|
|
21
|
+
// included: Athena reports a rejected query by failing the execution, and
|
|
22
|
+
// a throw that escapes here leaves the spinner running — which is a timer,
|
|
23
|
+
// so the process stays alive with nothing left to do and has to be killed
|
|
24
|
+
// by hand.
|
|
25
|
+
const result = await safe(this.runAndFetch(query, parameters, maxAgeInMinutes));
|
|
22
26
|
if (result.success === true) {
|
|
23
27
|
this.cli.successAction("ATHENA:QUERY");
|
|
24
28
|
return result.data;
|
|
25
29
|
}
|
|
30
|
+
// Rethrown rather than answered with an empty result: no rows and no rows
|
|
31
|
+
// found look the same to a caller, and a pre-warm list that silently came
|
|
32
|
+
// back empty is worse than one that failed.
|
|
26
33
|
this.cli.failAction("ATHENA:QUERY");
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
throw result.errorObj ?? new Error(result.error);
|
|
35
|
+
}
|
|
36
|
+
/** The query, from sending it off to the rows it produced. */
|
|
37
|
+
async runAndFetch(query, parameters, maxAgeInMinutes) {
|
|
38
|
+
const queryId = await this.runQuery(query, parameters, maxAgeInMinutes);
|
|
39
|
+
return this.fetchResult(queryId);
|
|
29
40
|
}
|
|
30
41
|
castValueToType(value, type) {
|
|
31
42
|
if (value === null)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|