@warlock.js/core 5.35.0 → 5.37.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 (133) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/esm/cli/commands/migrate.command.mjs +10 -0
  3. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  4. package/esm/cli/commands/release/release-command.action.mjs +245 -0
  5. package/esm/cli/commands/release/release-command.action.mjs.map +1 -0
  6. package/esm/cli/commands/release.command.mjs +66 -0
  7. package/esm/cli/commands/release.command.mjs.map +1 -0
  8. package/esm/cli/framework-cli-commands.mjs +2 -0
  9. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  10. package/esm/database/migrate-action.mjs +24 -2
  11. package/esm/database/migrate-action.mjs.map +1 -1
  12. package/esm/dev-server/loader/load-hook.d.mts.map +1 -1
  13. package/esm/dev-server/loader/load-hook.mjs +6 -0
  14. package/esm/dev-server/loader/load-hook.mjs.map +1 -1
  15. package/esm/dev-server/module-loader.mjs +1 -1
  16. package/esm/dev-server/parse-imports.mjs +1 -1
  17. package/esm/dev-server/type-generator.mjs +1 -1
  18. package/esm/e2e/cdp.d.mts.map +1 -1
  19. package/esm/e2e/cdp.mjs +9 -2
  20. package/esm/e2e/cdp.mjs.map +1 -1
  21. package/esm/e2e/page.d.mts +9 -1
  22. package/esm/e2e/page.d.mts.map +1 -1
  23. package/esm/e2e/page.mjs +45 -7
  24. package/esm/e2e/page.mjs.map +1 -1
  25. package/esm/e2e/types.d.mts +18 -0
  26. package/esm/e2e/types.d.mts.map +1 -1
  27. package/esm/e2e/types.mjs.map +1 -1
  28. package/esm/errors/error-codes.d.mts +24 -0
  29. package/esm/errors/error-codes.d.mts.map +1 -1
  30. package/esm/errors/error-codes.mjs +24 -0
  31. package/esm/errors/error-codes.mjs.map +1 -1
  32. package/esm/generations/features/test.feature.mjs +7 -7
  33. package/esm/generations/features/test.feature.mjs.map +1 -1
  34. package/esm/generations/features/web.feature.mjs +3 -3
  35. package/esm/generations/features/web.feature.mjs.map +1 -1
  36. package/esm/index.d.mts +6 -3
  37. package/esm/index.mjs +4 -2
  38. package/esm/localization/index.mjs +3 -1
  39. package/esm/localization/translation-usage-graph.d.mts +49 -0
  40. package/esm/localization/translation-usage-graph.d.mts.map +1 -0
  41. package/esm/localization/translation-usage-graph.mjs +128 -0
  42. package/esm/localization/translation-usage-graph.mjs.map +1 -0
  43. package/esm/localization/translation-usage.d.mts +85 -0
  44. package/esm/localization/translation-usage.d.mts.map +1 -0
  45. package/esm/localization/translation-usage.mjs +307 -0
  46. package/esm/localization/translation-usage.mjs.map +1 -0
  47. package/esm/production/build-budgets.d.mts +18 -0
  48. package/esm/production/build-budgets.d.mts.map +1 -1
  49. package/esm/production/build-budgets.mjs.map +1 -1
  50. package/esm/production/route-registration-snapshot.mjs +12 -3
  51. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  52. package/esm/release/activate.mjs +261 -0
  53. package/esm/release/activate.mjs.map +1 -0
  54. package/esm/release/candidate.mjs +107 -0
  55. package/esm/release/candidate.mjs.map +1 -0
  56. package/esm/release/cascade-migration-provider.mjs +32 -0
  57. package/esm/release/cascade-migration-provider.mjs.map +1 -0
  58. package/esm/release/config.mjs +60 -0
  59. package/esm/release/config.mjs.map +1 -0
  60. package/esm/release/gate.mjs +67 -0
  61. package/esm/release/gate.mjs.map +1 -0
  62. package/esm/release/migrations-proof.mjs +100 -0
  63. package/esm/release/migrations-proof.mjs.map +1 -0
  64. package/esm/release/prune.mjs +42 -0
  65. package/esm/release/prune.mjs.map +1 -0
  66. package/esm/release/report.mjs +25 -0
  67. package/esm/release/report.mjs.map +1 -0
  68. package/esm/release/runner.mjs +55 -0
  69. package/esm/release/runner.mjs.map +1 -0
  70. package/esm/release/semver.mjs +53 -0
  71. package/esm/release/semver.mjs.map +1 -0
  72. package/esm/release/stage.mjs +122 -0
  73. package/esm/release/stage.mjs.map +1 -0
  74. package/esm/release/store.mjs +131 -0
  75. package/esm/release/store.mjs.map +1 -0
  76. package/esm/release/types.d.mts +102 -0
  77. package/esm/release/types.d.mts.map +1 -0
  78. package/esm/release/verify.mjs +94 -0
  79. package/esm/release/verify.mjs.map +1 -0
  80. package/esm/router/index.d.mts +2 -2
  81. package/esm/router/index.mjs +2 -2
  82. package/esm/router/router.d.mts +4 -0
  83. package/esm/router/router.d.mts.map +1 -1
  84. package/esm/router/router.mjs +14 -3
  85. package/esm/router/router.mjs.map +1 -1
  86. package/esm/router/types.d.mts +45 -0
  87. package/esm/router/types.d.mts.map +1 -1
  88. package/esm/router/unguarded-route-diagnostics.d.mts +6 -0
  89. package/esm/router/unguarded-route-diagnostics.d.mts.map +1 -1
  90. package/esm/router/unguarded-route-diagnostics.mjs +10 -2
  91. package/esm/router/unguarded-route-diagnostics.mjs.map +1 -1
  92. package/esm/tooling/check/api-route-references.d.mts +16 -0
  93. package/esm/tooling/check/api-route-references.d.mts.map +1 -0
  94. package/esm/tooling/check/api-route-references.mjs +146 -0
  95. package/esm/tooling/check/api-route-references.mjs.map +1 -0
  96. package/esm/tooling/check/conventions-step.d.mts +6 -3
  97. package/esm/tooling/check/conventions-step.d.mts.map +1 -1
  98. package/esm/tooling/check/conventions-step.mjs +19 -10
  99. package/esm/tooling/check/conventions-step.mjs.map +1 -1
  100. package/esm/tooling/check/run-check.d.mts.map +1 -1
  101. package/esm/tooling/check/run-check.mjs +4 -2
  102. package/esm/tooling/check/run-check.mjs.map +1 -1
  103. package/esm/tooling/check/security-conventions.mjs +19 -1
  104. package/esm/tooling/check/security-conventions.mjs.map +1 -1
  105. package/esm/tooling/check/translation-conventions.mjs +32 -0
  106. package/esm/tooling/check/translation-conventions.mjs.map +1 -0
  107. package/esm/tooling/inspect/collect-files.mjs +1 -1
  108. package/esm/tooling/inspect/collect-routes.mjs +3 -2
  109. package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
  110. package/esm/upgrade/codemods/index.mjs +2 -0
  111. package/esm/upgrade/codemods/index.mjs.map +1 -1
  112. package/esm/upgrade/codemods/vite-vitest.mjs +170 -0
  113. package/esm/upgrade/codemods/vite-vitest.mjs.map +1 -0
  114. package/esm/upgrade/notes.mjs +4 -0
  115. package/esm/upgrade/notes.mjs.map +1 -1
  116. package/esm/validation/init.mjs +11 -4
  117. package/esm/validation/init.mjs.map +1 -1
  118. package/esm/validation/translate-rule.mjs +63 -0
  119. package/esm/validation/translate-rule.mjs.map +1 -0
  120. package/esm/warlock-config/index.d.mts +3 -2
  121. package/esm/warlock-config/types.d.mts +8 -0
  122. package/esm/warlock-config/types.d.mts.map +1 -1
  123. package/llms-full.txt +221 -18
  124. package/llms.txt +1 -0
  125. package/package.json +13 -13
  126. package/skills/deploy-release/SKILL.md +76 -0
  127. package/skills/index.md +1 -1
  128. package/skills/register-route/SKILL.md +25 -0
  129. package/skills/test-service/SKILL.md +8 -8
  130. package/skills/upgrade-from-5-29/SKILL.md +91 -0
  131. package/skills/use-localization/SKILL.md +4 -0
  132. package/skills/warlock-check/SKILL.md +11 -8
  133. package/skills/warlock-conventions/SKILL.md +1 -1
