1// Alchemy modifications are licensed under Apache-2.0.
2// This file includes third-party code; see /THIRD_PARTY_LICENSES.md.
3import { MAX_ROUTES_RULE_LENGTH, MAX_ROUTES_RULES } from "./constants.ts";
4import type { StaticRouting } from "../types.ts";
5
6const MAX_REPORTED_DUPLICATED_RULES = 5;
7
8// copy of what EWC does. Wrangler uploads the rules in one array (so the API is consistent with Wrangler config),
9// but router Worker expects the rules to be split into two arrays, which we do here.
10// This logic is translated from assets/staticrouting.go.
11
12export function parseStaticRouting(input: Array<string>): StaticRouting {
13 if (input.length === 0) {
14 throw new Error(
15 "No `run_worker_first` rules were provided; must provide at least 1 rule.",
16 );
17 }
18 if (input.length > MAX_ROUTES_RULES) {
19 const seenRules = new Set<string>();
20 const duplicatedRules = new Set<string>();
21
22 for (const rule of input) {
23 if (seenRules.has(rule)) {
24 duplicatedRules.add(rule);
25 }
26 seenRules.add(rule);
27 }
28
29 if (duplicatedRules.size > 0) {
30 const duplicateEntryCount = input.length - seenRules.size;
31 const reportedDuplicatedRules = [...seenRules]
32 .filter((rule) => duplicatedRules.has(rule))
33 .slice(0, MAX_REPORTED_DUPLICATED_RULES);
34 const unreportedDuplicatedRuleCount =
35 duplicatedRules.size - reportedDuplicatedRules.length;
36 const unreportedDuplicatedRulesMessage =
37 unreportedDuplicatedRuleCount > 0
38 ? `\n...and ${unreportedDuplicatedRuleCount} more duplicated ${unreportedDuplicatedRuleCount === 1 ? "rule" : "rules"}.`
39 : "";
40
41 throw new Error(
42 `Too many \`run_worker_first\` rules were provided; ${input.length} rules provided (${seenRules.size} distinct, ${duplicateEntryCount} duplicate ${duplicateEntryCount === 1 ? "entry" : "entries"}) exceeds max of ${MAX_ROUTES_RULES}. Note: duplicate entries count towards the route limit. Ensure that no duplicate rules are present in your \`run_worker_first\` configuration.\n\nThe duplicated rules found are:\n${reportedDuplicatedRules.map((rule) => `- ${JSON.stringify(rule)}`).join("\n")}${unreportedDuplicatedRulesMessage}`,
43 );
44 }
45
46 throw new Error(
47 `Too many \`run_worker_first\` rules were provided; ${input.length} rules provided exceeds max of ${MAX_ROUTES_RULES}.`,
48 );
49 }
50
51 const rawAssetWorkerRules = [];
52 const assetWorkerRules = [];
53 const userWorkerRules = [];
54 const invalidRules = [];
55
56 for (const rule of input) {
57 if (rule.startsWith("!/")) {
58 assetWorkerRules.push(rule.slice(1)); // Remove leading !
59 rawAssetWorkerRules.push(rule);
60 } else if (rule.startsWith("/")) {
61 userWorkerRules.push(rule);
62 } else if (rule.startsWith("!")) {
63 invalidRules.push(`'${rule}': negative rules must start with '!/'`);
64 } else {
65 invalidRules.push(`'${rule}': rules must start with '/' or '!/'`);
66 }
67 }
68
69 if (assetWorkerRules.length > 0 && userWorkerRules.length === 0) {
70 throw new Error(
71 "Only negative `run_worker_first` rules were provided; must provide at least 1 non-negative rule",
72 );
73 }
74
75 const invalidAssetWorkerRules =
76 validateStaticRoutingRules(rawAssetWorkerRules);
77 const invalidUserWorkerRules = validateStaticRoutingRules(userWorkerRules);
78 const errorMessage = formatInvalidRoutes([
79 ...invalidRules,
80 ...invalidUserWorkerRules,
81 ...invalidAssetWorkerRules,
82 ]);
83
84 if (errorMessage) {
85 throw new Error(errorMessage);
86 }
87
88 return { asset_worker: assetWorkerRules, user_worker: userWorkerRules };
89}
90
91function validateStaticRoutingRules(rules: Array<string>): Array<string> {
92 const invalid: Array<string> = [];
93 const seen = new Set<string>();
94 for (const rule of rules) {
95 if (rule.length > MAX_ROUTES_RULE_LENGTH) {
96 invalid.push(
97 `'${rule}': all rules must be less than ${MAX_ROUTES_RULE_LENGTH} characters in length`,
98 );
99 }
100 if (seen.has(rule)) {
101 invalid.push(`'${rule}': rule is a duplicate; rules must be unique`);
102 }
103 if (rule.endsWith("*")) {
104 // Check for redundant rules due to a glob
105 for (const otherRule of rules) {
106 if (otherRule !== rule && otherRule.startsWith(rule.slice(0, -1))) {
107 invalid.push(`'${otherRule}': rule '${rule}' makes it redundant`);
108 }
109 }
110 }
111 seen.add(rule);
112 }
113 return invalid;
114}
115
116const formatInvalidRoutes = (invalidRules: Array<string>) => {
117 if (invalidRules.length === 0) {
118 return undefined;
119 }
120 return `Invalid routes in \`run_worker_first\`:\n` + invalidRules.join("\n");
121};