@speedkit/cli 4.26.0 → 4.27.1
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 +17 -0
- package/README.md +203 -15
- package/dist/commands/auto-prewarm.d.ts +12 -1
- package/dist/commands/auto-prewarm.js +26 -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 +27 -0
- package/dist/commands/query/parameter.js +28 -0
- package/dist/commands/query/prewarm.d.ts +32 -0
- package/dist/commands/query/prewarm.js +32 -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 +106 -0
- package/dist/models/cli-parameters.js +183 -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-factory.d.ts +4 -1
- package/dist/services/athena/athena-service-factory.js +9 -3
- package/dist/services/athena/athena-service.d.ts +13 -1
- package/dist/services/athena/athena-service.js +27 -5
- package/dist/services/athena/athena-service.spec.d.ts +1 -0
- package/dist/services/athena/athena-service.spec.js +93 -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 +20 -2
- package/dist/services/prewarm/auto-pre-warm-factory.js +24 -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 +36 -8
- package/dist/services/query-builder/query-builder-model.d.ts +33 -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 +27 -0
- package/dist/services/query-builder/query-command.js +36 -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 +401 -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,159 @@
|
|
|
1
|
+
import { DFA, isDisjointWith, JS, NFA } from "refa";
|
|
2
|
+
import { escapeRegexLiteral } from "../sql/index.js";
|
|
3
|
+
import { matchesExactly, } from "./rule-model.js";
|
|
4
|
+
/**
|
|
5
|
+
* The language of one condition — the union of its entries, since one matching
|
|
6
|
+
* entry satisfies the condition.
|
|
7
|
+
*
|
|
8
|
+
* Returns `undefined` when an entry has no automaton at all, and the caller
|
|
9
|
+
* then leaves the rule out of every reduction.
|
|
10
|
+
*/
|
|
11
|
+
export function conditionLanguage(condition, ruleSetVersion, bound) {
|
|
12
|
+
// The reducer asks the same condition the same question many times over,
|
|
13
|
+
// once per rule or entry it is compared with, and building an automaton is
|
|
14
|
+
// the expensive part. Callers only ever read the result.
|
|
15
|
+
const cacheKey = `${ruleSetVersion}:${bound}`;
|
|
16
|
+
const cached = cache.get(condition);
|
|
17
|
+
if (cached?.has(cacheKey)) {
|
|
18
|
+
return cached.get(cacheKey);
|
|
19
|
+
}
|
|
20
|
+
const language = buildConditionLanguage(condition, ruleSetVersion, bound);
|
|
21
|
+
if (cached) {
|
|
22
|
+
cached.set(cacheKey, language);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
cache.set(condition, new Map([[cacheKey, language]]));
|
|
26
|
+
}
|
|
27
|
+
return language;
|
|
28
|
+
}
|
|
29
|
+
/** Automata already built, per condition. Never handed out for mutation. */
|
|
30
|
+
const cache = new WeakMap();
|
|
31
|
+
function buildConditionLanguage(condition, ruleSetVersion, bound) {
|
|
32
|
+
let language;
|
|
33
|
+
for (const pattern of condition.patterns) {
|
|
34
|
+
const entry = patternLanguage(pattern, ruleSetVersion, bound);
|
|
35
|
+
if (!entry) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
if (!language) {
|
|
39
|
+
language = entry;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Automata built for different character ranges cannot be combined; that
|
|
43
|
+
// happens when one pattern is unicode-aware and another is not.
|
|
44
|
+
if (language.maxCharacter !== entry.maxCharacter) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const united = attempt(() => {
|
|
48
|
+
language?.union(entry);
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
if (!united) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// The union of many patterns is where determinisation gets expensive, and
|
|
56
|
+
// its size is what decides that cost — so the same cap applies to it.
|
|
57
|
+
return language && language.countNodes() > MAX_NODES ? undefined : language;
|
|
58
|
+
}
|
|
59
|
+
/** The complement of an `atMost` language. */
|
|
60
|
+
export function outsideOf(language) {
|
|
61
|
+
return attempt(() => {
|
|
62
|
+
const complement = DFA.fromFA(language);
|
|
63
|
+
complement.complement();
|
|
64
|
+
return complement;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* True when the language stays inside the one `outside` was built from —
|
|
69
|
+
* `language ⊆ superset ⟺ language ∩ ¬superset = ∅`.
|
|
70
|
+
*
|
|
71
|
+
* @param language - has to be an `atLeast` language.
|
|
72
|
+
* @param outside - has to come from {@link outsideOf} on an `atMost` language.
|
|
73
|
+
*/
|
|
74
|
+
export function isInside(language, outside) {
|
|
75
|
+
if (language.maxCharacter !== outside.maxCharacter) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return attempt(() => isDisjointWith(language, outside));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* True when no subject is accepted by both languages. Both have to be
|
|
82
|
+
* `atLeast` languages.
|
|
83
|
+
*/
|
|
84
|
+
export function languagesDisjoint(left, right) {
|
|
85
|
+
if (left.maxCharacter !== right.maxCharacter) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return attempt(() => isDisjointWith(left, right));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The language of a single condition entry.
|
|
92
|
+
*
|
|
93
|
+
* The automaton describes *whole* subjects, so where the pattern is not
|
|
94
|
+
* anchored it is padded: an unanchored JavaScript pattern matches anywhere in
|
|
95
|
+
* the subject, and a version 1 string is a prefix of it.
|
|
96
|
+
*/
|
|
97
|
+
function patternLanguage(pattern, ruleSetVersion, bound) {
|
|
98
|
+
if (pattern.kind === "string") {
|
|
99
|
+
const value = escapeRegexLiteral(pattern.value);
|
|
100
|
+
const source = matchesExactly(ruleSetVersion) ? value : `${value}${ANY}`;
|
|
101
|
+
// A string condition lower-cases both sides, which the `i` flag mirrors.
|
|
102
|
+
return build(source, "i", bound);
|
|
103
|
+
}
|
|
104
|
+
const { flags, source } = pattern.value;
|
|
105
|
+
const anchoredStart = source.startsWith("^");
|
|
106
|
+
const anchoredEnd = /(?<!\\)\$$/.test(source);
|
|
107
|
+
const inner = source.slice(anchoredStart ? 1 : 0, anchoredEnd ? -1 : undefined);
|
|
108
|
+
return build(`${anchoredStart ? "" : ANY}(?:${inner})${anchoredEnd ? "" : ANY}`,
|
|
109
|
+
// A global or sticky flag says nothing about the language.
|
|
110
|
+
flags.replaceAll(/[gy]/g, ""), bound);
|
|
111
|
+
}
|
|
112
|
+
/** Matches any subject, used to pad an unanchored pattern. */
|
|
113
|
+
const ANY = String.raw `[\s\S]*`;
|
|
114
|
+
/**
|
|
115
|
+
* The most states an automaton may have to take part in a reduction.
|
|
116
|
+
*
|
|
117
|
+
* Comparing two automata costs far more than building them, and past a certain
|
|
118
|
+
* size that cost falls off a cliff. Measured over the configs in the
|
|
119
|
+
* onboarding repo: at 200 the slowest customer builds its query in 1.0s, at
|
|
120
|
+
* 250 the same customer takes 9.7s, and none of them reduces any better for
|
|
121
|
+
* it. The largest condition that ever carried a reduction has 189 states,
|
|
122
|
+
* which is what this leaves room for.
|
|
123
|
+
*
|
|
124
|
+
* Refusing an automaton only costs a reduction — the rule stays in the query,
|
|
125
|
+
* and the query stays correct — so the cap is safe to tighten and expensive to
|
|
126
|
+
* raise.
|
|
127
|
+
*/
|
|
128
|
+
const MAX_NODES = 200;
|
|
129
|
+
function build(source, flags, bound) {
|
|
130
|
+
return attempt(() => {
|
|
131
|
+
const { expression, maxCharacter } = JS.Parser.fromLiteral({
|
|
132
|
+
flags,
|
|
133
|
+
source,
|
|
134
|
+
}).parse({
|
|
135
|
+
// An assertion left inside the pattern is widened or narrowed, whichever
|
|
136
|
+
// keeps the caller's answer sound.
|
|
137
|
+
assertions: bound === "atLeast" ? "ignore" : "disable",
|
|
138
|
+
backreferences: "throw",
|
|
139
|
+
});
|
|
140
|
+
const nfa = NFA.fromRegex(expression, { maxCharacter });
|
|
141
|
+
return nfa.countNodes() > MAX_NODES ? undefined : nfa;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Runs an automaton operation, or gives up on it.
|
|
146
|
+
*
|
|
147
|
+
* Every step can fail on a pattern that is legal but too much for an
|
|
148
|
+
* automaton — a back reference, or a determinisation that would need more
|
|
149
|
+
* states than `refa` allows. Failing means "undecidable", which costs a
|
|
150
|
+
* reduction and never a correct query.
|
|
151
|
+
*/
|
|
152
|
+
function attempt(operation) {
|
|
153
|
+
try {
|
|
154
|
+
return operation();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ConditionKey } from "@speedkit/rules";
|
|
2
|
+
/**
|
|
3
|
+
* The rule shapes of a Speed Kit config, normalized into something the query
|
|
4
|
+
* builder can reason about.
|
|
5
|
+
*
|
|
6
|
+
* A `SpeedKitRule` in a config is loose: a condition may be a string, a
|
|
7
|
+
* regular expression or an array of both, a rule may carry criteria that have
|
|
8
|
+
* nothing to do with the URL, and the rule-set version decides what a string
|
|
9
|
+
* even means. Everything downstream — relevance, reduction, translation —
|
|
10
|
+
* works on the types here instead of on the raw config.
|
|
11
|
+
*/
|
|
12
|
+
/** The rule-set version a config runs under when it declares none. */
|
|
13
|
+
export declare const DEFAULT_RULE_SET_VERSION = 1;
|
|
14
|
+
/** Content types a page impression can have. */
|
|
15
|
+
export declare const DOCUMENT_CONTENT_TYPES: readonly ["document", "navigate"];
|
|
16
|
+
/** The condition keys naming a part of the URL a query can match on. */
|
|
17
|
+
export declare const URL_CONDITION_KEYS: readonly [ConditionKey.Host, ConditionKey.Pathname, ConditionKey.Url];
|
|
18
|
+
/**
|
|
19
|
+
* Criteria of a `SpeedKitRule` that a page impression does not carry, so they
|
|
20
|
+
* cannot be part of a query. A rule using one of these is either approximated
|
|
21
|
+
* (when it also has URL conditions) or reported as untranslatable.
|
|
22
|
+
*
|
|
23
|
+
* `hashparam` is among them: the runtime matches it against the fragment,
|
|
24
|
+
* which is not part of a URL the way the warehouse stores it.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CRITERIA_OUTSIDE_THE_URL: ConditionKey[];
|
|
27
|
+
/** The parts of a URL a rule can match on. */
|
|
28
|
+
export type RuleSubject = "host" | "pathname" | "url";
|
|
29
|
+
/** One entry of a rule condition. Entries of a condition are OR'd. */
|
|
30
|
+
export type ConditionPattern = {
|
|
31
|
+
readonly kind: "regexp";
|
|
32
|
+
readonly value: RegExp;
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: "string";
|
|
35
|
+
readonly value: string;
|
|
36
|
+
};
|
|
37
|
+
export interface RuleCondition {
|
|
38
|
+
/** OR'd — one matching entry satisfies the condition. */
|
|
39
|
+
readonly patterns: readonly ConditionPattern[];
|
|
40
|
+
readonly subject: RuleSubject;
|
|
41
|
+
}
|
|
42
|
+
export interface NormalizedRule {
|
|
43
|
+
/** AND'd — every condition of a rule has to match. */
|
|
44
|
+
readonly conditions: readonly RuleCondition[];
|
|
45
|
+
/**
|
|
46
|
+
* True when the rule can never match, whatever the URL.
|
|
47
|
+
*
|
|
48
|
+
* The runtime builds one condition per key it knows and requires all of
|
|
49
|
+
* them, so two shapes match nothing: a rule with no such key at all, and a
|
|
50
|
+
* rule whose condition is an empty list. The second one is easy to write by
|
|
51
|
+
* accident — `host: hasWebPSupport ? [] : [...]` is a real config — and
|
|
52
|
+
* reading it as "matches everything" turns a blacklist into `where false`.
|
|
53
|
+
*/
|
|
54
|
+
readonly matchesNothing: boolean;
|
|
55
|
+
/** Undefined means the rule applies to every content type. */
|
|
56
|
+
readonly contentTypes?: readonly string[];
|
|
57
|
+
/** Criteria that were present but cannot be expressed over a URL. */
|
|
58
|
+
readonly criteriaOutsideTheUrl: readonly string[];
|
|
59
|
+
/** Version this rule is parsed under; a rule may override the config's. */
|
|
60
|
+
readonly ruleSetVersion: number;
|
|
61
|
+
/** Where the rule came from, e.g. `disabledSites[1]`. For reporting. */
|
|
62
|
+
readonly source: string;
|
|
63
|
+
}
|
|
64
|
+
export interface StripParameterRule {
|
|
65
|
+
/** Pages the stripping is limited to, if any. */
|
|
66
|
+
readonly enabledSites?: readonly NormalizedRule[];
|
|
67
|
+
/** OR'd parameter-name patterns. */
|
|
68
|
+
readonly params: readonly ConditionPattern[];
|
|
69
|
+
/** Resources the stripping is limited to, if any. */
|
|
70
|
+
readonly rules?: readonly NormalizedRule[];
|
|
71
|
+
/** Decides whether a parameter name matches exactly or as a prefix. */
|
|
72
|
+
readonly ruleSetVersion: number;
|
|
73
|
+
readonly source: string;
|
|
74
|
+
}
|
|
75
|
+
export interface ParsedRuleSets {
|
|
76
|
+
readonly blacklist: readonly NormalizedRule[];
|
|
77
|
+
/**
|
|
78
|
+
* `disabledScopes`, as rules. Not a rule set in the config — a list of path
|
|
79
|
+
* prefixes the service worker must never claim — but it decides which pages
|
|
80
|
+
* Speed Kit touches just like `disabledSites` does, so the query treats it
|
|
81
|
+
* the same way.
|
|
82
|
+
*/
|
|
83
|
+
readonly disabledScopes: readonly NormalizedRule[];
|
|
84
|
+
readonly disabledSites: readonly NormalizedRule[];
|
|
85
|
+
readonly enabledSites: readonly NormalizedRule[];
|
|
86
|
+
readonly ruleSetVersion: number;
|
|
87
|
+
readonly stripQueryParams: readonly StripParameterRule[];
|
|
88
|
+
readonly whitelist: readonly NormalizedRule[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The subject a condition is really matched against. A `pathname` condition
|
|
92
|
+
* reads the query string too unless the rule set is version 2, which makes it
|
|
93
|
+
* a different subject — and one a query must not compare against the other.
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolvedSubject(subject: RuleSubject, ruleSetVersion: number): ResolvedSubject;
|
|
96
|
+
/** The subjects a query provides, after the version has been resolved. */
|
|
97
|
+
export type ResolvedSubject = "host" | "path" | "pathWithSearch" | "url";
|
|
98
|
+
/**
|
|
99
|
+
* True when the version matches parameter names and string conditions
|
|
100
|
+
* exactly. Version 1 treats them as prefixes and also feeds the query string
|
|
101
|
+
* into `pathname` conditions.
|
|
102
|
+
*
|
|
103
|
+
* The comparison is exact, not `>= 2`: `testCondition` in `@speedkit/rules`
|
|
104
|
+
* checks the version with `=== 2`, so a config declaring anything else falls
|
|
105
|
+
* back to the version 1 behaviour.
|
|
106
|
+
*/
|
|
107
|
+
export declare function matchesExactly(ruleSetVersion: number): boolean;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ConditionKey } from "@speedkit/rules";
|
|
2
|
+
/**
|
|
3
|
+
* The rule shapes of a Speed Kit config, normalized into something the query
|
|
4
|
+
* builder can reason about.
|
|
5
|
+
*
|
|
6
|
+
* A `SpeedKitRule` in a config is loose: a condition may be a string, a
|
|
7
|
+
* regular expression or an array of both, a rule may carry criteria that have
|
|
8
|
+
* nothing to do with the URL, and the rule-set version decides what a string
|
|
9
|
+
* even means. Everything downstream — relevance, reduction, translation —
|
|
10
|
+
* works on the types here instead of on the raw config.
|
|
11
|
+
*/
|
|
12
|
+
/** The rule-set version a config runs under when it declares none. */
|
|
13
|
+
export const DEFAULT_RULE_SET_VERSION = 1;
|
|
14
|
+
/** Content types a page impression can have. */
|
|
15
|
+
export const DOCUMENT_CONTENT_TYPES = ["document", "navigate"];
|
|
16
|
+
/** The condition keys naming a part of the URL a query can match on. */
|
|
17
|
+
export const URL_CONDITION_KEYS = [
|
|
18
|
+
ConditionKey.Host,
|
|
19
|
+
ConditionKey.Pathname,
|
|
20
|
+
ConditionKey.Url,
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Criteria of a `SpeedKitRule` that a page impression does not carry, so they
|
|
24
|
+
* cannot be part of a query. A rule using one of these is either approximated
|
|
25
|
+
* (when it also has URL conditions) or reported as untranslatable.
|
|
26
|
+
*
|
|
27
|
+
* `hashparam` is among them: the runtime matches it against the fragment,
|
|
28
|
+
* which is not part of a URL the way the warehouse stores it.
|
|
29
|
+
*/
|
|
30
|
+
export const CRITERIA_OUTSIDE_THE_URL = Object.values(ConditionKey).filter((key) => key !== ConditionKey.ContentType &&
|
|
31
|
+
!URL_CONDITION_KEYS.includes(key));
|
|
32
|
+
/**
|
|
33
|
+
* The subject a condition is really matched against. A `pathname` condition
|
|
34
|
+
* reads the query string too unless the rule set is version 2, which makes it
|
|
35
|
+
* a different subject — and one a query must not compare against the other.
|
|
36
|
+
*/
|
|
37
|
+
export function resolvedSubject(subject, ruleSetVersion) {
|
|
38
|
+
if (subject !== "pathname") {
|
|
39
|
+
return subject;
|
|
40
|
+
}
|
|
41
|
+
return matchesExactly(ruleSetVersion) ? "path" : "pathWithSearch";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* True when the version matches parameter names and string conditions
|
|
45
|
+
* exactly. Version 1 treats them as prefixes and also feeds the query string
|
|
46
|
+
* into `pathname` conditions.
|
|
47
|
+
*
|
|
48
|
+
* The comparison is exact, not `>= 2`: `testCondition` in `@speedkit/rules`
|
|
49
|
+
* checks the version with `=== 2`, so a config declaring anything else falls
|
|
50
|
+
* back to the version 1 behaviour.
|
|
51
|
+
*/
|
|
52
|
+
export function matchesExactly(ruleSetVersion) {
|
|
53
|
+
return ruleSetVersion === 2;
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ConditionPattern, NormalizedRule, ParsedRuleSets, StripParameterRule } from "./rule-model.js";
|
|
2
|
+
/** Reads the rule sets a query is built from out of an evaluated config. */
|
|
3
|
+
export declare function parseRuleSets(config: unknown): ParsedRuleSets;
|
|
4
|
+
/** The declared rule-set version, or 1 for a config that declares none. */
|
|
5
|
+
export declare function parseRuleSetVersion(config: unknown): number;
|
|
6
|
+
/** Normalizes a `SpeedKitRule[]` — anything else yields no rules. */
|
|
7
|
+
export declare function parseRules(value: unknown, source: string, ruleSetVersion: number): NormalizedRule[];
|
|
8
|
+
/**
|
|
9
|
+
* Normalizes a `Condition` — a string, a regular expression, or an array of
|
|
10
|
+
* both — into a flat list of patterns.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseCondition(value: unknown): ConditionPattern[];
|
|
13
|
+
/**
|
|
14
|
+
* Turns `disabledScopes` into rules.
|
|
15
|
+
*
|
|
16
|
+
* A scope is a service-worker scope, not a Speed Kit condition: a
|
|
17
|
+
* case-sensitive path prefix, matched on the URL string with no regular
|
|
18
|
+
* expression and no wildcard, so `/checkout` covers `/checkout/payment` and
|
|
19
|
+
* `/checkoutTunnel` but not `/en/checkout`. Written as an anchored pattern it
|
|
20
|
+
* says exactly that, and it can then be read, reduced and translated like
|
|
21
|
+
* every other rule.
|
|
22
|
+
*
|
|
23
|
+
* Pages under a scope are served by an empty service worker, so Speed Kit
|
|
24
|
+
* never touches them — they belong in neither a pre-warm list nor a parameter
|
|
25
|
+
* analysis.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseDisabledScopes(value: unknown): NormalizedRule[];
|
|
28
|
+
/**
|
|
29
|
+
* Normalizes `stripQueryParams`, which is either a bare condition (every
|
|
30
|
+
* parameter matching it is stripped everywhere) or a list of rules that scope
|
|
31
|
+
* the stripping to certain pages or resources.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseStripQueryParams(value: unknown, ruleSetVersion: number): StripParameterRule[];
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { types } from "node:util";
|
|
2
|
+
import { ConditionKey } from "@speedkit/rules";
|
|
3
|
+
import { escapeRegexLiteral } from "../sql/index.js";
|
|
4
|
+
import { CRITERIA_OUTSIDE_THE_URL, DEFAULT_RULE_SET_VERSION, } from "./rule-model.js";
|
|
5
|
+
const SUBJECTS = ["host", "pathname", "url"];
|
|
6
|
+
/** Reads the rule sets a query is built from out of an evaluated config. */
|
|
7
|
+
export function parseRuleSets(config) {
|
|
8
|
+
const speedKitConfig = asRecord(config) ?? {};
|
|
9
|
+
const ruleSetVersion = parseRuleSetVersion(speedKitConfig);
|
|
10
|
+
return {
|
|
11
|
+
blacklist: parseRules(speedKitConfig.blacklist, "blacklist", ruleSetVersion),
|
|
12
|
+
disabledScopes: parseDisabledScopes(speedKitConfig.disabledScopes),
|
|
13
|
+
disabledSites: parseRules(speedKitConfig.disabledSites, "disabledSites", ruleSetVersion),
|
|
14
|
+
enabledSites: parseRules(speedKitConfig.enabledSites, "enabledSites", ruleSetVersion),
|
|
15
|
+
ruleSetVersion,
|
|
16
|
+
stripQueryParams: parseStripQueryParams(speedKitConfig.stripQueryParams, ruleSetVersion),
|
|
17
|
+
whitelist: parseRules(speedKitConfig.whitelist, "whitelist", ruleSetVersion),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** The declared rule-set version, or 1 for a config that declares none. */
|
|
21
|
+
export function parseRuleSetVersion(config) {
|
|
22
|
+
const ruleSetConfig = asRecord(asRecord(config)?.ruleSetConfig);
|
|
23
|
+
const version = Number(ruleSetConfig?.version);
|
|
24
|
+
return Number.isFinite(version) && version > 0
|
|
25
|
+
? version
|
|
26
|
+
: DEFAULT_RULE_SET_VERSION;
|
|
27
|
+
}
|
|
28
|
+
/** Normalizes a `SpeedKitRule[]` — anything else yields no rules. */
|
|
29
|
+
export function parseRules(value, source, ruleSetVersion) {
|
|
30
|
+
if (!Array.isArray(value)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const rules = [];
|
|
34
|
+
for (const [index, entry] of value.entries()) {
|
|
35
|
+
const rule = parseRule(entry, `${source}[${index}]`, ruleSetVersion);
|
|
36
|
+
if (rule) {
|
|
37
|
+
rules.push(rule);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return rules;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Normalizes a `Condition` — a string, a regular expression, or an array of
|
|
44
|
+
* both — into a flat list of patterns.
|
|
45
|
+
*/
|
|
46
|
+
export function parseCondition(value) {
|
|
47
|
+
if (value === undefined || value === null) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
51
|
+
const patterns = [];
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
// The config is evaluated in a vm context, so its regular expressions come
|
|
54
|
+
// from another realm: `instanceof RegExp` is false for them here, and it
|
|
55
|
+
// is false again in `testCondition`. Rebuilding them in this realm is what
|
|
56
|
+
// makes every later check see a regular expression.
|
|
57
|
+
if (types.isRegExp(entry)) {
|
|
58
|
+
const { flags, source } = entry;
|
|
59
|
+
patterns.push({ kind: "regexp", value: new RegExp(source, flags) });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (typeof entry === "string" || typeof entry === "number") {
|
|
63
|
+
patterns.push({ kind: "string", value: String(entry) });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return patterns;
|
|
67
|
+
}
|
|
68
|
+
function parseRule(value, source, ruleSetVersion) {
|
|
69
|
+
const rule = asRecord(value);
|
|
70
|
+
if (!rule) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const conditions = [];
|
|
74
|
+
for (const subject of SUBJECTS) {
|
|
75
|
+
const patterns = parseCondition(rule[subject]);
|
|
76
|
+
if (patterns.length > 0) {
|
|
77
|
+
conditions.push({ patterns, subject });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const version = Number(rule.ruleSetVersion);
|
|
81
|
+
const keys = Object.values(ConditionKey).filter((key) => rule[key] !== undefined);
|
|
82
|
+
return {
|
|
83
|
+
conditions,
|
|
84
|
+
matchesNothing: keys.length === 0 ||
|
|
85
|
+
// An empty condition is satisfied by nothing, and every condition of a
|
|
86
|
+
// rule has to hold.
|
|
87
|
+
keys.some((key) => Array.isArray(rule[key]) && rule[key].length === 0),
|
|
88
|
+
contentTypes: parseContentTypes(rule.contentType),
|
|
89
|
+
criteriaOutsideTheUrl: CRITERIA_OUTSIDE_THE_URL.filter((criterion) => rule[criterion] !== undefined),
|
|
90
|
+
ruleSetVersion: Number.isFinite(version) && version > 0 ? version : ruleSetVersion,
|
|
91
|
+
source,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function parseContentTypes(value) {
|
|
95
|
+
if (value === undefined || value === null) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
99
|
+
const contentTypes = entries
|
|
100
|
+
.filter((entry) => typeof entry === "string")
|
|
101
|
+
.map((entry) => entry.toLowerCase());
|
|
102
|
+
return contentTypes.length > 0 ? contentTypes : undefined;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Turns `disabledScopes` into rules.
|
|
106
|
+
*
|
|
107
|
+
* A scope is a service-worker scope, not a Speed Kit condition: a
|
|
108
|
+
* case-sensitive path prefix, matched on the URL string with no regular
|
|
109
|
+
* expression and no wildcard, so `/checkout` covers `/checkout/payment` and
|
|
110
|
+
* `/checkoutTunnel` but not `/en/checkout`. Written as an anchored pattern it
|
|
111
|
+
* says exactly that, and it can then be read, reduced and translated like
|
|
112
|
+
* every other rule.
|
|
113
|
+
*
|
|
114
|
+
* Pages under a scope are served by an empty service worker, so Speed Kit
|
|
115
|
+
* never touches them — they belong in neither a pre-warm list nor a parameter
|
|
116
|
+
* analysis.
|
|
117
|
+
*/
|
|
118
|
+
export function parseDisabledScopes(value) {
|
|
119
|
+
if (!Array.isArray(value)) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const rules = [];
|
|
123
|
+
for (const [index, entry] of value.entries()) {
|
|
124
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
rules.push({
|
|
128
|
+
conditions: [
|
|
129
|
+
{
|
|
130
|
+
patterns: [
|
|
131
|
+
{
|
|
132
|
+
kind: "regexp",
|
|
133
|
+
value: new RegExp(`^${escapeRegexLiteral(entry)}`),
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
subject: "pathname",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
criteriaOutsideTheUrl: [],
|
|
140
|
+
matchesNothing: false,
|
|
141
|
+
// Version 2 keeps the query string out of the subject, which is what a
|
|
142
|
+
// path prefix is matched against.
|
|
143
|
+
ruleSetVersion: 2,
|
|
144
|
+
source: `disabledScopes[${index}]`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return rules;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Normalizes `stripQueryParams`, which is either a bare condition (every
|
|
151
|
+
* parameter matching it is stripped everywhere) or a list of rules that scope
|
|
152
|
+
* the stripping to certain pages or resources.
|
|
153
|
+
*/
|
|
154
|
+
export function parseStripQueryParams(value, ruleSetVersion) {
|
|
155
|
+
if (value === undefined || value === null) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
159
|
+
// A bare condition: strings and regular expressions, no scoping.
|
|
160
|
+
const bare = entries.filter((entry) => typeof entry === "string" || entry instanceof RegExp);
|
|
161
|
+
if (bare.length === entries.length) {
|
|
162
|
+
const params = parseCondition(bare);
|
|
163
|
+
return params.length > 0
|
|
164
|
+
? [{ params, ruleSetVersion, source: "stripQueryParams" }]
|
|
165
|
+
: [];
|
|
166
|
+
}
|
|
167
|
+
const rules = [];
|
|
168
|
+
for (const [index, entry] of entries.entries()) {
|
|
169
|
+
const rule = asRecord(entry);
|
|
170
|
+
const params = parseCondition(rule?.params);
|
|
171
|
+
if (!rule || params.length === 0) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const source = `stripQueryParams[${index}]`;
|
|
175
|
+
rules.push({
|
|
176
|
+
enabledSites: parseRules(rule.enabledSites, `${source}.enabledSites`, ruleSetVersion),
|
|
177
|
+
params,
|
|
178
|
+
rules: parseRules(rule.rules, `${source}.rules`, ruleSetVersion),
|
|
179
|
+
ruleSetVersion,
|
|
180
|
+
source,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return rules;
|
|
184
|
+
}
|
|
185
|
+
function asRecord(value) {
|
|
186
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
187
|
+
? value
|
|
188
|
+
: undefined;
|
|
189
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { NormalizedRule } from "./rule-model.js";
|
|
2
|
+
import { RuleNote } from "./rule-relevance.js";
|
|
3
|
+
/**
|
|
4
|
+
* How much comparing the reductions may do in total.
|
|
5
|
+
*
|
|
6
|
+
* Deciding one pattern against another is cheap for the patterns configs
|
|
7
|
+
* normally use, but a handful of them build automata whose determinisation
|
|
8
|
+
* costs seconds. Reducing is an optimisation — a query that keeps a redundant
|
|
9
|
+
* rule is still correct — so the work is capped.
|
|
10
|
+
*
|
|
11
|
+
* The cap counts comparisons rather than milliseconds. A clock makes the
|
|
12
|
+
* result depend on how busy the machine was: the same config would reduce
|
|
13
|
+
* further on a quiet laptop than in CI, and two runs would disagree about a
|
|
14
|
+
* query nobody changed. Counting the work makes the output a function of the
|
|
15
|
+
* config alone, which is what a golden query and a snapshot diff need.
|
|
16
|
+
*/
|
|
17
|
+
export interface ReductionBudget {
|
|
18
|
+
/** True once no comparison is left. */
|
|
19
|
+
exhausted: () => boolean;
|
|
20
|
+
/** Takes one comparison from the budget; false when there was none left. */
|
|
21
|
+
spend: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The default budget for one query build.
|
|
25
|
+
*
|
|
26
|
+
* Calibrated against the configs in the onboarding repo: a config whose rules
|
|
27
|
+
* can be reduced spends far less than this — the largest reduction observed
|
|
28
|
+
* needs a few hundred — while the pathological ones, whose automata blow up on
|
|
29
|
+
* determinisation, are stopped before the command starts to feel slow.
|
|
30
|
+
*/
|
|
31
|
+
export declare const BUDGET_COMPARISONS = 800;
|
|
32
|
+
/** A budget of that many comparisons. */
|
|
33
|
+
export declare function reductionBudget(comparisons?: number): ReductionBudget;
|
|
34
|
+
export interface ReductionResult {
|
|
35
|
+
/** What was reduced away, and what made it redundant. */
|
|
36
|
+
readonly notes: RuleNote[];
|
|
37
|
+
readonly rules: NormalizedRule[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The rules of one set, with everything a sibling already covers removed.
|
|
41
|
+
*
|
|
42
|
+
* Every rule set is a disjunction, and so is the entry list of a condition, so
|
|
43
|
+
* whatever is contained in something next to it contributes nothing: `a or b`
|
|
44
|
+
* is `b` when `a` implies `b`. Dropping it cannot change the result — it only
|
|
45
|
+
* shortens the query, which is the point.
|
|
46
|
+
*/
|
|
47
|
+
export declare function absorbRules(rules: readonly NormalizedRule[], budget?: ReductionBudget): ReductionResult;
|
|
48
|
+
/**
|
|
49
|
+
* The rules of an excluding set — `blacklist`, `disabledSites` — with
|
|
50
|
+
* everything that can never fire removed.
|
|
51
|
+
*
|
|
52
|
+
* A page has to pass every including set to reach the result at all, so an
|
|
53
|
+
* entry excluding pages none of them lets through excludes nothing. This is
|
|
54
|
+
* the bulk of what makes a generated query long: a config guards checkout,
|
|
55
|
+
* account and login while `enabledSites` only ever allowed the homepage and
|
|
56
|
+
* product pages. Configs write those guards as entries of a single rule, so
|
|
57
|
+
* the reduction works per entry and drops the rule only once a condition has
|
|
58
|
+
* no entry left that could match.
|
|
59
|
+
*
|
|
60
|
+
* @param reachable - The including sets, each a disjunction, all of which have
|
|
61
|
+
* to hold. An empty set means "everything" and is left out by the caller.
|
|
62
|
+
*/
|
|
63
|
+
export declare function dropUnreachableRules(rules: readonly NormalizedRule[], reachable: readonly (readonly NormalizedRule[])[], budget?: ReductionBudget): ReductionResult;
|
|
64
|
+
/**
|
|
65
|
+
* Rules of an including set that an excluding rule covers completely — pages
|
|
66
|
+
* the config means to accelerate but never will.
|
|
67
|
+
*
|
|
68
|
+
* Unlike the reductions above this changes nothing about the query: it is a
|
|
69
|
+
* finding about the config, and the caller reports it.
|
|
70
|
+
*/
|
|
71
|
+
export declare function findShadowedRules(included: readonly NormalizedRule[], excluding: readonly NormalizedRule[], budget?: ReductionBudget): RuleNote[];
|