@speedkit/cli 4.25.2 → 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.
Files changed (150) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +192 -19
  3. package/dist/commands/auto-prewarm.d.ts +8 -1
  4. package/dist/commands/auto-prewarm.js +18 -7
  5. package/dist/commands/generate-pop-config.d.ts +1 -0
  6. package/dist/commands/generate-pop-config.js +9 -3
  7. package/dist/commands/prewarm.js +3 -1
  8. package/dist/commands/query/parameter.d.ts +26 -0
  9. package/dist/commands/query/parameter.js +27 -0
  10. package/dist/commands/query/prewarm.d.ts +31 -0
  11. package/dist/commands/query/prewarm.js +31 -0
  12. package/dist/commands/revalidate.js +8 -6
  13. package/dist/helpers/clipboard.d.ts +19 -0
  14. package/dist/helpers/clipboard.js +70 -0
  15. package/dist/helpers/evaluate-speed-kit-config.d.ts +8 -18
  16. package/dist/helpers/evaluate-speed-kit-config.js +8 -6
  17. package/dist/helpers/evaluate-speed-kit-config.spec.d.ts +1 -0
  18. package/dist/helpers/evaluate-speed-kit-config.spec.js +78 -0
  19. package/dist/models/cli-parameters.d.ts +73 -0
  20. package/dist/models/cli-parameters.js +151 -0
  21. package/dist/models/cli-parameters.spec.d.ts +1 -0
  22. package/dist/models/cli-parameters.spec.js +47 -0
  23. package/dist/services/athena/athena-service.d.ts +2 -0
  24. package/dist/services/athena/athena-service.js +15 -4
  25. package/dist/services/athena/athena-service.spec.d.ts +1 -0
  26. package/dist/services/athena/athena-service.spec.js +74 -0
  27. package/dist/services/deploy/handler/install-resource-handler.js +3 -4
  28. package/dist/services/onboarding/onboarding-service-factory.js +2 -2
  29. package/dist/services/pop-config/pop-config-service.d.ts +8 -1
  30. package/dist/services/pop-config/pop-config-service.js +16 -28
  31. package/dist/services/prewarm/assets/asset-api-client.d.ts +1 -1
  32. package/dist/services/prewarm/assets/asset-api-client.js +5 -4
  33. package/dist/services/prewarm/auto-pre-warm-factory.d.ts +12 -2
  34. package/dist/services/prewarm/auto-pre-warm-factory.js +20 -6
  35. package/dist/services/prewarm/csv-reader.d.ts +21 -2
  36. package/dist/services/prewarm/csv-reader.js +71 -9
  37. package/dist/services/prewarm/csv-reader.spec.d.ts +1 -0
  38. package/dist/services/prewarm/csv-reader.spec.js +75 -0
  39. package/dist/services/prewarm/index.d.ts +1 -0
  40. package/dist/services/prewarm/index.js +1 -0
  41. package/dist/services/prewarm/pre-warm-factory.d.ts +0 -2
  42. package/dist/services/prewarm/pre-warm-factory.js +8 -11
  43. package/dist/services/prewarm/pre-warm-model.d.ts +14 -1
  44. package/dist/services/prewarm/pre-warm-model.js +0 -1
  45. package/dist/services/prewarm/pre-warm-service.d.ts +9 -4
  46. package/dist/services/prewarm/pre-warm-service.js +18 -14
  47. package/dist/services/prewarm/prewarm-targets.d.ts +12 -0
  48. package/dist/services/prewarm/prewarm-targets.js +18 -0
  49. package/dist/services/prewarm/prewarm-targets.spec.d.ts +1 -0
  50. package/dist/services/prewarm/prewarm-targets.spec.js +29 -0
  51. package/dist/services/query-builder/error/match-matches-nothing-error.d.ts +11 -0
  52. package/dist/services/query-builder/error/match-matches-nothing-error.js +17 -0
  53. package/dist/services/query-builder/queries/device.d.ts +21 -0
  54. package/dist/services/query-builder/queries/device.js +26 -0
  55. package/dist/services/query-builder/queries/index.d.ts +6 -0
  56. package/dist/services/query-builder/queries/index.js +6 -0
  57. package/dist/services/query-builder/queries/page-filter.d.ts +39 -0
  58. package/dist/services/query-builder/queries/page-filter.js +135 -0
  59. package/dist/services/query-builder/queries/page-source.d.ts +30 -0
  60. package/dist/services/query-builder/queries/page-source.js +79 -0
  61. package/dist/services/query-builder/queries/parameter-query.d.ts +14 -0
  62. package/dist/services/query-builder/queries/parameter-query.js +142 -0
  63. package/dist/services/query-builder/queries/pops-query.d.ts +27 -0
  64. package/dist/services/query-builder/queries/pops-query.js +65 -0
  65. package/dist/services/query-builder/queries/prewarm-query.d.ts +18 -0
  66. package/dist/services/query-builder/queries/prewarm-query.js +227 -0
  67. package/dist/services/query-builder/queries/query-model.d.ts +97 -0
  68. package/dist/services/query-builder/queries/query-model.js +19 -0
  69. package/dist/services/query-builder/queries/spec/golden-configs.d.ts +18 -0
  70. package/dist/services/query-builder/queries/spec/golden-configs.js +116 -0
  71. package/dist/services/query-builder/queries/spec/golden.spec.d.ts +1 -0
  72. package/dist/services/query-builder/queries/spec/golden.spec.js +161 -0
  73. package/dist/services/query-builder/queries/spec/pops-query.spec.d.ts +1 -0
  74. package/dist/services/query-builder/queries/spec/pops-query.spec.js +58 -0
  75. package/dist/services/query-builder/queries/spec/queries.spec.d.ts +1 -0
  76. package/dist/services/query-builder/queries/spec/queries.spec.js +312 -0
  77. package/dist/services/query-builder/queries/spec/reported-bugs.spec.d.ts +1 -0
  78. package/dist/services/query-builder/queries/spec/reported-bugs.spec.js +86 -0
  79. package/dist/services/query-builder/query-builder-factory.d.ts +2 -2
  80. package/dist/services/query-builder/query-builder-factory.js +35 -7
  81. package/dist/services/query-builder/query-builder-model.d.ts +31 -19
  82. package/dist/services/query-builder/query-builder-model.js +5 -5
  83. package/dist/services/query-builder/query-builder-service.d.ts +26 -5
  84. package/dist/services/query-builder/query-builder-service.js +157 -44
  85. package/dist/services/query-builder/query-command.d.ts +26 -0
  86. package/dist/services/query-builder/query-command.js +35 -0
  87. package/dist/services/query-builder/rules/index.d.ts +8 -0
  88. package/dist/services/query-builder/rules/index.js +8 -0
  89. package/dist/services/query-builder/rules/match-pattern.d.ts +51 -0
  90. package/dist/services/query-builder/rules/match-pattern.js +141 -0
  91. package/dist/services/query-builder/rules/pattern-language.d.ts +59 -0
  92. package/dist/services/query-builder/rules/pattern-language.js +159 -0
  93. package/dist/services/query-builder/rules/rule-model.d.ts +107 -0
  94. package/dist/services/query-builder/rules/rule-model.js +54 -0
  95. package/dist/services/query-builder/rules/rule-parser.d.ts +33 -0
  96. package/dist/services/query-builder/rules/rule-parser.js +189 -0
  97. package/dist/services/query-builder/rules/rule-reducer.d.ts +71 -0
  98. package/dist/services/query-builder/rules/rule-reducer.js +383 -0
  99. package/dist/services/query-builder/rules/rule-relevance.d.ts +43 -0
  100. package/dist/services/query-builder/rules/rule-relevance.js +69 -0
  101. package/dist/services/query-builder/rules/rule-to-sql.d.ts +51 -0
  102. package/dist/services/query-builder/rules/rule-to-sql.js +75 -0
  103. package/dist/services/query-builder/rules/spec/pattern-language.spec.d.ts +1 -0
  104. package/dist/services/query-builder/rules/spec/pattern-language.spec.js +90 -0
  105. package/dist/services/query-builder/rules/spec/rule-parser.spec.d.ts +1 -0
  106. package/dist/services/query-builder/rules/spec/rule-parser.spec.js +150 -0
  107. package/dist/services/query-builder/rules/spec/rule-reducer.spec.d.ts +1 -0
  108. package/dist/services/query-builder/rules/spec/rule-reducer.spec.js +169 -0
  109. package/dist/services/query-builder/rules/spec/rule-relevance.spec.d.ts +1 -0
  110. package/dist/services/query-builder/rules/spec/rule-relevance.spec.js +88 -0
  111. package/dist/services/query-builder/rules/spec/rule-to-sql.spec.d.ts +1 -0
  112. package/dist/services/query-builder/rules/spec/rule-to-sql.spec.js +110 -0
  113. package/dist/services/query-builder/rules/spec/runtime-semantics.spec.d.ts +1 -0
  114. package/dist/services/query-builder/rules/spec/runtime-semantics.spec.js +55 -0
  115. package/dist/services/query-builder/rules/spec/strip-parameters.spec.d.ts +1 -0
  116. package/dist/services/query-builder/rules/spec/strip-parameters.spec.js +85 -0
  117. package/dist/services/query-builder/rules/strip-parameters.d.ts +39 -0
  118. package/dist/services/query-builder/rules/strip-parameters.js +197 -0
  119. package/dist/services/query-builder/sql/expression.d.ts +73 -0
  120. package/dist/services/query-builder/sql/expression.js +133 -0
  121. package/dist/services/query-builder/sql/format.d.ts +14 -0
  122. package/dist/services/query-builder/sql/format.js +48 -0
  123. package/dist/services/query-builder/sql/index.d.ts +4 -0
  124. package/dist/services/query-builder/sql/index.js +4 -0
  125. package/dist/services/query-builder/sql/predicate.d.ts +66 -0
  126. package/dist/services/query-builder/sql/predicate.js +124 -0
  127. package/dist/services/query-builder/sql/regex.d.ts +28 -0
  128. package/dist/services/query-builder/sql/regex.js +81 -0
  129. package/dist/services/query-builder/sql/spec/expression.spec.d.ts +1 -0
  130. package/dist/services/query-builder/sql/spec/expression.spec.js +75 -0
  131. package/dist/services/query-builder/sql/spec/format.spec.d.ts +1 -0
  132. package/dist/services/query-builder/sql/spec/format.spec.js +37 -0
  133. package/dist/services/query-builder/sql/spec/predicate.spec.d.ts +1 -0
  134. package/dist/services/query-builder/sql/spec/predicate.spec.js +78 -0
  135. package/dist/services/query-builder/sql/spec/regex.spec.d.ts +1 -0
  136. package/dist/services/query-builder/sql/spec/regex.spec.js +62 -0
  137. package/dist/services/revalidate/revalidate-model.d.ts +9 -3
  138. package/dist/services/revalidate/revalidate-model.js +12 -3
  139. package/oclif.manifest.json +354 -104
  140. package/package.json +11 -2
  141. package/dist/commands/build-parameter-query.d.ts +0 -14
  142. package/dist/commands/build-parameter-query.js +0 -25
  143. package/dist/commands/build-prewarm-query.d.ts +0 -16
  144. package/dist/commands/build-prewarm-query.js +0 -37
  145. package/dist/helpers/build-query-helper.d.ts +0 -32
  146. package/dist/helpers/build-query-helper.js +0 -223
  147. package/dist/helpers/get-parsed-config.d.ts +0 -15
  148. package/dist/helpers/get-parsed-config.js +0 -62
  149. package/dist/services/onboarding/dashboard/parameter-query-builder.d.ts +0 -8
  150. package/dist/services/onboarding/dashboard/parameter-query-builder.js +0 -63