@@ -0,0 +1,307 @@
1
+ import { forEachChild, parentOf, parseSourceFile, positionOf } from "../tooling/syntax/ast.mjs";
2
+
3
+ //#region ../../packages/core/src/localization/translation-usage.ts
4
+ /** The translator functions every framework and application spelling resolves to, with the key's argument index. */
5
+ const TRANSLATOR_KEY_ARGUMENT = Object.freeze({
6
+ trans: 0,
7
+ t: 0,
8
+ transX: 0,
9
+ transFrom: 1
10
+ });
11
+ /** Factories that return a translator bound to the current request or page. */
12
+ const TRANSLATOR_FACTORIES = /* @__PURE__ */ new Set(["useTrans"]);
13
+ /** Parent node types in which a bare translator identifier is handed on as a value. */
14
+ const VALUE_POSITIONS = /* @__PURE__ */ new Set([
15
+ "CallExpression",
16
+ "NewExpression",
17
+ "JSXExpressionContainer",
18
+ "Property",
19
+ "ArrayExpression",
20
+ "ReturnStatement",
21
+ "AssignmentExpression",
22
+ "VariableDeclarator",
23
+ "ArrowFunctionExpression",
24
+ "ConditionalExpression",
25
+ "LogicalExpression",
26
+ "SpreadElement"
27
+ ]);
28
+ /** Build a usage value from parts, deduplicated and frozen. */
29
+ function usageOf(parts) {
30
+ return Object.freeze({
31
+ keys: Object.freeze([...new Set(parts.keys ?? [])]),
32
+ prefixes: Object.freeze([...new Set(parts.prefixes ?? [])]),
33
+ literals: Object.freeze([...new Set(parts.literals ?? [])]),
34
+ literalPrefixes: Object.freeze([...new Set(parts.literalPrefixes ?? [])]),
35
+ dynamic: parts.dynamic === true,
36
+ dynamicSites: Object.freeze([...parts.dynamicSites ?? []])
37
+ });
38
+ }
39
+ const EMPTY_TRANSLATION_USAGE = usageOf({});
40
+ /**
41
+ * Turn one `config.translations` entry into the usage it declares.
42
+ *
43
+ * `"statuses.*"` declares every key under `statuses.`, `"members"` declares a
44
+ * key and its subtree, and `"*"` declares everything. A wildcard anywhere but
45
+ * the final `.*`, or an empty entry, is refused rather than guessed at.
46
+ */
47
+ function translationDeclarationUsage(entry, file) {
48
+ const invalid = () => /* @__PURE__ */ new Error(`config.translations entry ${JSON.stringify(entry)} in "${file}" must be a translation key, a group, or a prefix such as "statuses.*"; a wildcard is allowed only as the final ".*".`);
49
+ if (entry.length === 0 || /\s/.test(entry)) throw invalid();
50
+ if (entry === "*") return usageOf({ prefixes: [""] });
51
+ if (entry.endsWith(".*")) {
52
+ const prefix = entry.slice(0, -1);
53
+ if (prefix.includes("*") || prefix.includes("..")) throw invalid();
54
+ return usageOf({ prefixes: [prefix] });
55
+ }
56
+ if (entry.includes("*")) throw invalid();
57
+ return usageOf({ keys: [entry] });
58
+ }
59
+ /** The union of several usages: a dynamic lookup anywhere makes the union dynamic. */
60
+ function mergeTranslationUsage(usages) {
61
+ return usageOf({
62
+ keys: usages.flatMap((usage) => usage.keys),
63
+ prefixes: usages.flatMap((usage) => usage.prefixes),
64
+ literals: usages.flatMap((usage) => usage.literals),
65
+ literalPrefixes: usages.flatMap((usage) => usage.literalPrefixes),
66
+ dynamic: usages.some((usage) => usage.dynamic),
67
+ dynamicSites: usages.flatMap((usage) => usage.dynamicSites)
68
+ });
69
+ }
70
+ /**
71
+ * Whether `key` is `declared`, lives under it, or is one of its plural
72
+ * variants. The localization runtime resolves `trans("cart.items", { count })`
73
+ * to `cart.items_one`, `cart.items_other` and the other count suffixes, so a
74
+ * name that is only a base of such keys reaches them. A suffix is one segment:
75
+ * `"cart.items_one"` matches `"cart.items"`, `"cart.items_one.note"` does not.
76
+ */
77
+ function reaches(declared, key) {
78
+ if (declared.endsWith(".")) return key.startsWith(declared);
79
+ if (key === declared || key.startsWith(`${declared}.`)) return true;
80
+ if (!key.startsWith(`${declared}_`)) return false;
81
+ return !key.includes(".", declared.length);
82
+ }
83
+ /** Whether a usage can reach `key`. A dynamic usage reaches every key. */
84
+ function isTranslationKeyUsed(usage, key) {
85
+ if (usage.dynamic) return true;
86
+ if (usage.keys.some((declared) => reaches(declared, key))) return true;
87
+ if (usage.literals.some((literal) => reaches(literal, key))) return true;
88
+ return usage.prefixes.some((prefix) => key.startsWith(prefix)) || usage.literalPrefixes.some((prefix) => key.startsWith(prefix));
89
+ }
90
+ /** Whether a string could be a translation key, a group or a key prefix: dotted word segments, no spaces. */
91
+ const KEY_SHAPE = /^[A-Za-z_$][\w$-]*(?:\.[\w$-]+)*\.?$/;
92
+ /** Longest literal considered; a real key is far shorter, and anything longer is prose or data. */
93
+ const MAX_LITERAL_LENGTH = 160;
94
+ function isKeyShaped(text) {
95
+ return text.length <= MAX_LITERAL_LENGTH && KEY_SHAPE.test(text);
96
+ }
97
+ function unwrap(node) {
98
+ let current = node;
99
+ while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") current = current.expression;
100
+ return current;
101
+ }
102
+ function combine(left, right) {
103
+ if (left === "dynamic" || right === "dynamic") return "dynamic";
104
+ if (left === "none") return right;
105
+ if (right === "none") return left;
106
+ return [...left, ...right];
107
+ }
108
+ function resolveKeyExpression(node, constants) {
109
+ if (node === void 0) return "none";
110
+ const value = unwrap(node);
111
+ if (value.type === "Literal" && typeof value.value === "string") return [{
112
+ kind: "key",
113
+ value: value.value
114
+ }];
115
+ if (value.type === "TemplateLiteral") {
116
+ const head = value.quasis[0]?.value.cooked ?? "";
117
+ if (head.length === 0) return "dynamic";
118
+ return value.expressions.length === 0 ? [{
119
+ kind: "key",
120
+ value: head
121
+ }] : [{
122
+ kind: "prefix",
123
+ value: head
124
+ }];
125
+ }
126
+ if (value.type === "Identifier") {
127
+ const constant = constants.get(value.name);
128
+ return constant === void 0 ? "dynamic" : [{
129
+ kind: "key",
130
+ value: constant
131
+ }];
132
+ }
133
+ if (value.type === "ConditionalExpression") return combine(resolveKeyExpression(value.consequent, constants), resolveKeyExpression(value.alternate, constants));
134
+ if (value.type === "LogicalExpression") return combine(resolveKeyExpression(value.left, constants), resolveKeyExpression(value.right, constants));
135
+ if (value.type === "ObjectExpression") return "none";
136
+ return "dynamic";
137
+ }
138
+ /** Top-level `const NAME = "literal"` declarations, which a key may name through an identifier. */
139
+ function moduleConstants(source) {
140
+ const constants = /* @__PURE__ */ new Map();
141
+ for (const statement of source.body) {
142
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
143
+ if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") continue;
144
+ for (const declarator of declaration.declarations) {
145
+ if (declarator.id.type !== "Identifier" || declarator.init === null) continue;
146
+ const init = unwrap(declarator.init);
147
+ if (init.type === "Literal" && typeof init.value === "string") constants.set(declarator.id.name, init.value);
148
+ else if (init.type === "TemplateLiteral" && init.expressions.length === 0) constants.set(declarator.id.name, init.quasis[0]?.value.cooked ?? "");
149
+ }
150
+ }
151
+ return constants;
152
+ }
153
+ /**
154
+ * Local names bound to a translator: the result of `useTrans()`, including an
155
+ * aliased import of it. Calling one of them is a lookup like calling `trans`.
156
+ */
157
+ function translatorBindings(source) {
158
+ const factories = new Set(TRANSLATOR_FACTORIES);
159
+ const bindings = /* @__PURE__ */ new Set();
160
+ for (const statement of source.body) {
161
+ if (statement.type !== "ImportDeclaration") continue;
162
+ for (const specifier of statement.specifiers) {
163
+ if (specifier.type !== "ImportSpecifier") continue;
164
+ const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : "";
165
+ if (TRANSLATOR_FACTORIES.has(imported)) factories.add(specifier.local.name);
166
+ }
167
+ }
168
+ const visit = (node) => {
169
+ if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.init !== null && node.init.type === "CallExpression" && node.init.callee.type === "Identifier" && factories.has(node.init.callee.name)) bindings.add(node.id.name);
170
+ forEachChild(node, visit);
171
+ };
172
+ forEachChild(source, visit);
173
+ return bindings;
174
+ }
175
+ function isTranslatorName(name, bindings) {
176
+ return Object.hasOwn(TRANSLATOR_KEY_ARGUMENT, name) || bindings.has(name);
177
+ }
178
+ /**
179
+ * Scan one source file for translation lookups and the modules it loads.
180
+ *
181
+ * Literal keys, static-prefix templates, identifiers bound to a module-level
182
+ * string constant, and choices between those (`a ? "x" : "y"`) all resolve. Any
183
+ * other key expression is `dynamic`, and so is a translator that escapes as a
184
+ * value (passed on, returned, stored): whoever receives it can call it with any
185
+ * key.
186
+ */
187
+ function scanTranslationSource(fileName, text) {
188
+ const source = parseSourceFile(fileName, text);
189
+ const constants = moduleConstants(source);
190
+ const bindings = translatorBindings(source);
191
+ const keys = [];
192
+ const prefixes = [];
193
+ const literals = [];
194
+ const literalPrefixes = [];
195
+ const dynamicSites = [];
196
+ const imports = [];
197
+ const markDynamic = (node, lookup) => {
198
+ dynamicSites.push({
199
+ file: fileName,
200
+ ...positionOf(source, node),
201
+ text: lookup
202
+ });
203
+ };
204
+ const visit = (node) => {
205
+ if (node.type === "ImportDeclaration") {
206
+ const specifiers = node.specifiers;
207
+ if (!(node.importKind === "type" || specifiers.length > 0 && specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type"))) imports.push(node.source.value);
208
+ } else if ((node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") && node.source !== null && node.exportKind !== "type") imports.push(node.source.value);
209
+ else if (node.type === "ImportExpression") {
210
+ const specifier = unwrap(node.source);
211
+ if (specifier.type === "Literal" && typeof specifier.value === "string") imports.push(specifier.value);
212
+ } else if (node.type === "CallExpression") {
213
+ const callee = unwrap(node.callee);
214
+ if (callee.type === "Identifier" && isTranslatorName(callee.name, bindings)) {
215
+ const index = TRANSLATOR_KEY_ARGUMENT[callee.name] ?? 0;
216
+ const argument = node.arguments[index];
217
+ const resolution = argument === void 0 ? "none" : argument.type === "SpreadElement" ? "dynamic" : resolveKeyExpression(argument, constants);
218
+ if (resolution === "dynamic") markDynamic(argument ?? node, argument === void 0 ? "" : sourceText(text, argument));
219
+ else if (resolution !== "none") for (const candidate of resolution) if (candidate.kind === "key") keys.push(candidate.value);
220
+ else prefixes.push(candidate.value);
221
+ }
222
+ } else if (node.type === "Identifier" && isTranslatorName(node.name, bindings)) {
223
+ const parent = parentOf(node);
224
+ if (parent !== void 0 && escapesAsValue(parent, node)) markDynamic(node, node.name);
225
+ } else if (node.type === "Literal" && typeof node.value === "string") {
226
+ if (!isModuleSpecifier(node) && isKeyShaped(node.value)) literals.push(node.value);
227
+ } else if (node.type === "TemplateLiteral") {
228
+ const head = node.quasis[0]?.value.cooked ?? "";
229
+ if (isKeyShaped(head)) (node.expressions.length === 0 ? literals : literalPrefixes).push(head);
230
+ }
231
+ forEachChild(node, visit);
232
+ };
233
+ forEachChild(source, visit);
234
+ return {
235
+ usage: usageOf({
236
+ keys,
237
+ prefixes,
238
+ literals: literals.filter((literal) => !keys.includes(literal)),
239
+ literalPrefixes: literalPrefixes.filter((literal) => !prefixes.includes(literal)),
240
+ dynamic: dynamicSites.length > 0,
241
+ dynamicSites
242
+ }),
243
+ imports: [...new Set(imports)]
244
+ };
245
+ }
246
+ /** The specifier of an import, re-export or dynamic import: a module path, never a translation key. */
247
+ function isModuleSpecifier(node) {
248
+ const parent = parentOf(node);
249
+ if (parent === void 0) return false;
250
+ switch (parent.type) {
251
+ case "ImportDeclaration":
252
+ case "ExportNamedDeclaration":
253
+ case "ExportAllDeclaration":
254
+ case "ImportExpression": return parent.source === node;
255
+ default: return false;
256
+ }
257
+ }
258
+ /** A translator identifier handed on as a value, rather than called or declared. */
259
+ function escapesAsValue(parent, node) {
260
+ if (!VALUE_POSITIONS.has(parent.type)) return false;
261
+ switch (parent.type) {
262
+ case "CallExpression":
263
+ case "NewExpression": return parent.callee !== node;
264
+ case "Property": return parent.value === node;
265
+ case "VariableDeclarator": return parent.init === node;
266
+ case "ArrowFunctionExpression": return parent.body === node;
267
+ case "AssignmentExpression": return parent.right === node;
268
+ default: return true;
269
+ }
270
+ }
271
+ function sourceText(text, node) {
272
+ return text.slice(node.start, node.end);
273
+ }
274
+ /**
275
+ * The `config.translations` declaration of a page's setup file, as its raw
276
+ * entries. Read statically, like `config.route`, so the build never runs the
277
+ * file. `undefined` when the file declares none.
278
+ */
279
+ function readTranslationDeclarations(fileName, text) {
280
+ const source = parseSourceFile(fileName, text);
281
+ const malformed = () => /* @__PURE__ */ new Error(`config.translations in "${fileName}" must be an array of string literals.`);
282
+ for (const statement of source.body) {
283
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : void 0;
284
+ if (declaration?.type !== "VariableDeclaration") continue;
285
+ for (const declarator of declaration.declarations) {
286
+ if (declarator.id.type !== "Identifier" || declarator.id.name !== "config") continue;
287
+ if (declarator.init === null) continue;
288
+ const init = unwrap(declarator.init);
289
+ if (init.type !== "ObjectExpression") continue;
290
+ for (const property of init.properties) {
291
+ if (property.type !== "Property" || property.computed) continue;
292
+ if ((property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" && typeof property.key.value === "string" ? property.key.value : void 0) !== "translations") continue;
293
+ const value = unwrap(property.value);
294
+ if (value.type !== "ArrayExpression") throw malformed();
295
+ return value.elements.map((element) => {
296
+ const literal = element === null ? void 0 : unwrap(element);
297
+ if (literal?.type !== "Literal" || typeof literal.value !== "string") throw malformed();
298
+ return literal.value;
299
+ });
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ //#endregion
306
+ export { EMPTY_TRANSLATION_USAGE, isTranslationKeyUsed, mergeTranslationUsage, readTranslationDeclarations, scanTranslationSource, translationDeclarationUsage, usageOf };
307
+ //# sourceMappingURL=translation-usage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translation-usage.mjs","names":["ast.parseSourceFile","ast.positionOf","ast.parentOf"],"sources":["../../../../../../../../packages/core/src/localization/translation-usage.ts"],"sourcesContent":["import * as ast from \"../tooling/syntax/ast\";\n\n/**\n * What one page (or one module) can look up in the translation table, derived\n * from static source. Consumers decide what to ship; this type only says which\n * keys a client render may ask for.\n *\n * - `keys` are exact keys. A key also delivers its whole subtree, so a declared\n * group (`\"members\"`) and a literal leaf (`\"members.title\"`) use one matching\n * rule.\n * - `prefixes` come from template keys with a static prefix\n * (`` trans(`statuses.${status}`) `` gives `\"statuses.\"`). Every key that\n * starts with one is reachable.\n * - `dynamic` is set when a lookup key cannot be known before render. The\n * consumer must then assume every key is reachable.\n */\nexport type TranslationUsage = {\n readonly keys: readonly string[];\n readonly prefixes: readonly string[];\n /**\n * Key-shaped string literals written anywhere in the walked modules, outside\n * a recognized lookup (a helper's argument, a constant, an object that maps to\n * keys, a prop). A literal counts as used when it equals an existing key,\n * names a group or subtree, or is the base of plural variants\n * (`\"cart.items\"` reaches `\"cart.items_one\"` and `\"cart.items_other\"`).\n */\n readonly literals: readonly string[];\n /** The static heads of template literals with substitutions, which reach every key that starts with one. */\n readonly literalPrefixes: readonly string[];\n readonly dynamic: boolean;\n /** Where each unresolvable lookup was written, for diagnostics. */\n readonly dynamicSites: readonly TranslationDynamicSite[];\n};\n\nexport type TranslationDynamicSite = {\n readonly file: string;\n readonly line: number;\n readonly column: number;\n /** The source text of the lookup's key argument, as written. */\n readonly text: string;\n};\n\n/** What one source file contributes: its own usage plus the modules it loads at run time. */\nexport type TranslationSourceScan = {\n readonly usage: TranslationUsage;\n /** Specifiers of value imports, re-exports and dynamic `import()` calls. */\n readonly imports: readonly string[];\n};\n\n/** The translator functions every framework and application spelling resolves to, with the key's argument index. */\nconst TRANSLATOR_KEY_ARGUMENT: Readonly<Record<string, number>> = Object.freeze({\n trans: 0,\n t: 0,\n transX: 0,\n transFrom: 1,\n});\n\n/** Factories that return a translator bound to the current request or page. */\nconst TRANSLATOR_FACTORIES = new Set([\"useTrans\"]);\n\n/** Parent node types in which a bare translator identifier is handed on as a value. */\nconst VALUE_POSITIONS = new Set([\n \"CallExpression\",\n \"NewExpression\",\n \"JSXExpressionContainer\",\n \"Property\",\n \"ArrayExpression\",\n \"ReturnStatement\",\n \"AssignmentExpression\",\n \"VariableDeclarator\",\n \"ArrowFunctionExpression\",\n \"ConditionalExpression\",\n \"LogicalExpression\",\n \"SpreadElement\",\n]);\n\n/** Build a usage value from parts, deduplicated and frozen. */\nexport function usageOf(parts: {\n readonly keys?: readonly string[];\n readonly prefixes?: readonly string[];\n readonly literals?: readonly string[];\n readonly literalPrefixes?: readonly string[];\n readonly dynamic?: boolean;\n readonly dynamicSites?: readonly TranslationDynamicSite[];\n}): TranslationUsage {\n return Object.freeze({\n keys: Object.freeze([...new Set(parts.keys ?? [])]),\n prefixes: Object.freeze([...new Set(parts.prefixes ?? [])]),\n literals: Object.freeze([...new Set(parts.literals ?? [])]),\n literalPrefixes: Object.freeze([...new Set(parts.literalPrefixes ?? [])]),\n dynamic: parts.dynamic === true,\n dynamicSites: Object.freeze([...(parts.dynamicSites ?? [])]),\n });\n}\n\nexport const EMPTY_TRANSLATION_USAGE: TranslationUsage = usageOf({});\n\n/**\n * Turn one `config.translations` entry into the usage it declares.\n *\n * `\"statuses.*\"` declares every key under `statuses.`, `\"members\"` declares a\n * key and its subtree, and `\"*\"` declares everything. A wildcard anywhere but\n * the final `.*`, or an empty entry, is refused rather than guessed at.\n */\nexport function translationDeclarationUsage(entry: string, file: string): TranslationUsage {\n const invalid = (): Error =>\n new Error(\n `config.translations entry ${JSON.stringify(entry)} in \"${file}\" must be a translation key, ` +\n 'a group, or a prefix such as \"statuses.*\"; a wildcard is allowed only as the final \".*\".',\n );\n\n if (entry.length === 0 || /\\s/.test(entry)) throw invalid();\n if (entry === \"*\") return usageOf({ prefixes: [\"\"] });\n\n if (entry.endsWith(\".*\")) {\n const prefix = entry.slice(0, -1);\n if (prefix.includes(\"*\") || prefix.includes(\"..\")) throw invalid();\n return usageOf({ prefixes: [prefix] });\n }\n\n if (entry.includes(\"*\")) throw invalid();\n\n return usageOf({ keys: [entry] });\n}\n\n/** The union of several usages: a dynamic lookup anywhere makes the union dynamic. */\nexport function mergeTranslationUsage(usages: readonly TranslationUsage[]): TranslationUsage {\n return usageOf({\n keys: usages.flatMap((usage) => usage.keys),\n prefixes: usages.flatMap((usage) => usage.prefixes),\n literals: usages.flatMap((usage) => usage.literals),\n literalPrefixes: usages.flatMap((usage) => usage.literalPrefixes),\n dynamic: usages.some((usage) => usage.dynamic),\n dynamicSites: usages.flatMap((usage) => usage.dynamicSites),\n });\n}\n\n/**\n * Whether `key` is `declared`, lives under it, or is one of its plural\n * variants. The localization runtime resolves `trans(\"cart.items\", { count })`\n * to `cart.items_one`, `cart.items_other` and the other count suffixes, so a\n * name that is only a base of such keys reaches them. A suffix is one segment:\n * `\"cart.items_one\"` matches `\"cart.items\"`, `\"cart.items_one.note\"` does not.\n */\nfunction reaches(declared: string, key: string): boolean {\n if (declared.endsWith(\".\")) return key.startsWith(declared);\n if (key === declared || key.startsWith(`${declared}.`)) return true;\n if (!key.startsWith(`${declared}_`)) return false;\n\n return !key.includes(\".\", declared.length);\n}\n\n/** Whether a usage can reach `key`. A dynamic usage reaches every key. */\nexport function isTranslationKeyUsed(usage: TranslationUsage, key: string): boolean {\n if (usage.dynamic) return true;\n if (usage.keys.some((declared) => reaches(declared, key))) return true;\n if (usage.literals.some((literal) => reaches(literal, key))) return true;\n\n return (\n usage.prefixes.some((prefix) => key.startsWith(prefix)) ||\n usage.literalPrefixes.some((prefix) => key.startsWith(prefix))\n );\n}\n\n/** Whether a string could be a translation key, a group or a key prefix: dotted word segments, no spaces. */\nconst KEY_SHAPE = /^[A-Za-z_$][\\w$-]*(?:\\.[\\w$-]+)*\\.?$/;\n\n/** Longest literal considered; a real key is far shorter, and anything longer is prose or data. */\nconst MAX_LITERAL_LENGTH = 160;\n\nfunction isKeyShaped(text: string): boolean {\n return text.length <= MAX_LITERAL_LENGTH && KEY_SHAPE.test(text);\n}\n\ntype KeyCandidate = { readonly kind: \"key\" | \"prefix\"; readonly value: string };\n\n/** Candidates, `\"dynamic\"` when the key is unknown, or `\"none\"` when there is no key (an inline translation object, no argument). */\ntype KeyResolution = readonly KeyCandidate[] | \"dynamic\" | \"none\";\n\nfunction unwrap(node: ast.Node): ast.Node {\n let current = node;\n while (\n current.type === \"ParenthesizedExpression\" ||\n current.type === \"TSAsExpression\" ||\n current.type === \"TSSatisfiesExpression\" ||\n current.type === \"TSNonNullExpression\"\n ) {\n current = current.expression;\n }\n return current;\n}\n\nfunction combine(left: KeyResolution, right: KeyResolution): KeyResolution {\n if (left === \"dynamic\" || right === \"dynamic\") return \"dynamic\";\n if (left === \"none\") return right;\n if (right === \"none\") return left;\n return [...left, ...right];\n}\n\nfunction resolveKeyExpression(\n node: ast.Node | undefined,\n constants: ReadonlyMap<string, string>,\n): KeyResolution {\n if (node === undefined) return \"none\";\n\n const value = unwrap(node);\n\n if (value.type === \"Literal\" && typeof value.value === \"string\") {\n return [{ kind: \"key\", value: value.value }];\n }\n\n if (value.type === \"TemplateLiteral\") {\n const head = value.quasis[0]?.value.cooked ?? \"\";\n if (head.length === 0) return \"dynamic\";\n return value.expressions.length === 0\n ? [{ kind: \"key\", value: head }]\n : [{ kind: \"prefix\", value: head }];\n }\n\n if (value.type === \"Identifier\") {\n const constant = constants.get(value.name);\n return constant === undefined ? \"dynamic\" : [{ kind: \"key\", value: constant }];\n }\n\n if (value.type === \"ConditionalExpression\") {\n return combine(\n resolveKeyExpression(value.consequent, constants),\n resolveKeyExpression(value.alternate, constants),\n );\n }\n\n if (value.type === \"LogicalExpression\") {\n return combine(\n resolveKeyExpression(value.left, constants),\n resolveKeyExpression(value.right, constants),\n );\n }\n\n // An inline `{ en: \"...\", ar: \"...\" }` translation carries its own text.\n if (value.type === \"ObjectExpression\") return \"none\";\n\n return \"dynamic\";\n}\n\n/** Top-level `const NAME = \"literal\"` declarations, which a key may name through an identifier. */\nfunction moduleConstants(source: ast.SourceFile): Map<string, string> {\n const constants = new Map<string, string>();\n\n for (const statement of source.body) {\n const declaration =\n statement.type === \"ExportNamedDeclaration\" ? statement.declaration : statement;\n\n if (declaration?.type !== \"VariableDeclaration\" || declaration.kind !== \"const\") continue;\n\n for (const declarator of declaration.declarations) {\n if (declarator.id.type !== \"Identifier\" || declarator.init === null) continue;\n\n const init = unwrap(declarator.init);\n\n if (init.type === \"Literal\" && typeof init.value === \"string\") {\n constants.set(declarator.id.name, init.value);\n } else if (init.type === \"TemplateLiteral\" && init.expressions.length === 0) {\n constants.set(declarator.id.name, init.quasis[0]?.value.cooked ?? \"\");\n }\n }\n }\n\n return constants;\n}\n\n/**\n * Local names bound to a translator: the result of `useTrans()`, including an\n * aliased import of it. Calling one of them is a lookup like calling `trans`.\n */\nfunction translatorBindings(source: ast.SourceFile): Set<string> {\n const factories = new Set(TRANSLATOR_FACTORIES);\n const bindings = new Set<string>();\n\n for (const statement of source.body) {\n if (statement.type !== \"ImportDeclaration\") continue;\n for (const specifier of statement.specifiers) {\n if (specifier.type !== \"ImportSpecifier\") continue;\n const imported = specifier.imported.type === \"Identifier\" ? specifier.imported.name : \"\";\n if (TRANSLATOR_FACTORIES.has(imported)) factories.add(specifier.local.name);\n }\n }\n\n const visit = (node: ast.Node): void => {\n if (\n node.type === \"VariableDeclarator\" &&\n node.id.type === \"Identifier\" &&\n node.init !== null &&\n node.init.type === \"CallExpression\" &&\n node.init.callee.type === \"Identifier\" &&\n factories.has(node.init.callee.name)\n ) {\n bindings.add(node.id.name);\n }\n ast.forEachChild(node, visit);\n };\n\n ast.forEachChild(source, visit);\n\n return bindings;\n}\n\nfunction isTranslatorName(name: string, bindings: ReadonlySet<string>): boolean {\n return Object.hasOwn(TRANSLATOR_KEY_ARGUMENT, name) || bindings.has(name);\n}\n\n/**\n * Scan one source file for translation lookups and the modules it loads.\n *\n * Literal keys, static-prefix templates, identifiers bound to a module-level\n * string constant, and choices between those (`a ? \"x\" : \"y\"`) all resolve. Any\n * other key expression is `dynamic`, and so is a translator that escapes as a\n * value (passed on, returned, stored): whoever receives it can call it with any\n * key.\n */\nexport function scanTranslationSource(fileName: string, text: string): TranslationSourceScan {\n const source = ast.parseSourceFile(fileName, text);\n const constants = moduleConstants(source);\n const bindings = translatorBindings(source);\n const keys: string[] = [];\n const prefixes: string[] = [];\n const literals: string[] = [];\n const literalPrefixes: string[] = [];\n const dynamicSites: TranslationDynamicSite[] = [];\n const imports: string[] = [];\n\n const markDynamic = (node: ast.Node, lookup: string): void => {\n dynamicSites.push({ file: fileName, ...ast.positionOf(source, node), text: lookup });\n };\n\n const visit = (node: ast.Node): void => {\n if (node.type === \"ImportDeclaration\") {\n const specifiers = node.specifiers;\n const typeOnly =\n node.importKind === \"type\" ||\n (specifiers.length > 0 &&\n specifiers.every(\n (specifier) => specifier.type === \"ImportSpecifier\" && specifier.importKind === \"type\",\n ));\n if (!typeOnly) imports.push(node.source.value);\n } else if (\n (node.type === \"ExportNamedDeclaration\" || node.type === \"ExportAllDeclaration\") &&\n node.source !== null &&\n node.exportKind !== \"type\"\n ) {\n imports.push(node.source.value);\n } else if (node.type === \"ImportExpression\") {\n const specifier = unwrap(node.source);\n if (specifier.type === \"Literal\" && typeof specifier.value === \"string\") {\n imports.push(specifier.value);\n }\n } else if (node.type === \"CallExpression\") {\n const callee = unwrap(node.callee);\n\n if (callee.type === \"Identifier\" && isTranslatorName(callee.name, bindings)) {\n const index = TRANSLATOR_KEY_ARGUMENT[callee.name] ?? 0;\n const argument = node.arguments[index];\n const resolution =\n argument === undefined\n ? \"none\"\n : argument.type === \"SpreadElement\"\n ? \"dynamic\"\n : resolveKeyExpression(argument, constants);\n\n if (resolution === \"dynamic\") {\n markDynamic(argument ?? node, argument === undefined ? \"\" : sourceText(text, argument));\n } else if (resolution !== \"none\") {\n for (const candidate of resolution) {\n if (candidate.kind === \"key\") keys.push(candidate.value);\n else prefixes.push(candidate.value);\n }\n }\n }\n } else if (node.type === \"Identifier\" && isTranslatorName(node.name, bindings)) {\n const parent = ast.parentOf(node);\n if (parent !== undefined && escapesAsValue(parent, node)) markDynamic(node, node.name);\n } else if (node.type === \"Literal\" && typeof node.value === \"string\") {\n if (!isModuleSpecifier(node) && isKeyShaped(node.value)) literals.push(node.value);\n } else if (node.type === \"TemplateLiteral\") {\n const head = node.quasis[0]?.value.cooked ?? \"\";\n if (isKeyShaped(head)) {\n (node.expressions.length === 0 ? literals : literalPrefixes).push(head);\n }\n }\n\n ast.forEachChild(node, visit);\n };\n\n ast.forEachChild(source, visit);\n\n return {\n usage: usageOf({\n keys,\n prefixes,\n // A literal already read as a lookup key is not repeated.\n literals: literals.filter((literal) => !keys.includes(literal)),\n literalPrefixes: literalPrefixes.filter((literal) => !prefixes.includes(literal)),\n dynamic: dynamicSites.length > 0,\n dynamicSites,\n }),\n imports: [...new Set(imports)],\n };\n}\n\n/** The specifier of an import, re-export or dynamic import: a module path, never a translation key. */\nfunction isModuleSpecifier(node: ast.Node): boolean {\n const parent = ast.parentOf(node);\n\n if (parent === undefined) return false;\n\n switch (parent.type) {\n case \"ImportDeclaration\":\n case \"ExportNamedDeclaration\":\n case \"ExportAllDeclaration\":\n case \"ImportExpression\":\n return parent.source === node;\n default:\n return false;\n }\n}\n\n/** A translator identifier handed on as a value, rather than called or declared. */\nfunction escapesAsValue(parent: ast.Node, node: ast.Node): boolean {\n if (!VALUE_POSITIONS.has(parent.type)) return false;\n\n switch (parent.type) {\n case \"CallExpression\":\n case \"NewExpression\":\n return parent.callee !== node;\n case \"Property\":\n return parent.value === node;\n case \"VariableDeclarator\":\n return parent.init === node;\n case \"ArrowFunctionExpression\":\n return parent.body === node;\n case \"AssignmentExpression\":\n return parent.right === node;\n default:\n return true;\n }\n}\n\nfunction sourceText(text: string, node: ast.Node): string {\n return text.slice(node.start, node.end);\n}\n\n/**\n * The `config.translations` declaration of a page's setup file, as its raw\n * entries. Read statically, like `config.route`, so the build never runs the\n * file. `undefined` when the file declares none.\n */\nexport function readTranslationDeclarations(\n fileName: string,\n text: string,\n): readonly string[] | undefined {\n const source = ast.parseSourceFile(fileName, text);\n const malformed = (): Error =>\n new Error(`config.translations in \"${fileName}\" must be an array of string literals.`);\n\n for (const statement of source.body) {\n const declaration =\n statement.type === \"ExportNamedDeclaration\" ? statement.declaration : undefined;\n if (declaration?.type !== \"VariableDeclaration\") continue;\n\n for (const declarator of declaration.declarations) {\n if (declarator.id.type !== \"Identifier\" || declarator.id.name !== \"config\") continue;\n if (declarator.init === null) continue;\n\n const init = unwrap(declarator.init);\n if (init.type !== \"ObjectExpression\") continue;\n\n for (const property of init.properties) {\n if (property.type !== \"Property\" || property.computed) continue;\n\n const name =\n property.key.type === \"Identifier\"\n ? property.key.name\n : property.key.type === \"Literal\" && typeof property.key.value === \"string\"\n ? property.key.value\n : undefined;\n if (name !== \"translations\") continue;\n\n const value = unwrap(property.value);\n if (value.type !== \"ArrayExpression\") throw malformed();\n\n return value.elements.map((element) => {\n const literal = element === null ? undefined : unwrap(element);\n if (literal?.type !== \"Literal\" || typeof literal.value !== \"string\") throw malformed();\n return literal.value;\n });\n }\n }\n }\n\n return undefined;\n}\n"],"mappings":"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"}
@@ -3,12 +3,30 @@ export type PageBudget = {
3
3
  js?: string;
4
4
  css?: string;
5
5
  };
6
+ /**
7
+ * The inline payload budget (`build.budgets.payload`): how much hydration data
8
+ * one rendered document may embed. Measured at runtime, per document, and
9
+ * reported once per route; see `web/src/server/payload-budget.ts`.
10
+ */
11
+ export type PayloadBudget = {
12
+ /** Limit for one document, e.g. "64kb"; defaults to 64kb. */
13
+ page?: string;
14
+ /** Route-path globs supporting *, ** and ?. The later matching entry sets the limit. */
15
+ pages?: Record<string, string>;
16
+ /**
17
+ * `"warn"` (default) logs a warning and `"error"` logs at error level. Neither
18
+ * fails a request: the budget reports, it does not gate.
19
+ */
20
+ mode?: "warn" | "error";
21
+ };
6
22
  export type BuildBudgets = {
7
23
  page?: PageBudget;
8
24
  /** Route-path globs supporting *, ** and ?. Later matching entries win per field. */
9
25
  pages?: Record<string, PageBudget>;
10
26
  /** Defaults to error when budgets are configured. */
11
27
  mode?: "error" | "warn";
28
+ /** Inline payload budget per rendered document. Checked at runtime, not by `warlock build`. */
29
+ payload?: PayloadBudget;
12
30
  };
13
31
  export type BudgetPage = {
14
32
  page: string;
@@ -1 +1 @@
1
- {"version":3,"file":"build-budgets.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/production/build-budgets.ts"],"mappings":";YAGY;EAAe;EAAa;;YAC5B;EACV,OAAO;;EAEP,QAAQ,eAAe;;EAEvB;;YAEU;EACV;EACA;EACA;EACA;EACA;IAAW;IAAY;;EACvB;EACA;IAAW;IAAY;;EACvB;IAAU;IAAY;;;YAEZ;EACV;EACA;EACA;EACA;EACA,SAAS;;YAEC;EACV;EACA;EACA,OAAO;EACP,YAAY"}
1
+ {"version":3,"file":"build-budgets.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/production/build-budgets.ts"],"mappings":";YAGY;EAAe;EAAa;;;;;;;YAM5B;;EAEV;;EAEA,QAAQ;;;;;EAKR;;YAEU;EACV,OAAO;;EAEP,QAAQ,eAAe;;EAEvB;;EAEA,UAAU;;YAEA;EACV;EACA;EACA;EACA;EACA;IAAW;IAAY;;EACvB;EACA;IAAW;IAAY;;EACvB;IAAU;IAAY;;;YAEZ;EACV;EACA;EACA;EACA;EACA,SAAS;;YAEC;EACV;EACA;EACA,OAAO;EACP,YAAY"}
@@ -1 +1 @@
1
- {"version":3,"file":"build-budgets.mjs","names":[],"sources":["../../../../../../../../packages/core/src/production/build-budgets.ts"],"sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport type PageBudget = { js?: string; css?: string };\nexport type BuildBudgets = {\n page?: PageBudget;\n /** Route-path globs supporting *, ** and ?. Later matching entries win per field. */\n pages?: Record<string, PageBudget>;\n /** Defaults to error when budgets are configured. */\n mode?: \"error\" | \"warn\";\n};\nexport type BudgetPage = {\n page: string;\n source: string;\n js: number;\n css: number;\n dynamic: { js: number; css: number };\n files: string[];\n modules: { id: string; bytes: number }[];\n delta?: { js: number; css: number };\n};\nexport type BudgetViolation = {\n page: string;\n type: \"js\" | \"css\";\n budget: number;\n actual: number;\n modules: BudgetPage[\"modules\"];\n};\nexport type BuildBudgetReport = {\n version: 1;\n compression: \"gzip\";\n pages: BudgetPage[];\n violations: BudgetViolation[];\n};\nexport const BUILD_BUDGET_REPORT_FILE = \"build-budgets.json\";\n\nexport function parseBudgetSize(size: string): number {\n const match = /^(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb)$/i.exec(size.trim());\n if (!match)\n throw new Error(\n `Invalid build budget ${JSON.stringify(size)}: use bytes, kb or mb, e.g. \"150kb\".`,\n );\n const units: Record<string, number> = { b: 1, kb: 1024, mb: 1024 * 1024 };\n const bytes = Number(match[1]) * (units[match[2]?.toLowerCase() ?? \"\"] ?? 0);\n if (!Number.isFinite(bytes)) throw new Error(`Invalid build budget ${JSON.stringify(size)}.`);\n return bytes;\n}\n\nfunction routeMatches(pattern: string, route: string): boolean {\n if ([\"[\", \"]\", \"{\", \"}\", \"(\", \")\", \"!\", \"\\\\\"].some((token) => pattern.includes(token))) {\n throw new Error(\n `Invalid build budget glob ${JSON.stringify(pattern)}: supported wildcards are *, ** and ?.`,\n );\n }\n let expression = \"^\";\n for (let i = 0; i < pattern.length; i++) {\n const character = pattern[i];\n if (character === \"*\" && pattern[i + 1] === \"*\") {\n i++;\n if (pattern[i + 1] === \"/\") {\n expression += \"(?:.*/)?\";\n i++;\n } else expression += \".*\";\n } else if (character === \"*\") expression += \"[^/]*\";\n else if (character === \"?\") expression += \"[^/]\";\n else expression += character?.replace(/[.+^$|]/g, \"\\\\$&\") ?? \"\";\n }\n return new RegExp(`${expression}$`).test(route);\n}\n\nexport function evaluateBuildBudgets(\n pages: BudgetPage[],\n budgets?: BuildBudgets,\n previous?: BuildBudgetReport,\n): BuildBudgetReport {\n if (budgets?.mode !== undefined && budgets.mode !== \"error\" && budgets.mode !== \"warn\") {\n throw new Error('build.budgets.mode must be \"error\" or \"warn\".');\n }\n // Validate even overrides that do not match any current route.\n for (const budget of [budgets?.page, ...Object.values(budgets?.pages ?? {})]) {\n for (const size of Object.values(budget ?? {})) {\n if (size !== undefined) parseBudgetSize(size);\n }\n }\n for (const pattern of Object.keys(budgets?.pages ?? {})) routeMatches(pattern, \"\");\n const violations: BudgetViolation[] = [];\n const result = pages\n .map((page) => {\n let limits = { ...budgets?.page };\n for (const [pattern, override] of Object.entries(budgets?.pages ?? {})) {\n if (routeMatches(pattern, page.page)) limits = { ...limits, ...override };\n }\n for (const type of [\"js\", \"css\"] as const) {\n const configured = limits[type];\n if (configured === undefined) continue;\n const budget = parseBudgetSize(configured);\n if (page[type] > budget)\n violations.push({\n page: page.page,\n type,\n budget,\n actual: page[type],\n modules: page.modules,\n });\n }\n const old = previous?.pages.find(\n (entry) => entry.page === page.page && entry.source === page.source,\n );\n return {\n ...page,\n delta: old ? { js: page.js - old.js, css: page.css - old.css } : undefined,\n };\n })\n .sort((a, b) => b.js + b.css - a.js - a.css || a.page.localeCompare(b.page));\n return { version: 1, compression: \"gzip\", pages: result, violations };\n}\n\nexport async function readBuildBudgetReport(\n outdir: string,\n): Promise<BuildBudgetReport | undefined> {\n try {\n const report: BuildBudgetReport = JSON.parse(\n await readFile(path.join(outdir, BUILD_BUDGET_REPORT_FILE), \"utf8\"),\n );\n if (\n !report ||\n typeof report !== \"object\" ||\n report.version !== 1 ||\n report.compression !== \"gzip\" ||\n !Array.isArray(report.pages)\n )\n return undefined;\n return report;\n } catch (error) {\n if (\n error instanceof SyntaxError ||\n (error instanceof Error && \"code\" in error && error.code === \"ENOENT\")\n )\n return undefined;\n throw error;\n }\n}\n\nconst kb = (bytes: number) => `${(bytes / 1024).toFixed(2)}kb`;\nconst delta = (value: number | undefined) =>\n value === undefined ? \"—\" : `${value >= 0 ? \"+\" : \"\"}${kb(value)}`;\nexport function formatBuildBudgets(report: BuildBudgetReport): string {\n return [\n \"First-visit client assets (gzip); dynamic imports excluded from budgets\",\n \"Page | JS | Δ JS | CSS | Δ CSS | Dynamic JS | Dynamic CSS | Largest modules (rendered bytes)\",\n ...report.pages.map(\n (page) =>\n `${page.page} | ${kb(page.js)} | ${delta(page.delta?.js)} | ${kb(page.css)} | ${delta(page.delta?.css)} | ${kb(page.dynamic.js)} | ${kb(page.dynamic.css)} | ${page.modules\n .slice(0, 3)\n .map((module) => `${module.id} (${module.bytes}b)`)\n .join(\", \")}`,\n ),\n ].join(\"\\n\");\n}\nexport function formatBudgetViolations(report: BuildBudgetReport): string {\n return report.violations\n .map(\n (violation) =>\n `Page ${violation.page}: ${violation.type.toUpperCase()} budget ${kb(violation.budget)}, actual ${kb(violation.actual)} (limit ${violation.budget}b, actual ${violation.actual}b). Biggest modules (rendered bytes): ${violation.modules.map((module) => `${module.id} (${module.bytes}b)`).join(\", \") || \"none\"}`,\n )\n .join(\"\\n\");\n}\nexport async function writeBuildBudgetReport(\n outdir: string,\n report: BuildBudgetReport,\n): Promise<void> {\n await writeFile(\n path.join(outdir, BUILD_BUDGET_REPORT_FILE),\n `${JSON.stringify(report, null, 2)}\\n`,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"build-budgets.mjs","names":[],"sources":["../../../../../../../../packages/core/src/production/build-budgets.ts"],"sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport type PageBudget = { js?: string; css?: string };\n/**\n * The inline payload budget (`build.budgets.payload`): how much hydration data\n * one rendered document may embed. Measured at runtime, per document, and\n * reported once per route; see `web/src/server/payload-budget.ts`.\n */\nexport type PayloadBudget = {\n /** Limit for one document, e.g. \"64kb\"; defaults to 64kb. */\n page?: string;\n /** Route-path globs supporting *, ** and ?. The later matching entry sets the limit. */\n pages?: Record<string, string>;\n /**\n * `\"warn\"` (default) logs a warning and `\"error\"` logs at error level. Neither\n * fails a request: the budget reports, it does not gate.\n */\n mode?: \"warn\" | \"error\";\n};\nexport type BuildBudgets = {\n page?: PageBudget;\n /** Route-path globs supporting *, ** and ?. Later matching entries win per field. */\n pages?: Record<string, PageBudget>;\n /** Defaults to error when budgets are configured. */\n mode?: \"error\" | \"warn\";\n /** Inline payload budget per rendered document. Checked at runtime, not by `warlock build`. */\n payload?: PayloadBudget;\n};\nexport type BudgetPage = {\n page: string;\n source: string;\n js: number;\n css: number;\n dynamic: { js: number; css: number };\n files: string[];\n modules: { id: string; bytes: number }[];\n delta?: { js: number; css: number };\n};\nexport type BudgetViolation = {\n page: string;\n type: \"js\" | \"css\";\n budget: number;\n actual: number;\n modules: BudgetPage[\"modules\"];\n};\nexport type BuildBudgetReport = {\n version: 1;\n compression: \"gzip\";\n pages: BudgetPage[];\n violations: BudgetViolation[];\n};\nexport const BUILD_BUDGET_REPORT_FILE = \"build-budgets.json\";\n\nexport function parseBudgetSize(size: string): number {\n const match = /^(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb)$/i.exec(size.trim());\n if (!match)\n throw new Error(\n `Invalid build budget ${JSON.stringify(size)}: use bytes, kb or mb, e.g. \"150kb\".`,\n );\n const units: Record<string, number> = { b: 1, kb: 1024, mb: 1024 * 1024 };\n const bytes = Number(match[1]) * (units[match[2]?.toLowerCase() ?? \"\"] ?? 0);\n if (!Number.isFinite(bytes)) throw new Error(`Invalid build budget ${JSON.stringify(size)}.`);\n return bytes;\n}\n\nfunction routeMatches(pattern: string, route: string): boolean {\n if ([\"[\", \"]\", \"{\", \"}\", \"(\", \")\", \"!\", \"\\\\\"].some((token) => pattern.includes(token))) {\n throw new Error(\n `Invalid build budget glob ${JSON.stringify(pattern)}: supported wildcards are *, ** and ?.`,\n );\n }\n let expression = \"^\";\n for (let i = 0; i < pattern.length; i++) {\n const character = pattern[i];\n if (character === \"*\" && pattern[i + 1] === \"*\") {\n i++;\n if (pattern[i + 1] === \"/\") {\n expression += \"(?:.*/)?\";\n i++;\n } else expression += \".*\";\n } else if (character === \"*\") expression += \"[^/]*\";\n else if (character === \"?\") expression += \"[^/]\";\n else expression += character?.replace(/[.+^$|]/g, \"\\\\$&\") ?? \"\";\n }\n return new RegExp(`${expression}$`).test(route);\n}\n\nexport function evaluateBuildBudgets(\n pages: BudgetPage[],\n budgets?: BuildBudgets,\n previous?: BuildBudgetReport,\n): BuildBudgetReport {\n if (budgets?.mode !== undefined && budgets.mode !== \"error\" && budgets.mode !== \"warn\") {\n throw new Error('build.budgets.mode must be \"error\" or \"warn\".');\n }\n // Validate even overrides that do not match any current route.\n for (const budget of [budgets?.page, ...Object.values(budgets?.pages ?? {})]) {\n for (const size of Object.values(budget ?? {})) {\n if (size !== undefined) parseBudgetSize(size);\n }\n }\n for (const pattern of Object.keys(budgets?.pages ?? {})) routeMatches(pattern, \"\");\n const violations: BudgetViolation[] = [];\n const result = pages\n .map((page) => {\n let limits = { ...budgets?.page };\n for (const [pattern, override] of Object.entries(budgets?.pages ?? {})) {\n if (routeMatches(pattern, page.page)) limits = { ...limits, ...override };\n }\n for (const type of [\"js\", \"css\"] as const) {\n const configured = limits[type];\n if (configured === undefined) continue;\n const budget = parseBudgetSize(configured);\n if (page[type] > budget)\n violations.push({\n page: page.page,\n type,\n budget,\n actual: page[type],\n modules: page.modules,\n });\n }\n const old = previous?.pages.find(\n (entry) => entry.page === page.page && entry.source === page.source,\n );\n return {\n ...page,\n delta: old ? { js: page.js - old.js, css: page.css - old.css } : undefined,\n };\n })\n .sort((a, b) => b.js + b.css - a.js - a.css || a.page.localeCompare(b.page));\n return { version: 1, compression: \"gzip\", pages: result, violations };\n}\n\nexport async function readBuildBudgetReport(\n outdir: string,\n): Promise<BuildBudgetReport | undefined> {\n try {\n const report: BuildBudgetReport = JSON.parse(\n await readFile(path.join(outdir, BUILD_BUDGET_REPORT_FILE), \"utf8\"),\n );\n if (\n !report ||\n typeof report !== \"object\" ||\n report.version !== 1 ||\n report.compression !== \"gzip\" ||\n !Array.isArray(report.pages)\n )\n return undefined;\n return report;\n } catch (error) {\n if (\n error instanceof SyntaxError ||\n (error instanceof Error && \"code\" in error && error.code === \"ENOENT\")\n )\n return undefined;\n throw error;\n }\n}\n\nconst kb = (bytes: number) => `${(bytes / 1024).toFixed(2)}kb`;\nconst delta = (value: number | undefined) =>\n value === undefined ? \"—\" : `${value >= 0 ? \"+\" : \"\"}${kb(value)}`;\nexport function formatBuildBudgets(report: BuildBudgetReport): string {\n return [\n \"First-visit client assets (gzip); dynamic imports excluded from budgets\",\n \"Page | JS | Δ JS | CSS | Δ CSS | Dynamic JS | Dynamic CSS | Largest modules (rendered bytes)\",\n ...report.pages.map(\n (page) =>\n `${page.page} | ${kb(page.js)} | ${delta(page.delta?.js)} | ${kb(page.css)} | ${delta(page.delta?.css)} | ${kb(page.dynamic.js)} | ${kb(page.dynamic.css)} | ${page.modules\n .slice(0, 3)\n .map((module) => `${module.id} (${module.bytes}b)`)\n .join(\", \")}`,\n ),\n ].join(\"\\n\");\n}\nexport function formatBudgetViolations(report: BuildBudgetReport): string {\n return report.violations\n .map(\n (violation) =>\n `Page ${violation.page}: ${violation.type.toUpperCase()} budget ${kb(violation.budget)}, actual ${kb(violation.actual)} (limit ${violation.budget}b, actual ${violation.actual}b). Biggest modules (rendered bytes): ${violation.modules.map((module) => `${module.id} (${module.bytes}b)`).join(\", \") || \"none\"}`,\n )\n .join(\"\\n\");\n}\nexport async function writeBuildBudgetReport(\n outdir: string,\n report: BuildBudgetReport,\n): Promise<void> {\n await writeFile(\n path.join(outdir, BUILD_BUDGET_REPORT_FILE),\n `${JSON.stringify(report, null, 2)}\\n`,\n );\n}\n"],"mappings":"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"}
@@ -32,12 +32,18 @@ function validateSnapshot(value) {
32
32
  const routes = value.routes.map((route) => {
33
33
  if (!isRecord(route) || typeof route.name !== "string" || typeof route.path !== "string" || !isRequestMethod(route.method)) throw new Error("Route registration child sent an invalid route record.");
34
34
  const response = route.response;
35
- const allowedKeys = response === void 0 ? 3 : 4;
36
- if (Object.keys(route).length !== allowedKeys || response !== void 0 && !isResponseTypeRecord(response)) throw new Error("Route registration child sent a route record with non-browser-safe fields.");
35
+ const clientVisible = route.clientVisible;
36
+ const sites = route.sites;
37
+ const guarded = route.guarded;
38
+ const allowedKeys = 3 + (response === void 0 ? 0 : 1) + (clientVisible === void 0 ? 0 : 1) + (sites === void 0 ? 0 : 1) + (guarded === void 0 ? 0 : 1);
39
+ if (Object.keys(route).length !== allowedKeys || response !== void 0 && !isResponseTypeRecord(response) || clientVisible !== void 0 && clientVisible !== false || sites !== void 0 && !isStringList(sites) || guarded !== void 0 && guarded !== true) throw new Error("Route registration child sent a route record with non-browser-safe fields.");
37
40
  const base = {
38
41
  name: route.name,
39
42
  path: route.path,
40
- method: normalizeRequestMethod(route.method)
43
+ method: normalizeRequestMethod(route.method),
44
+ ...clientVisible === false ? { clientVisible: false } : {},
45
+ ...sites === void 0 ? {} : { sites: Object.freeze([...sites]) },
46
+ ...guarded === true ? { guarded: true } : {}
41
47
  };
42
48
  return Object.freeze(response === void 0 ? base : {
43
49
  ...base,
@@ -49,6 +55,9 @@ function validateSnapshot(value) {
49
55
  routes: Object.freeze(routes)
50
56
  });
51
57
  }
58
+ function isStringList(value) {
59
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
60
+ }
52
61
  function isResponseTypeRecord(value) {
53
62
  return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
54
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"route-registration-snapshot.mjs","names":[],"sources":["../../../../../../../../packages/core/src/production/route-registration-snapshot.ts"],"sourcesContent":["import type { NamedApiRoute, RequestMethod } from \"../router/types\";\nimport type { Environment, RuntimeStrategy } from \"../utils/environment\";\nimport {\n runRegistrationChild,\n type RegistrationChildForker,\n type RegistrationChildProcess,\n} from \"./registration-child-runner\";\n\nconst PROTOCOL_VERSION = 1;\n\nexport type RouteRegistrationSnapshot = Readonly<{\n version: typeof PROTOCOL_VERSION;\n routes: readonly NamedApiRoute[];\n}>;\n\nexport type RouteRegistrationChildRequest = Readonly<{\n version: typeof PROTOCOL_VERSION;\n cwd: string;\n environment?: Environment;\n runtimeStrategy?: RuntimeStrategy;\n}>;\n\nexport type RouteRegistrationChildProcess = RegistrationChildProcess;\n\nexport type RouteRegistrationSnapshotOptions = Readonly<{\n cwd?: string;\n environment?: Environment;\n runtimeStrategy?: RuntimeStrategy;\n timeoutMs?: number;\n childEntry?: string;\n forkProcess?: RegistrationChildForker;\n}>;\n\n/**\n * Collect named application API routes in a fresh process.\n *\n * The child imports application registration modules but deliberately does not\n * register, boot, or start connectors. Application module top-level code can\n * still have arbitrary user-owned side effects; this boundary is framework-only.\n */\nexport function collectRouteRegistrationSnapshot(\n options: RouteRegistrationSnapshotOptions = {},\n): Promise<RouteRegistrationSnapshot> {\n const cwd = options.cwd ?? process.cwd();\n\n return runRegistrationChild<RouteRegistrationSnapshot>({\n cwd,\n timeoutMs: options.timeoutMs,\n childEntry: options.childEntry,\n forkProcess: options.forkProcess,\n request: {\n version: PROTOCOL_VERSION,\n cwd,\n environment: options.environment,\n runtimeStrategy: options.runtimeStrategy,\n } satisfies RouteRegistrationChildRequest,\n resultMessageType: \"route-registration:snapshot\",\n resultNoun: \"snapshot\",\n readResult: (message) => validateSnapshot(message.snapshot),\n });\n}\n\nfunction validateSnapshot(value: unknown): RouteRegistrationSnapshot {\n if (!isRecord(value) || value.version !== PROTOCOL_VERSION || !Array.isArray(value.routes)) {\n throw new Error(\"Route registration child sent an invalid snapshot protocol payload.\");\n }\n\n const routes = value.routes.map((route) => {\n if (\n !isRecord(route) ||\n typeof route.name !== \"string\" ||\n typeof route.path !== \"string\" ||\n !isRequestMethod(route.method)\n ) {\n throw new Error(\"Route registration child sent an invalid route record.\");\n }\n\n const response = route.response;\n const allowedKeys = response === undefined ? 3 : 4;\n\n if (\n Object.keys(route).length !== allowedKeys ||\n (response !== undefined && !isResponseTypeRecord(response))\n ) {\n throw new Error(\"Route registration child sent a route record with non-browser-safe fields.\");\n }\n\n const base = {\n name: route.name,\n path: route.path,\n method: normalizeRequestMethod(route.method),\n };\n\n return Object.freeze(\n response === undefined ? base : { ...base, response: Object.freeze({ ...response }) },\n );\n });\n\n return Object.freeze({ version: PROTOCOL_VERSION, routes: Object.freeze(routes) });\n}\n\nfunction isResponseTypeRecord(value: unknown): value is Record<string, string> {\n return isRecord(value) && Object.values(value).every((entry) => typeof entry === \"string\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isRequestMethod(value: unknown): value is RequestMethod {\n return (\n typeof value === \"string\" &&\n [\"get\", \"post\", \"put\", \"delete\", \"patch\", \"options\", \"head\", \"all\"].includes(\n value.toLowerCase(),\n )\n );\n}\n\nfunction normalizeRequestMethod(value: RequestMethod | string): RequestMethod {\n return value.toLowerCase() === \"all\" ? \"all\" : (value.toUpperCase() as RequestMethod);\n}\n"],"mappings":";;;AAQA,MAAM,mBAAmB;;;;;;;;AAgCzB,SAAgB,iCACd,UAA4C,CAAC,GACT;CACpC,MAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;CAEvC,OAAO,qBAAgD;EACrD;EACA,WAAW,QAAQ;EACnB,YAAY,QAAQ;EACpB,aAAa,QAAQ;EACrB,SAAS;GACP,SAAS;GACT;GACA,aAAa,QAAQ;GACrB,iBAAiB,QAAQ;EAC3B;EACA,mBAAmB;EACnB,YAAY;EACZ,aAAa,YAAY,iBAAiB,QAAQ,QAAQ;CAC5D,CAAC;AACH;AAEA,SAAS,iBAAiB,OAA2C;CACnE,IAAI,CAAC,SAAS,KAAK,KAAK,MAAM,YAAY,oBAAoB,CAAC,MAAM,QAAQ,MAAM,MAAM,GACvF,MAAM,IAAI,MAAM,qEAAqE;CAGvF,MAAM,SAAS,MAAM,OAAO,KAAK,UAAU;EACzC,IACE,CAAC,SAAS,KAAK,KACf,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,SAAS,YACtB,CAAC,gBAAgB,MAAM,MAAM,GAE7B,MAAM,IAAI,MAAM,wDAAwD;EAG1E,MAAM,WAAW,MAAM;EACvB,MAAM,cAAc,aAAa,SAAY,IAAI;EAEjD,IACE,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW,eAC7B,aAAa,UAAa,CAAC,qBAAqB,QAAQ,GAEzD,MAAM,IAAI,MAAM,4EAA4E;EAG9F,MAAM,OAAO;GACX,MAAM,MAAM;GACZ,MAAM,MAAM;GACZ,QAAQ,uBAAuB,MAAM,MAAM;EAC7C;EAEA,OAAO,OAAO,OACZ,aAAa,SAAY,OAAO;GAAE,GAAG;GAAM,UAAU,OAAO,OAAO,EAAE,GAAG,SAAS,CAAC;EAAE,CACtF;CACF,CAAC;CAED,OAAO,OAAO,OAAO;EAAE,SAAS;EAAkB,QAAQ,OAAO,OAAO,MAAM;CAAE,CAAC;AACnF;AAEA,SAAS,qBAAqB,OAAiD;CAC7E,OAAO,SAAS,KAAK,KAAK,OAAO,OAAO,KAAK,CAAC,CAAC,OAAO,UAAU,OAAO,UAAU,QAAQ;AAC3F;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,gBAAgB,OAAwC;CAC/D,OACE,OAAO,UAAU,YACjB;EAAC;EAAO;EAAQ;EAAO;EAAU;EAAS;EAAW;EAAQ;CAAK,CAAC,CAAC,SAClE,MAAM,YAAY,CACpB;AAEJ;AAEA,SAAS,uBAAuB,OAA8C;CAC5E,OAAO,MAAM,YAAY,MAAM,QAAQ,QAAS,MAAM,YAAY;AACpE"}
1
+ {"version":3,"file":"route-registration-snapshot.mjs","names":[],"sources":["../../../../../../../../packages/core/src/production/route-registration-snapshot.ts"],"sourcesContent":["import type { NamedApiRoute, RequestMethod } from \"../router/types\";\nimport type { Environment, RuntimeStrategy } from \"../utils/environment\";\nimport {\n runRegistrationChild,\n type RegistrationChildForker,\n type RegistrationChildProcess,\n} from \"./registration-child-runner\";\n\nconst PROTOCOL_VERSION = 1;\n\nexport type RouteRegistrationSnapshot = Readonly<{\n version: typeof PROTOCOL_VERSION;\n routes: readonly NamedApiRoute[];\n}>;\n\nexport type RouteRegistrationChildRequest = Readonly<{\n version: typeof PROTOCOL_VERSION;\n cwd: string;\n environment?: Environment;\n runtimeStrategy?: RuntimeStrategy;\n}>;\n\nexport type RouteRegistrationChildProcess = RegistrationChildProcess;\n\nexport type RouteRegistrationSnapshotOptions = Readonly<{\n cwd?: string;\n environment?: Environment;\n runtimeStrategy?: RuntimeStrategy;\n timeoutMs?: number;\n childEntry?: string;\n forkProcess?: RegistrationChildForker;\n}>;\n\n/**\n * Collect named application API routes in a fresh process.\n *\n * The child imports application registration modules but deliberately does not\n * register, boot, or start connectors. Application module top-level code can\n * still have arbitrary user-owned side effects; this boundary is framework-only.\n */\nexport function collectRouteRegistrationSnapshot(\n options: RouteRegistrationSnapshotOptions = {},\n): Promise<RouteRegistrationSnapshot> {\n const cwd = options.cwd ?? process.cwd();\n\n return runRegistrationChild<RouteRegistrationSnapshot>({\n cwd,\n timeoutMs: options.timeoutMs,\n childEntry: options.childEntry,\n forkProcess: options.forkProcess,\n request: {\n version: PROTOCOL_VERSION,\n cwd,\n environment: options.environment,\n runtimeStrategy: options.runtimeStrategy,\n } satisfies RouteRegistrationChildRequest,\n resultMessageType: \"route-registration:snapshot\",\n resultNoun: \"snapshot\",\n readResult: (message) => validateSnapshot(message.snapshot),\n });\n}\n\nfunction validateSnapshot(value: unknown): RouteRegistrationSnapshot {\n if (!isRecord(value) || value.version !== PROTOCOL_VERSION || !Array.isArray(value.routes)) {\n throw new Error(\"Route registration child sent an invalid snapshot protocol payload.\");\n }\n\n const routes = value.routes.map((route) => {\n if (\n !isRecord(route) ||\n typeof route.name !== \"string\" ||\n typeof route.path !== \"string\" ||\n !isRequestMethod(route.method)\n ) {\n throw new Error(\"Route registration child sent an invalid route record.\");\n }\n\n const response = route.response;\n const clientVisible = route.clientVisible;\n const sites = route.sites;\n const guarded = route.guarded;\n const allowedKeys =\n 3 +\n (response === undefined ? 0 : 1) +\n (clientVisible === undefined ? 0 : 1) +\n (sites === undefined ? 0 : 1) +\n (guarded === undefined ? 0 : 1);\n\n if (\n Object.keys(route).length !== allowedKeys ||\n (response !== undefined && !isResponseTypeRecord(response)) ||\n (clientVisible !== undefined && clientVisible !== false) ||\n (sites !== undefined && !isStringList(sites)) ||\n (guarded !== undefined && guarded !== true)\n ) {\n throw new Error(\"Route registration child sent a route record with non-browser-safe fields.\");\n }\n\n const base = {\n name: route.name,\n path: route.path,\n method: normalizeRequestMethod(route.method),\n ...(clientVisible === false ? { clientVisible: false as const } : {}),\n ...(sites === undefined ? {} : { sites: Object.freeze([...sites]) }),\n ...(guarded === true ? { guarded: true as const } : {}),\n };\n\n return Object.freeze(\n response === undefined ? base : { ...base, response: Object.freeze({ ...response }) },\n );\n });\n\n return Object.freeze({ version: PROTOCOL_VERSION, routes: Object.freeze(routes) });\n}\n\nfunction isStringList(value: unknown): value is readonly string[] {\n return Array.isArray(value) && value.every((entry) => typeof entry === \"string\");\n}\n\nfunction isResponseTypeRecord(value: unknown): value is Record<string, string> {\n return isRecord(value) && Object.values(value).every((entry) => typeof entry === \"string\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isRequestMethod(value: unknown): value is RequestMethod {\n return (\n typeof value === \"string\" &&\n [\"get\", \"post\", \"put\", \"delete\", \"patch\", \"options\", \"head\", \"all\"].includes(\n value.toLowerCase(),\n )\n );\n}\n\nfunction normalizeRequestMethod(value: RequestMethod | string): RequestMethod {\n return value.toLowerCase() === \"all\" ? \"all\" : (value.toUpperCase() as RequestMethod);\n}\n"],"mappings":";;;AAQA,MAAM,mBAAmB;;;;;;;;AAgCzB,SAAgB,iCACd,UAA4C,CAAC,GACT;CACpC,MAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;CAEvC,OAAO,qBAAgD;EACrD;EACA,WAAW,QAAQ;EACnB,YAAY,QAAQ;EACpB,aAAa,QAAQ;EACrB,SAAS;GACP,SAAS;GACT;GACA,aAAa,QAAQ;GACrB,iBAAiB,QAAQ;EAC3B;EACA,mBAAmB;EACnB,YAAY;EACZ,aAAa,YAAY,iBAAiB,QAAQ,QAAQ;CAC5D,CAAC;AACH;AAEA,SAAS,iBAAiB,OAA2C;CACnE,IAAI,CAAC,SAAS,KAAK,KAAK,MAAM,YAAY,oBAAoB,CAAC,MAAM,QAAQ,MAAM,MAAM,GACvF,MAAM,IAAI,MAAM,qEAAqE;CAGvF,MAAM,SAAS,MAAM,OAAO,KAAK,UAAU;EACzC,IACE,CAAC,SAAS,KAAK,KACf,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,SAAS,YACtB,CAAC,gBAAgB,MAAM,MAAM,GAE7B,MAAM,IAAI,MAAM,wDAAwD;EAG1E,MAAM,WAAW,MAAM;EACvB,MAAM,gBAAgB,MAAM;EAC5B,MAAM,QAAQ,MAAM;EACpB,MAAM,UAAU,MAAM;EACtB,MAAM,cACJ,KACC,aAAa,SAAY,IAAI,MAC7B,kBAAkB,SAAY,IAAI,MAClC,UAAU,SAAY,IAAI,MAC1B,YAAY,SAAY,IAAI;EAE/B,IACE,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW,eAC7B,aAAa,UAAa,CAAC,qBAAqB,QAAQ,KACxD,kBAAkB,UAAa,kBAAkB,SACjD,UAAU,UAAa,CAAC,aAAa,KAAK,KAC1C,YAAY,UAAa,YAAY,MAEtC,MAAM,IAAI,MAAM,4EAA4E;EAG9F,MAAM,OAAO;GACX,MAAM,MAAM;GACZ,MAAM,MAAM;GACZ,QAAQ,uBAAuB,MAAM,MAAM;GAC3C,GAAI,kBAAkB,QAAQ,EAAE,eAAe,MAAe,IAAI,CAAC;GACnE,GAAI,UAAU,SAAY,CAAC,IAAI,EAAE,OAAO,OAAO,OAAO,CAAC,GAAG,KAAK,CAAC,EAAE;GAClE,GAAI,YAAY,OAAO,EAAE,SAAS,KAAc,IAAI,CAAC;EACvD;EAEA,OAAO,OAAO,OACZ,aAAa,SAAY,OAAO;GAAE,GAAG;GAAM,UAAU,OAAO,OAAO,EAAE,GAAG,SAAS,CAAC;EAAE,CACtF;CACF,CAAC;CAED,OAAO,OAAO,OAAO;EAAE,SAAS;EAAkB,QAAQ,OAAO,OAAO,MAAM;CAAE,CAAC;AACnF;AAEA,SAAS,aAAa,OAA4C;CAChE,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,UAAU,OAAO,UAAU,QAAQ;AACjF;AAEA,SAAS,qBAAqB,OAAiD;CAC7E,OAAO,SAAS,KAAK,KAAK,OAAO,OAAO,KAAK,CAAC,CAAC,OAAO,UAAU,OAAO,UAAU,QAAQ;AAC3F;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,gBAAgB,OAAwC;CAC/D,OACE,OAAO,UAAU,YACjB;EAAC;EAAO;EAAQ;EAAO;EAAU;EAAS;EAAW;EAAQ;CAAK,CAAC,CAAC,SAClE,MAAM,YAAY,CACpB;AAEJ;AAEA,SAAS,uBAAuB,OAA8C;CAC5E,OAAO,MAAM,YAAY,MAAM,QAAQ,QAAS,MAAM,YAAY;AACpE"}