@expo/code-review-cli 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as cheerio from "cheerio";
3
+ const discardedSelectors = [
4
+ "script",
5
+ "style",
6
+ "noscript",
7
+ "svg",
8
+ "nav",
9
+ "footer",
10
+ "form",
11
+ "button",
12
+ "[hidden]",
13
+ "[aria-hidden='true']",
14
+ ].join(",");
15
+ const blockSelectors = [
16
+ "address",
17
+ "article",
18
+ "aside",
19
+ "blockquote",
20
+ "br",
21
+ "dd",
22
+ "div",
23
+ "dl",
24
+ "dt",
25
+ "figcaption",
26
+ "figure",
27
+ "h1",
28
+ "h2",
29
+ "h3",
30
+ "h4",
31
+ "h5",
32
+ "h6",
33
+ "li",
34
+ "main",
35
+ "p",
36
+ "pre",
37
+ "section",
38
+ "table",
39
+ "td",
40
+ "th",
41
+ "tr",
42
+ ].join(",");
43
+ function cleanText(value) {
44
+ return value
45
+ .replace(/\r/g, "")
46
+ .replace(/[ \t]+/g, " ")
47
+ .replace(/ *\n */g, "\n")
48
+ .replace(/\n{3,}/g, "\n\n")
49
+ .trim();
50
+ }
51
+ function cleanTitle(value) {
52
+ return cleanText(value)
53
+ .replace(/\s*[|–—-]\s*Apple Developer Documentation$/i, "")
54
+ .replace(/\s*[|–—-]\s*Android Developers$/i, "")
55
+ .trim();
56
+ }
57
+ export function extractDocumentationPage(html, url, platform, source = {}) {
58
+ const $ = cheerio.load(html);
59
+ $(discardedSelectors).remove();
60
+ const main = $("main").first().length
61
+ ? $("main").first()
62
+ : $("article").first().length
63
+ ? $("article").first()
64
+ : $("[role='main']").first().length
65
+ ? $("[role='main']").first()
66
+ : $("body").first();
67
+ main.find(blockSelectors).each((_, element) => {
68
+ $(element).append("\n");
69
+ });
70
+ const body = cleanText(main.text());
71
+ const title = cleanTitle($("h1").first().text() || $("title").first().text());
72
+ if (!title || body.length < 80) {
73
+ return null;
74
+ }
75
+ const links = new Set();
76
+ $("a[href]").each((_, element) => {
77
+ const href = $(element).attr("href");
78
+ if (href) {
79
+ links.add(href);
80
+ }
81
+ });
82
+ return {
83
+ document: { platform, title, url, body, ...source },
84
+ links: [...links],
85
+ };
86
+ }
87
+ export function chunkDocument(document, indexedAt, targetCharacters = 1400, overlapCharacters = 180) {
88
+ const paragraphs = document.body
89
+ .split(/\n{2,}/)
90
+ .map((paragraph) => paragraph.trim())
91
+ .filter(Boolean);
92
+ const passages = [];
93
+ let current = "";
94
+ const flush = () => {
95
+ const passage = current.trim();
96
+ if (passage) {
97
+ passages.push(passage);
98
+ }
99
+ const overlapStart = Math.max(0, passage.length - overlapCharacters);
100
+ const overlapTail = passage.slice(overlapStart);
101
+ const sentenceBoundary = overlapTail.match(/[.!?]\s+/);
102
+ current =
103
+ sentenceBoundary?.index !== undefined
104
+ ? overlapTail.slice(sentenceBoundary.index + sentenceBoundary[0].length)
105
+ : "";
106
+ };
107
+ for (const paragraph of paragraphs) {
108
+ if (current && current.length + paragraph.length + 2 > targetCharacters) {
109
+ flush();
110
+ }
111
+ if (paragraph.length > targetCharacters * 2) {
112
+ let remaining = paragraph;
113
+ while (remaining.length > targetCharacters) {
114
+ const splitAt = remaining.lastIndexOf(" ", targetCharacters);
115
+ const boundary = splitAt > targetCharacters / 2 ? splitAt : targetCharacters;
116
+ current = `${current}\n\n${remaining.slice(0, boundary)}`.trim();
117
+ flush();
118
+ remaining = remaining.slice(boundary).trim();
119
+ }
120
+ current = `${current}\n\n${remaining}`.trim();
121
+ }
122
+ else {
123
+ current = `${current}\n\n${paragraph}`.trim();
124
+ }
125
+ }
126
+ flush();
127
+ const documentId = createHash("sha256")
128
+ .update(`${document.provider ?? document.platform}\0${document.url}\0${document.title}`)
129
+ .digest("hex")
130
+ .slice(0, 16);
131
+ return passages.map((passage, chunkIndex) => ({
132
+ ...document,
133
+ body: undefined,
134
+ id: `${document.provider ?? document.platform}:${documentId}#${chunkIndex}`,
135
+ passage,
136
+ indexedAt,
137
+ }));
138
+ }
@@ -0,0 +1,32 @@
1
+ export function extractMarkdownDocumentationPage(markdown, url, platform, source = {}) {
2
+ const sanitized = markdown
3
+ .replace(/<!--[^]*?-->/g, "")
4
+ .replace(/<script\b[^>]*>[^]*?<\/script>/gi, "")
5
+ .replace(/<style\b[^>]*>[^]*?<\/style>/gi, "")
6
+ .replace(/\r/g, "");
7
+ const withoutFrontMatter = sanitized.replace(/^---\n[^]*?\n---\n/, "");
8
+ const title = withoutFrontMatter.match(/^#\s+(.+)$/m)?.[1]?.trim() ??
9
+ withoutFrontMatter.match(/^([^\n]+)\n(?:=+|-+)\s*$/m)?.[1]?.trim() ??
10
+ "";
11
+ const body = withoutFrontMatter
12
+ .replace(/^#{1,6}\s+/gm, "")
13
+ .replace(/^(?:=+|-+)\s*$/gm, "")
14
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
15
+ .replace(/<[^>]+>/g, " ")
16
+ .replace(/[ \t]+/g, " ")
17
+ .replace(/\n{3,}/g, "\n\n")
18
+ .trim();
19
+ if (!title || body.length < 80)
20
+ return null;
21
+ const links = [...sanitized.matchAll(/\[[^\]]+\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g)].flatMap((match) => (match[1] ? [match[1]] : []));
22
+ return {
23
+ document: {
24
+ platform,
25
+ title,
26
+ url,
27
+ body,
28
+ ...source,
29
+ },
30
+ links,
31
+ };
32
+ }
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import { chunkDocument, extractDocumentationPage } from "./html.js";
3
+ import { okHttpProvider, resolveAllowedUrl } from "./providers.js";
4
+ import { readBodyWithLimit } from "./response.js";
5
+ import { buildSearchIndex, searchDocumentation } from "./search-index.js";
6
+ const OKHTTP_SEARCH_INDEX = "https://lysine.dev/okhttp/search/search_index.json";
7
+ const OKHTTP_SEARCH_INDEX_LIMIT_BYTES = 1_000_000;
8
+ const OKHTTP_SEARCH_TIMEOUT_MS = 10_000;
9
+ const okHttpSearchIndexSchema = z.object({
10
+ docs: z
11
+ .array(z.object({
12
+ location: z.string().max(2_000),
13
+ title: z.string().min(1).max(500),
14
+ text: z.string().max(100_000),
15
+ }))
16
+ .max(1_000),
17
+ });
18
+ const indexCache = new WeakMap();
19
+ async function loadOkHttpSearchIndex(fetchImplementation) {
20
+ const response = await fetchImplementation(OKHTTP_SEARCH_INDEX, {
21
+ redirect: "manual",
22
+ signal: AbortSignal.timeout(OKHTTP_SEARCH_TIMEOUT_MS),
23
+ headers: {
24
+ accept: "application/json",
25
+ "accept-language": "en-US,en;q=0.9",
26
+ "user-agent": "review-research-mcp/0.2 (+official documentation search index)",
27
+ },
28
+ });
29
+ if (response.status >= 300 && response.status < 400) {
30
+ throw new Error(`OkHttp search index unexpectedly redirected with HTTP ${response.status}`);
31
+ }
32
+ if (!response.ok) {
33
+ throw new Error(`OkHttp search index returned HTTP ${response.status}`);
34
+ }
35
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
36
+ if (!contentType.includes("application/json")) {
37
+ throw new Error(`OkHttp search index returned unsupported content type: ${contentType || "missing"}`);
38
+ }
39
+ const parsed = okHttpSearchIndexSchema.parse(JSON.parse(await readBodyWithLimit(response, OKHTTP_SEARCH_INDEX_LIMIT_BYTES)));
40
+ const indexedAt = new Date().toISOString();
41
+ const seenDocuments = new Set();
42
+ const documents = parsed.docs.flatMap((entry) => {
43
+ try {
44
+ const url = resolveAllowedUrl(okHttpProvider, entry.location, "https://lysine.dev/okhttp/");
45
+ const key = `${url.href}|${entry.title}`;
46
+ if (seenDocuments.has(key))
47
+ return [];
48
+ seenDocuments.add(key);
49
+ const extracted = extractDocumentationPage(`<!doctype html><html><body><main><h1>${entry.title}</h1>${entry.text}</main></body></html>`, url.href, "android", { provider: "okhttp", sourceKind: "official-guide" });
50
+ return extracted ? [extracted.document] : [];
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ });
56
+ const chunks = documents.flatMap((document) => chunkDocument(document, indexedAt));
57
+ return buildSearchIndex(chunks, documents.length, indexedAt);
58
+ }
59
+ function cachedOkHttpSearchIndex(fetchImplementation) {
60
+ const cached = indexCache.get(fetchImplementation);
61
+ if (cached)
62
+ return cached;
63
+ const pending = loadOkHttpSearchIndex(fetchImplementation);
64
+ indexCache.set(fetchImplementation, pending);
65
+ return pending;
66
+ }
67
+ export async function searchOkHttpDocumentation(query, limit, fetchImplementation = fetch) {
68
+ const index = await cachedOkHttpSearchIndex(fetchImplementation);
69
+ const search = (value) => searchDocumentation(index, value, {
70
+ platform: "android",
71
+ providers: ["okhttp"],
72
+ sourceKinds: ["official-guide"],
73
+ limit,
74
+ });
75
+ const conceptTokens = [...new Set(query.match(/\b[a-z][a-z0-9-]{3,}\b/g) ?? [])];
76
+ const concepts = conceptTokens.join(" ");
77
+ const candidates = concepts && concepts !== query
78
+ ? [
79
+ ...conceptTokens.flatMap((concept) => search(concept).slice(0, 1)),
80
+ ...search(concepts),
81
+ ...search(query),
82
+ ]
83
+ : search(query);
84
+ const seen = new Set();
85
+ return candidates
86
+ .filter((result) => {
87
+ const key = `${result.url}|${result.title}`;
88
+ if (seen.has(key))
89
+ return false;
90
+ seen.add(key);
91
+ return true;
92
+ })
93
+ .slice(0, limit);
94
+ }
@@ -0,0 +1,4 @@
1
+ import { fileURLToPath } from "node:url";
2
+ export const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
3
+ export const defaultConfigPath = fileURLToPath(new URL("../../research/sources.json", import.meta.url));
4
+ export const defaultIndexPath = fileURLToPath(new URL("../../research/data/docs-index.json", import.meta.url));
@@ -0,0 +1,322 @@
1
+ function isSecurePublicUrl(url, hostname) {
2
+ return (url.protocol === "https:" &&
3
+ url.hostname === hostname &&
4
+ (url.port === "" || url.port === "443") &&
5
+ url.username === "" &&
6
+ url.password === "");
7
+ }
8
+ function hasAllowedPath(url, prefixes) {
9
+ return prefixes.some((prefix) => url.pathname === prefix || url.pathname.startsWith(`${prefix}/`));
10
+ }
11
+ function acceptsOrigin(url, origins) {
12
+ return origins.some(({ hostname, prefixes }) => isSecurePublicUrl(url, hostname) && hasAllowedPath(url, prefixes));
13
+ }
14
+ function canonicalizeDocumentationUrl(url) {
15
+ const canonical = new URL(url.href);
16
+ canonical.hash = "";
17
+ canonical.search = "";
18
+ canonical.hostname = canonical.hostname.toLowerCase();
19
+ canonical.pathname = canonical.pathname.replace(/\/{2,}/g, "/");
20
+ if (canonical.pathname.length > 1) {
21
+ canonical.pathname = canonical.pathname.replace(/\/$/, "");
22
+ }
23
+ return canonical;
24
+ }
25
+ function htmlProvider(id, platform, displayName, origins, preserveTrailingSlash = false) {
26
+ return {
27
+ id,
28
+ platform,
29
+ displayName,
30
+ accepts(url) {
31
+ return acceptsOrigin(url, origins);
32
+ },
33
+ acceptsRequest(url) {
34
+ return this.accepts(url);
35
+ },
36
+ canonicalize(url) {
37
+ const hadTrailingSlash = url.pathname.endsWith("/");
38
+ const canonical = canonicalizeDocumentationUrl(url);
39
+ if (preserveTrailingSlash && hadTrailingSlash && !canonical.pathname.endsWith("/")) {
40
+ canonical.pathname += "/";
41
+ }
42
+ return canonical;
43
+ },
44
+ requestUrl(documentUrl) {
45
+ if (preserveTrailingSlash && !documentUrl.pathname.endsWith("/")) {
46
+ const request = new URL(documentUrl.href);
47
+ request.pathname += "/";
48
+ return request;
49
+ }
50
+ return documentUrl;
51
+ },
52
+ responseFormat() {
53
+ return "html";
54
+ },
55
+ };
56
+ }
57
+ function appleDocCProvider(id, displayName, prefixes) {
58
+ return {
59
+ id,
60
+ platform: "apple",
61
+ displayName,
62
+ accepts(url) {
63
+ return isSecurePublicUrl(url, "developer.apple.com") && hasAllowedPath(url, prefixes);
64
+ },
65
+ acceptsRequest(url) {
66
+ if (this.accepts(url))
67
+ return true;
68
+ if (!isSecurePublicUrl(url, "developer.apple.com") ||
69
+ !url.pathname.startsWith("/tutorials/data/documentation/") ||
70
+ !url.pathname.endsWith(".json")) {
71
+ return false;
72
+ }
73
+ const correspondingDocumentUrl = new URL(url.href);
74
+ correspondingDocumentUrl.pathname = url.pathname
75
+ .slice("/tutorials/data".length)
76
+ .replace(/\.json$/, "");
77
+ return hasAllowedPath(correspondingDocumentUrl, prefixes);
78
+ },
79
+ canonicalize: canonicalizeDocumentationUrl,
80
+ requestUrl(documentUrl) {
81
+ if (documentUrl.pathname.startsWith("/documentation/")) {
82
+ return new URL(`/tutorials/data${documentUrl.pathname.toLowerCase()}.json`, documentUrl.origin);
83
+ }
84
+ return documentUrl;
85
+ },
86
+ responseFormat(documentUrl) {
87
+ return documentUrl.pathname.startsWith("/documentation/") ? "docc-json" : "html";
88
+ },
89
+ };
90
+ }
91
+ export const appleProvider = appleDocCProvider("apple", "Apple Developer Documentation", [
92
+ "/documentation",
93
+ "/design/human-interface-guidelines",
94
+ ]);
95
+ export const appleReleasesProvider = appleDocCProvider("apple-releases", "Apple platform and Xcode release notes", [
96
+ "/documentation/xcode-release-notes",
97
+ "/documentation/ios-ipados-release-notes",
98
+ "/documentation/macos-release-notes",
99
+ "/documentation/tvos-release-notes",
100
+ "/documentation/watchos-release-notes",
101
+ "/documentation/visionos-release-notes",
102
+ ]);
103
+ const swiftEvolutionOrigins = [
104
+ { hostname: "www.swift.org", prefixes: ["/swift-evolution"] },
105
+ {
106
+ hostname: "github.com",
107
+ prefixes: ["/swiftlang/swift-evolution/blob/main/proposals"],
108
+ },
109
+ ];
110
+ export const swiftEvolutionProvider = {
111
+ id: "swift-evolution",
112
+ platform: "apple",
113
+ displayName: "Swift Evolution",
114
+ accepts(url) {
115
+ return acceptsOrigin(url, swiftEvolutionOrigins);
116
+ },
117
+ acceptsRequest(url) {
118
+ return (this.accepts(url) ||
119
+ (isSecurePublicUrl(url, "raw.githubusercontent.com") &&
120
+ hasAllowedPath(url, ["/swiftlang/swift-evolution/refs/heads/main/proposals"])));
121
+ },
122
+ canonicalize: canonicalizeDocumentationUrl,
123
+ requestUrl(documentUrl) {
124
+ const match = documentUrl.pathname.match(/^\/swiftlang\/swift-evolution\/blob\/main\/proposals\/(.+\.md)$/);
125
+ return match
126
+ ? new URL(`https://raw.githubusercontent.com/swiftlang/swift-evolution/refs/heads/main/proposals/${match[1]}`)
127
+ : documentUrl;
128
+ },
129
+ responseFormat(documentUrl) {
130
+ return documentUrl.hostname === "github.com" ? "markdown" : "html";
131
+ },
132
+ };
133
+ const androidPrefixes = [
134
+ "/build",
135
+ "/develop",
136
+ "/guide",
137
+ "/jetpack",
138
+ "/kotlin",
139
+ "/reference",
140
+ "/studio",
141
+ "/topic",
142
+ "/training",
143
+ ];
144
+ export const androidProvider = htmlProvider("android", "android", "Android and Google Play services documentation", [
145
+ { hostname: "developer.android.com", prefixes: androidPrefixes },
146
+ { hostname: "developers.google.com", prefixes: ["/android/reference"] },
147
+ ]);
148
+ export const androidReleasesProvider = htmlProvider("android-releases", "android", "Android platform release notes and behavior changes", [{ hostname: "developer.android.com", prefixes: ["/about/versions"] }]);
149
+ export const media3Provider = htmlProvider("media3", "android", "Jetpack Media3 documentation", [
150
+ {
151
+ hostname: "developer.android.com",
152
+ prefixes: ["/media/media3", "/jetpack/androidx/releases/media3", "/reference/androidx/media3"],
153
+ },
154
+ ]);
155
+ export const glideProvider = htmlProvider("glide", "android", "Glide documentation", [
156
+ {
157
+ hostname: "bumptech.github.io",
158
+ prefixes: ["/glide/doc", "/glide/javadocs"],
159
+ },
160
+ ]);
161
+ // OkHttp moved from Square to the Commonhaus-backed Lysine organization in 2026.
162
+ // The maintainer and Commonhaus independently document the transfer:
163
+ // https://jakewharton.com/the-lysine-contingency/
164
+ // https://www.commonhaus.org/activity/315.html
165
+ // Use its canonical project domain instead of coupling trust to either GitHub owner name.
166
+ export const okHttpProvider = htmlProvider("okhttp", "android", "OkHttp project documentation", [
167
+ { hostname: "lysine.dev", prefixes: ["/okhttp"] },
168
+ ]);
169
+ export const kotlinCoroutinesProvider = htmlProvider("kotlin-coroutines", "android", "Kotlin coroutines documentation", [
170
+ {
171
+ hostname: "kotlinlang.org",
172
+ prefixes: ["/docs", "/api/kotlinx.coroutines"],
173
+ },
174
+ ]);
175
+ export const gradleProvider = htmlProvider("gradle", "android", "Gradle documentation", [
176
+ { hostname: "docs.gradle.org", prefixes: ["/current"] },
177
+ ]);
178
+ export const agpProvider = htmlProvider("agp", "android", "Android Gradle plugin documentation", [
179
+ {
180
+ hostname: "developer.android.com",
181
+ prefixes: ["/build", "/reference/tools/gradle-api"],
182
+ },
183
+ ]);
184
+ const jetbrainsIssueOrigins = [
185
+ {
186
+ hostname: "youtrack.jetbrains.com",
187
+ prefixes: ["/issue", "/projects/IDEA/issues"],
188
+ },
189
+ ];
190
+ export const jetbrainsIssuesProvider = {
191
+ id: "jetbrains-issues",
192
+ platform: "android",
193
+ displayName: "JetBrains YouTrack issues",
194
+ accepts(url) {
195
+ return acceptsOrigin(url, jetbrainsIssueOrigins);
196
+ },
197
+ acceptsRequest(url) {
198
+ return (this.accepts(url) ||
199
+ (isSecurePublicUrl(url, "youtrack.jetbrains.com") &&
200
+ /^\/api\/issues\/[A-Z][A-Z0-9]+-\d+$/.test(url.pathname)));
201
+ },
202
+ canonicalize: canonicalizeDocumentationUrl,
203
+ requestUrl(documentUrl) {
204
+ const issueId = documentUrl.pathname.match(/(?:^|\/)([A-Z][A-Z0-9]+-\d+)(?:\/|$)/)?.[1];
205
+ if (!issueId)
206
+ return documentUrl;
207
+ const request = new URL(`https://youtrack.jetbrains.com/api/issues/${issueId}`);
208
+ request.searchParams.set("fields", "idReadable,summary,description,customFields(name,value(name)),comments(text,author(name),created)");
209
+ return request;
210
+ },
211
+ responseFormat() {
212
+ return "youtrack-json";
213
+ },
214
+ };
215
+ export const expoProvider = htmlProvider("expo", "react-native", "Expo documentation", [
216
+ { hostname: "docs.expo.dev", prefixes: [""] },
217
+ ]);
218
+ export const reactNativeProvider = htmlProvider("react-native", "react-native", "React Native documentation", [
219
+ {
220
+ hostname: "reactnative.dev",
221
+ prefixes: ["/docs", "/architecture"],
222
+ },
223
+ ]);
224
+ export const reactNativeReanimatedProvider = htmlProvider("react-native-reanimated", "react-native", "React Native Reanimated documentation", [
225
+ {
226
+ hostname: "docs.swmansion.com",
227
+ prefixes: ["/react-native-reanimated/docs"],
228
+ },
229
+ ], true);
230
+ export const reactNativeGestureHandlerProvider = htmlProvider("react-native-gesture-handler", "react-native", "React Native Gesture Handler documentation", [
231
+ {
232
+ hostname: "docs.swmansion.com",
233
+ prefixes: ["/react-native-gesture-handler/docs"],
234
+ },
235
+ ], true);
236
+ const reactNativeScreensOrigins = [
237
+ { hostname: "docs.swmansion.com", prefixes: ["/react-native-screens"] },
238
+ {
239
+ hostname: "github.com",
240
+ prefixes: ["/software-mansion/react-native-screens/blob/main/README.md"],
241
+ },
242
+ ];
243
+ export const reactNativeScreensProvider = {
244
+ id: "react-native-screens",
245
+ platform: "react-native",
246
+ displayName: "React Native Screens documentation",
247
+ accepts(url) {
248
+ return acceptsOrigin(url, reactNativeScreensOrigins);
249
+ },
250
+ acceptsRequest(url) {
251
+ return (this.accepts(url) ||
252
+ (isSecurePublicUrl(url, "raw.githubusercontent.com") &&
253
+ hasAllowedPath(url, ["/software-mansion/react-native-screens/refs/heads/main/README.md"])));
254
+ },
255
+ canonicalize: canonicalizeDocumentationUrl,
256
+ requestUrl(documentUrl) {
257
+ return documentUrl.hostname === "github.com"
258
+ ? new URL("https://raw.githubusercontent.com/software-mansion/react-native-screens/refs/heads/main/README.md")
259
+ : documentUrl;
260
+ },
261
+ responseFormat(documentUrl) {
262
+ return documentUrl.hostname === "github.com" ? "markdown" : "html";
263
+ },
264
+ };
265
+ export const reactNativeWorkletsProvider = htmlProvider("react-native-worklets", "react-native", "React Native Worklets documentation", [
266
+ {
267
+ hostname: "docs.swmansion.com",
268
+ prefixes: ["/react-native-worklets/docs"],
269
+ },
270
+ ], true);
271
+ const providers = {
272
+ apple: appleProvider,
273
+ "apple-releases": appleReleasesProvider,
274
+ "swift-evolution": swiftEvolutionProvider,
275
+ android: androidProvider,
276
+ "android-releases": androidReleasesProvider,
277
+ media3: media3Provider,
278
+ glide: glideProvider,
279
+ okhttp: okHttpProvider,
280
+ "kotlin-coroutines": kotlinCoroutinesProvider,
281
+ gradle: gradleProvider,
282
+ agp: agpProvider,
283
+ "jetbrains-issues": jetbrainsIssuesProvider,
284
+ expo: expoProvider,
285
+ "react-native": reactNativeProvider,
286
+ "react-native-reanimated": reactNativeReanimatedProvider,
287
+ "react-native-gesture-handler": reactNativeGestureHandlerProvider,
288
+ "react-native-screens": reactNativeScreensProvider,
289
+ "react-native-worklets": reactNativeWorkletsProvider,
290
+ };
291
+ export function getProvider(provider) {
292
+ return providers[provider];
293
+ }
294
+ export function resolveAllowedUrl(provider, rawUrl, baseUrl) {
295
+ let parsed;
296
+ try {
297
+ parsed = baseUrl ? new URL(rawUrl, baseUrl) : new URL(rawUrl);
298
+ }
299
+ catch {
300
+ throw new Error(`Invalid ${provider.id} documentation URL`);
301
+ }
302
+ const canonical = provider.canonicalize(parsed);
303
+ if (!provider.accepts(canonical)) {
304
+ throw new Error(`URL is outside the ${provider.id} documentation allowlist`);
305
+ }
306
+ return canonical;
307
+ }
308
+ export function resolveAllowedRequestUrl(provider, rawUrl, baseUrl) {
309
+ let parsed;
310
+ try {
311
+ parsed = baseUrl ? new URL(rawUrl, baseUrl) : new URL(rawUrl);
312
+ }
313
+ catch {
314
+ throw new Error(`Invalid ${provider.id} request URL`);
315
+ }
316
+ parsed.hash = "";
317
+ parsed.hostname = parsed.hostname.toLowerCase();
318
+ if (!provider.acceptsRequest(parsed)) {
319
+ throw new Error(`Request URL is outside the ${provider.id} network allowlist`);
320
+ }
321
+ return parsed;
322
+ }