@@ -0,0 +1,51 @@
1
+ import { NormalizedRule } from "./rule-model.js";
2
+ /**
3
+ * An ad-hoc narrowing of a query, given on the command line as a regular
4
+ * expression over the path.
5
+ *
6
+ * It is compiled into the same shape a config rule has, so everything that
7
+ * follows — the check against the config, the translation to SQL, the
8
+ * warnings about constructs Athena reads differently — is the code that
9
+ * already runs for the config's own rules, and the pattern cannot come to
10
+ * mean something else here than it would in a config.
11
+ *
12
+ * The imports here are deliberately kept to the leaves of the rule layer: the
13
+ * flag definitions load this module, and pulling the automata library into
14
+ * every `sk` invocation to validate a pattern would be a poor trade.
15
+ */
16
+ /** Where a `--match` pattern shows up in reports and errors. */
17
+ export declare const MATCH_SOURCE = "--match";
18
+ /**
19
+ * Compiles one `--match` value into a regular expression.
20
+ *
21
+ * Athena carries flags inside the pattern and JavaScript carries them beside
22
+ * it. The operator writes the Athena form, so a leading `(?i)` is lifted into
23
+ * real flags here — `new RegExp('(?i)…')` would throw — and `toAthenaRegex`
24
+ * puts it back when the pattern is emitted.
25
+ *
26
+ * @throws SyntaxError when the value is not a regular expression.
27
+ */
28
+ export declare function parseMatchPattern(value: string): RegExp;
29
+ /**
30
+ * The `--match` patterns as a rule: one condition on the pathname, its
31
+ * entries OR'd, so `--match a --match b` keeps the pages matching either.
32
+ */
33
+ export declare function buildMatchRule(values: readonly string[] | undefined): NormalizedRule | undefined;
34
+ /**
35
+ * An excluding rule restated over `path + search`, so that it can be held
36
+ * against a `--match` pattern — or `undefined` when it cannot be restated
37
+ * exactly.
38
+ *
39
+ * A version 2 `pathname` condition is matched against the path alone, a
40
+ * version 1 one against the path with its query string, and the reduction
41
+ * never compares patterns across those two subjects. Since a `--match`
42
+ * pattern is a version 1 condition, a version 2 rule has to be said again in
43
+ * that subject before the two can be compared at all.
44
+ *
45
+ * This one is used to prove that a pattern is **entirely excluded**, so the
46
+ * safe direction is the opposite of the usual: a rule restated wider than it
47
+ * is would refuse a pattern that does have pages. Anything that cannot be
48
+ * restated exactly is therefore left out of the comparison altogether, and
49
+ * missing an exclusion only costs the early break.
50
+ */
51
+ export declare function asPathWithSearchRule(rule: NormalizedRule): NormalizedRule | undefined;
@@ -0,0 +1,141 @@
1
+ import { escapeRegexLiteral } from "../sql/regex.js";
2
+ import { matchesExactly, } from "./rule-model.js";
3
+ /**
4
+ * An ad-hoc narrowing of a query, given on the command line as a regular
5
+ * expression over the path.
6
+ *
7
+ * It is compiled into the same shape a config rule has, so everything that
8
+ * follows — the check against the config, the translation to SQL, the
9
+ * warnings about constructs Athena reads differently — is the code that
10
+ * already runs for the config's own rules, and the pattern cannot come to
11
+ * mean something else here than it would in a config.
12
+ *
13
+ * The imports here are deliberately kept to the leaves of the rule layer: the
14
+ * flag definitions load this module, and pulling the automata library into
15
+ * every `sk` invocation to validate a pattern would be a poor trade.
16
+ */
17
+ /** Where a `--match` pattern shows up in reports and errors. */
18
+ export const MATCH_SOURCE = "--match";
19
+ /**
20
+ * A `--match` pattern is a version 1 `pathname` condition, whatever version
21
+ * the config runs under: version 1 matches a pathname against the path *and*
22
+ * the query string, which is the subject an operator narrowing a list wants —
23
+ * `^/p/` anchors at the start of the path and `[?&]size=` reaches a
24
+ * parameter.
25
+ */
26
+ const MATCH_RULE_SET_VERSION = 1;
27
+ /** Inline flags, the way Athena takes them: `(?i)`, `(?is)`, … */
28
+ const INLINE_FLAGS = /^\(\?([ims]+)\)/;
29
+ /**
30
+ * Compiles one `--match` value into a regular expression.
31
+ *
32
+ * Athena carries flags inside the pattern and JavaScript carries them beside
33
+ * it. The operator writes the Athena form, so a leading `(?i)` is lifted into
34
+ * real flags here — `new RegExp('(?i)…')` would throw — and `toAthenaRegex`
35
+ * puts it back when the pattern is emitted.
36
+ *
37
+ * @throws SyntaxError when the value is not a regular expression.
38
+ */
39
+ export function parseMatchPattern(value) {
40
+ const inline = INLINE_FLAGS.exec(value);
41
+ return inline
42
+ ? new RegExp(value.slice(inline[0].length), inline[1])
43
+ : new RegExp(value);
44
+ }
45
+ /**
46
+ * The `--match` patterns as a rule: one condition on the pathname, its
47
+ * entries OR'd, so `--match a --match b` keeps the pages matching either.
48
+ */
49
+ export function buildMatchRule(values) {
50
+ if (!values || values.length === 0) {
51
+ return undefined;
52
+ }
53
+ const patterns = values.map((value) => ({
54
+ kind: "regexp",
55
+ value: parseMatchPattern(value),
56
+ }));
57
+ const condition = { patterns, subject: "pathname" };
58
+ return {
59
+ conditions: [condition],
60
+ criteriaOutsideTheUrl: [],
61
+ matchesNothing: false,
62
+ ruleSetVersion: MATCH_RULE_SET_VERSION,
63
+ source: MATCH_SOURCE,
64
+ };
65
+ }
66
+ /**
67
+ * An excluding rule restated over `path + search`, so that it can be held
68
+ * against a `--match` pattern — or `undefined` when it cannot be restated
69
+ * exactly.
70
+ *
71
+ * A version 2 `pathname` condition is matched against the path alone, a
72
+ * version 1 one against the path with its query string, and the reduction
73
+ * never compares patterns across those two subjects. Since a `--match`
74
+ * pattern is a version 1 condition, a version 2 rule has to be said again in
75
+ * that subject before the two can be compared at all.
76
+ *
77
+ * This one is used to prove that a pattern is **entirely excluded**, so the
78
+ * safe direction is the opposite of the usual: a rule restated wider than it
79
+ * is would refuse a pattern that does have pages. Anything that cannot be
80
+ * restated exactly is therefore left out of the comparison altogether, and
81
+ * missing an exclusion only costs the early break.
82
+ */
83
+ export function asPathWithSearchRule(rule) {
84
+ // A version 1 rule already matches the subject the pattern is written for.
85
+ if (!matchesExactly(rule.ruleSetVersion)) {
86
+ return rule;
87
+ }
88
+ const conditions = [];
89
+ for (const condition of rule.conditions) {
90
+ // A rule constraining anything but the path cannot cover the pattern in
91
+ // the first place — the reduction checks that before it builds automata.
92
+ if (condition.subject !== "pathname") {
93
+ return undefined;
94
+ }
95
+ const patterns = [];
96
+ for (const pattern of condition.patterns) {
97
+ const restated = withSearch(pattern);
98
+ if (!restated) {
99
+ return undefined;
100
+ }
101
+ patterns.push(restated);
102
+ }
103
+ conditions.push({ patterns, subject: "pathname" });
104
+ }
105
+ return { ...rule, conditions, ruleSetVersion: MATCH_RULE_SET_VERSION };
106
+ }
107
+ /** What may follow the path in `path + search`. */
108
+ const SEARCH = String.raw `(?:\?[\s\S]*)?`;
109
+ /** Anything but a single `$` at the very end. */
110
+ const INNER_END_ANCHOR = /(?<!\\)\$(?!$)/;
111
+ /**
112
+ * One entry of a version 2 path condition, said over `path + search`.
113
+ *
114
+ * Only the shapes whose meaning is unchanged by the query string are
115
+ * restated. A pattern anchored at the start says the same thing about both
116
+ * subjects; where it ends at the end of the path, the query string is spelled
117
+ * out instead. An unanchored pattern does not survive: it would match inside
118
+ * a query string the path never contained.
119
+ */
120
+ function withSearch(pattern) {
121
+ if (pattern.kind === "string") {
122
+ // A version 2 string is the whole path, compared case-insensitively.
123
+ const value = escapeRegexLiteral(pattern.value);
124
+ return { kind: "regexp", value: new RegExp(`^${value}${SEARCH}$`, "i") };
125
+ }
126
+ const { flags, source } = pattern.value;
127
+ // An alternation may hide an anchor in a branch, which the reasoning below
128
+ // does not see.
129
+ if (!source.startsWith("^") || source.includes("|")) {
130
+ return undefined;
131
+ }
132
+ if (INNER_END_ANCHOR.test(source)) {
133
+ return undefined;
134
+ }
135
+ return {
136
+ kind: "regexp",
137
+ value: /(?<!\\)\$$/.test(source)
138
+ ? new RegExp(`${source.slice(0, -1)}${SEARCH}$`, flags)
139
+ : pattern.value,
140
+ };
141
+ }
@@ -0,0 +1,59 @@
1
+ import { DFA, NFA } from "refa";
2
+ import { RuleCondition } from "./rule-model.js";
3
+ /**
4
+ * The set of subjects a condition accepts, as a finite automaton.
5
+ *
6
+ * Deciding whether one rule makes another redundant means deciding whether one
7
+ * pattern's language contains or avoids another's — a question about regular
8
+ * languages, not about strings, so it is answered with automata rather than
9
+ * guessed. `refa` builds them from the very same JavaScript patterns the config
10
+ * uses.
11
+ */
12
+ export type ConditionLanguage = NFA;
13
+ /**
14
+ * Which side of the true language an automaton is allowed to fall on.
15
+ *
16
+ * Automata cannot express an assertion — a word boundary, or the `$` inside
17
+ * `/^\/shop(\/|$)/` — so a pattern using one is approximated. Approximating in
18
+ * an arbitrary direction would let the reducer drop a rule that does matter,
19
+ * so each question is asked with the bound that makes its answer sound:
20
+ *
21
+ * - `atLeast` accepts everything the pattern accepts, and possibly more. Two
22
+ * of these sharing nothing proves the patterns share nothing.
23
+ * - `atMost` accepts only what the pattern accepts, and possibly less. An
24
+ * `atLeast` language inside an `atMost` one proves containment.
25
+ *
26
+ * A pattern without assertions has one exact language, and both bounds are it.
27
+ */
28
+ export type LanguageBound = "atLeast" | "atMost";
29
+ /**
30
+ * The language of one condition — the union of its entries, since one matching
31
+ * entry satisfies the condition.
32
+ *
33
+ * Returns `undefined` when an entry has no automaton at all, and the caller
34
+ * then leaves the rule out of every reduction.
35
+ */
36
+ export declare function conditionLanguage(condition: RuleCondition, ruleSetVersion: number, bound: LanguageBound): ConditionLanguage | undefined;
37
+ /**
38
+ * Everything a language does *not* accept.
39
+ *
40
+ * Determinising a language to complement it is the expensive step of the whole
41
+ * reduction, and one rule is compared with many others, so a caller that asks
42
+ * repeatedly should build this once and reuse it.
43
+ */
44
+ export type LanguageOutside = DFA;
45
+ /** The complement of an `atMost` language. */
46
+ export declare function outsideOf(language: ConditionLanguage): LanguageOutside | undefined;
47
+ /**
48
+ * True when the language stays inside the one `outside` was built from —
49
+ * `language ⊆ superset ⟺ language ∩ ¬superset = ∅`.
50
+ *
51
+ * @param language - has to be an `atLeast` language.
52
+ * @param outside - has to come from {@link outsideOf} on an `atMost` language.
53
+ */
54
+ export declare function isInside(language: ConditionLanguage, outside: LanguageOutside): boolean | undefined;
55
+ /**
56
+ * True when no subject is accepted by both languages. Both have to be
57
+ * `atLeast` languages.
58
+ */
59
+ export declare function languagesDisjoint(left: ConditionLanguage, right: ConditionLanguage): boolean | undefined;
@@ -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[];