-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy pathworker.mjs
More file actions
130 lines (105 loc) · 3.73 KB
/
Copy pathworker.mjs
File metadata and controls
130 lines (105 loc) · 3.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
let redirectRulesPromise;
const DEFAULT_LOCALE = "en";
async function loadRedirectRules(env) {
// Use the built _redirects file from the assets bundle so build-time generated rules are honored.
const req = new Request("https://docs.rustfs.com/_redirects");
const res = await env.ASSETS.fetch(req);
if (!res.ok) return [];
const text = await res.text();
const rules = [];
for (const rawLine of text.split("\n")) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;
const match = line.match(/^(\S+)\s+(\S+)\s+(\d{3})$/);
if (!match) continue;
const source = match[1];
const target = match[2];
const status = Number(match[3]);
if (!source || !target || !Number.isInteger(status)) continue;
rules.push({ source, target, status });
}
return rules;
}
async function getRedirectRules(env) {
if (!redirectRulesPromise) {
redirectRulesPromise = loadRedirectRules(env);
}
return redirectRulesPromise;
}
function matchRedirect(pathname, rules) {
for (const rule of rules) {
const { source, target, status } = rule;
if (source.endsWith("/*")) {
const prefix = source.slice(0, -1);
if (!pathname.startsWith(prefix)) continue;
const splat = pathname.slice(prefix.length);
return {
target: target.replace(":splat", splat),
status,
};
}
if (pathname === source) {
return { target, status };
}
}
return null;
}
function splitLocale(pathname) {
const parts = pathname.split("/").filter(Boolean);
if (parts.length === 0) return { locale: null, remainder: pathname };
const locale = parts[0];
if (locale !== DEFAULT_LOCALE) return { locale: null, remainder: pathname };
const remainder = `/${parts.slice(1).join("/")}`;
return { locale, remainder: remainder === "/" ? "/" : remainder };
}
// Redirect sources never carry a trailing slash (see scripts/language-redirects.mjs),
// but inbound URLs may. Canonicalize before matching so /installation/ reuses the
// /installation rule instead of falling through to a missing asset.
function normalizePathname(pathname) {
if (pathname.length > 1 && pathname.endsWith("/")) {
return pathname.slice(0, -1);
}
return pathname;
}
function isLocalizedPath(pathname) {
return pathname === `/${DEFAULT_LOCALE}` || pathname.startsWith(`/${DEFAULT_LOCALE}/`);
}
function applyLocaleToTarget(target, locale) {
if (!target.startsWith("/")) return target;
if (isLocalizedPath(target)) return target;
if (target === "/") return `/${locale}`;
return `/${locale}${target}`;
}
export default {
async fetch(request, env) {
const method = request.method.toUpperCase();
if (method === "GET" || method === "HEAD") {
const url = new URL(request.url);
const rules = await getRedirectRules(env);
const pathname = normalizePathname(url.pathname);
let matched = matchRedirect(pathname, rules);
// English-only fallback: /en/foo can reuse base rule /foo -> /en/bar.
if (!matched) {
const { locale, remainder } = splitLocale(pathname);
if (locale) {
const baseMatch = matchRedirect(remainder, rules);
if (baseMatch) {
matched = {
status: baseMatch.status,
target: applyLocaleToTarget(baseMatch.target, locale),
};
}
}
}
if (matched) {
const targetUrl = new URL(matched.target, url);
targetUrl.search = url.search;
// Avoid redirect loops caused by equivalent source/target paths.
if (`${targetUrl.pathname}${targetUrl.search}` !== `${pathname}${url.search}`) {
return Response.redirect(targetUrl.toString(), matched.status);
}
}
}
return env.ASSETS.fetch(request);
},
};