@thallylabs/migrate 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1421 @@
1
+ // src/repository.ts
2
+ import { spawn } from "child_process";
3
+ import {
4
+ existsSync as existsSync2,
5
+ lstatSync,
6
+ readFileSync as readFileSync2,
7
+ readdirSync
8
+ } from "fs";
9
+ import { basename, dirname as dirname2, extname as extname2, relative as relative3 } from "path";
10
+
11
+ // src/mdx.ts
12
+ import matter from "gray-matter";
13
+ function titleFromId(id) {
14
+ const value = id.split("/").at(-1) ?? id;
15
+ if (value === "introduction") return "Introduction";
16
+ return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
17
+ }
18
+ function firstParagraph(content) {
19
+ for (const block of content.split(/\n\s*\n/)) {
20
+ const value = block.replace(/\s+/g, " ").trim();
21
+ if (!value || /^(?:#|```|:::|<|import\s|export\s)/.test(value)) continue;
22
+ return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").slice(0, 240);
23
+ }
24
+ return "";
25
+ }
26
+ function normalizeMdx(body) {
27
+ return body.replace(/<!--([\s\S]*?)-->/g, (_match, content) => `{/*${content}*/}`).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
28
+ }
29
+ function parseMarkdownPage(input) {
30
+ const parsed = matter(input.raw);
31
+ const body = normalizeMdx(parsed.content).trim();
32
+ if (parsed.data.openapi && !body) return null;
33
+ const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
34
+ const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : titleFromId(input.navigationId ?? input.id);
35
+ const description = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : firstParagraph(body);
36
+ return {
37
+ id: input.id,
38
+ navigationId: input.navigationId ?? input.id,
39
+ locale: input.locale,
40
+ title,
41
+ description,
42
+ keywords,
43
+ body,
44
+ source: input.source
45
+ };
46
+ }
47
+
48
+ // src/navigation.ts
49
+ import { existsSync, readFileSync } from "fs";
50
+ import { dirname, extname, relative as relative2 } from "path";
51
+
52
+ // src/path.ts
53
+ import { isAbsolute, relative, resolve, sep } from "path";
54
+ var SAFE_SEGMENT = /[^a-z0-9._-]+/g;
55
+ function slugifySegment(value) {
56
+ let decoded = value;
57
+ try {
58
+ decoded = decodeURIComponent(value);
59
+ } catch {
60
+ }
61
+ return decoded.toLowerCase().replace(/\.(?:html?|mdx?)$/i, "").replace(SAFE_SEGMENT, "-").replace(/(^-|-$)/g, "");
62
+ }
63
+ function pageIdFromReference(value) {
64
+ const withoutQuery = value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^\/+/, "");
65
+ if (!withoutQuery || withoutQuery.includes("\0")) return null;
66
+ const rawSegments = withoutQuery.split("/").filter(Boolean);
67
+ if (rawSegments.some((segment) => segment === ".." || segment === ".")) return null;
68
+ const last = rawSegments.at(-1)?.replace(/\.(?:mdx?|rst|txt)$/i, "") ?? "";
69
+ const baseSegments = /^(?:index|readme)$/i.test(last) ? rawSegments.slice(0, -1) : rawSegments;
70
+ if (!/^(?:index|readme)$/i.test(last)) {
71
+ baseSegments[baseSegments.length - 1] = last;
72
+ }
73
+ const segments = baseSegments.map(slugifySegment).filter(Boolean);
74
+ return segments.join("/") || "introduction";
75
+ }
76
+ function resolveWithin(root, candidate) {
77
+ if (isAbsolute(candidate) || candidate.includes("\0")) {
78
+ throw new Error(`Unsafe migration path: ${candidate}`);
79
+ }
80
+ const target = resolve(root, candidate);
81
+ const fromRoot = relative(resolve(root), target);
82
+ if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
83
+ throw new Error(`Migration path escapes its root: ${candidate}`);
84
+ }
85
+ return target;
86
+ }
87
+ function normalizeAssetPath(value) {
88
+ const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
89
+ if (!normalized || normalized.includes("\0")) return null;
90
+ const segments = normalized.split("/").filter(Boolean);
91
+ if (segments.some((segment) => segment === "." || segment === "..")) return null;
92
+ return segments.join("/");
93
+ }
94
+
95
+ // src/navigation.ts
96
+ var LANGUAGE_LABELS = {
97
+ ar: "Arabic",
98
+ de: "German",
99
+ en: "English",
100
+ es: "Spanish",
101
+ fr: "French",
102
+ hi: "Hindi",
103
+ it: "Italian",
104
+ ja: "Japanese",
105
+ ko: "Korean",
106
+ nl: "Dutch",
107
+ pl: "Polish",
108
+ pt: "Portuguese",
109
+ ru: "Russian",
110
+ tr: "Turkish",
111
+ uk: "Ukrainian",
112
+ zh: "Chinese"
113
+ };
114
+ function objectValue(value) {
115
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
116
+ }
117
+ function labelFor(value, fallback) {
118
+ for (const key of ["tab", "group", "anchor", "product", "dropdown", "version", "menu", "label", "name", "title"]) {
119
+ if (typeof value[key] === "string" && value[key]) return String(value[key]);
120
+ }
121
+ return fallback;
122
+ }
123
+ function localReference(value) {
124
+ return value.split("#", 1)[0];
125
+ }
126
+ function jsonPointer(root, pointer) {
127
+ if (!pointer || pointer === "#") return root;
128
+ const tokens = pointer.replace(/^#\/?/, "").split("/").filter(Boolean);
129
+ return tokens.reduce((current, token) => {
130
+ const object = objectValue(current);
131
+ if (!object) return void 0;
132
+ return object[token.replace(/~1/g, "/").replace(/~0/g, "~")];
133
+ }, root);
134
+ }
135
+ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
136
+ if (Array.isArray(value)) {
137
+ return value.map((entry) => resolveJsonReferences(entry, currentFile, repositoryRoot, stack));
138
+ }
139
+ const object = objectValue(value);
140
+ if (!object) return value;
141
+ if (typeof object.$ref === "string") {
142
+ const [filePart, pointer = ""] = object.$ref.split("#", 2);
143
+ const referencedFile = filePart ? resolveWithin(dirname(currentFile), filePart) : currentFile;
144
+ const relativePath = relative2(repositoryRoot, referencedFile);
145
+ resolveWithin(repositoryRoot, relativePath);
146
+ const stackKey = `${referencedFile}#${pointer}`;
147
+ if (stack.has(stackKey)) throw new Error(`Circular Mintlify $ref: ${object.$ref}`);
148
+ stack.add(stackKey);
149
+ const referencedRoot = JSON.parse(readFileSync(referencedFile, "utf8"));
150
+ const referenced = jsonPointer(referencedRoot, pointer ? `#${pointer}` : "");
151
+ const resolved = resolveJsonReferences(referenced, referencedFile, repositoryRoot, stack);
152
+ stack.delete(stackKey);
153
+ return resolved;
154
+ }
155
+ return Object.fromEntries(
156
+ Object.entries(object).map(([key, entry]) => [
157
+ key,
158
+ resolveJsonReferences(entry, currentFile, repositoryRoot, stack)
159
+ ])
160
+ );
161
+ }
162
+ function readMintlifyConfig(repositoryRoot) {
163
+ const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync);
164
+ if (!configPath) return null;
165
+ const raw = JSON.parse(readFileSync(configPath, "utf8"));
166
+ return resolveJsonReferences(raw, configPath, repositoryRoot, /* @__PURE__ */ new Set());
167
+ }
168
+ function normalizePageRef(value) {
169
+ if (/^(?:https?:)?\/\//i.test(value) || value.startsWith("#")) return null;
170
+ const ref = localReference(value).replace(/^\/+/, "").replace(/\.(?:mdx?|rst|txt)$/i, "");
171
+ return pageIdFromReference(ref);
172
+ }
173
+ function registerReference(value, context) {
174
+ const localePrefix = context.locale ? `${context.locale}/` : "";
175
+ const localizedValue = localePrefix && value.replace(/^\/+/, "").startsWith(localePrefix) ? value.replace(/^\/+/, "").slice(localePrefix.length) : value;
176
+ const navigationId = normalizePageRef(localizedValue);
177
+ if (!navigationId) return null;
178
+ const key = `${context.locale ?? ""}:${value}`;
179
+ if (!context.seenReferences.has(key)) {
180
+ context.seenReferences.add(key);
181
+ context.references.push({ ref: value, navigationId, locale: context.locale });
182
+ }
183
+ return navigationId;
184
+ }
185
+ function convertPage(value, context) {
186
+ if (typeof value === "string") return registerReference(value, context);
187
+ const object = objectValue(value);
188
+ if (!object) return null;
189
+ if (typeof object.page === "string") return registerReference(object.page, context);
190
+ const pages = Array.isArray(object.pages) ? object.pages : [];
191
+ if ("group" in object || pages.length > 0) {
192
+ const children = [];
193
+ if (typeof object.root === "string") {
194
+ const root = registerReference(object.root, context);
195
+ if (root) children.push(root);
196
+ }
197
+ for (const page of pages) {
198
+ const converted = convertPage(page, context);
199
+ if (converted) children.push(converted);
200
+ }
201
+ if (children.length === 0) return null;
202
+ return {
203
+ group: labelFor(object, "Documentation"),
204
+ ...typeof object.icon === "string" ? { icon: object.icon } : {},
205
+ pages: children
206
+ };
207
+ }
208
+ return null;
209
+ }
210
+ function convertGroups(values, context, fallbackGroup) {
211
+ const groups = [];
212
+ const loosePages = [];
213
+ for (const value of values) {
214
+ const converted = convertPage(value, context);
215
+ if (!converted) continue;
216
+ if (typeof converted === "string") loosePages.push(converted);
217
+ else groups.push(converted);
218
+ }
219
+ if (loosePages.length > 0) groups.unshift({ group: fallbackGroup, pages: loosePages });
220
+ return groups;
221
+ }
222
+ function convertContainerToTabs(containerValue, context, fallbackTab) {
223
+ const container = objectValue(containerValue);
224
+ if (!container) return [];
225
+ for (const key of ["tabs", "anchors", "products", "dropdowns", "versions", "menus"]) {
226
+ if (!Array.isArray(container[key])) continue;
227
+ const tabs = container[key].flatMap((value, index) => {
228
+ const object = objectValue(value);
229
+ if (!object) return [];
230
+ const tab = labelFor(object, `${fallbackTab} ${index + 1}`);
231
+ if (typeof object.href === "string" && !object.pages && !object.groups) {
232
+ return [{ tab, href: object.href }];
233
+ }
234
+ const nested = convertContainerToTabs(object, context, tab);
235
+ if (nested.length > 0) {
236
+ if (nested.length === 1) return [{ ...nested[0], tab }];
237
+ return nested.map((item) => ({ ...item, tab: `${tab}: ${item.tab}` }));
238
+ }
239
+ return [];
240
+ });
241
+ if (tabs.length > 0) return tabs;
242
+ }
243
+ const rawPages = [
244
+ ...Array.isArray(container.groups) ? container.groups : [],
245
+ ...Array.isArray(container.pages) ? container.pages : []
246
+ ];
247
+ if (typeof container.root === "string") rawPages.unshift(container.root);
248
+ const groups = convertGroups(rawPages, context, "Overview");
249
+ return groups.length > 0 ? [{ tab: fallbackTab, groups }] : [];
250
+ }
251
+ function projectMintlifyNavigation(config) {
252
+ const warnings = [];
253
+ const references = [];
254
+ const seenReferences = /* @__PURE__ */ new Set();
255
+ const navigation = objectValue(config.navigation) ?? config;
256
+ const languages = Array.isArray(navigation.languages) ? navigation.languages.map(objectValue).filter((value) => Boolean(value)) : [];
257
+ let tabs = [];
258
+ let i18n;
259
+ if (languages.length > 0) {
260
+ const defaultLanguage = languages.find((entry) => entry.language === "en") ?? languages[0];
261
+ const defaultLocale = String(defaultLanguage.language ?? "en");
262
+ const locales = languages.map((entry) => {
263
+ const code = String(entry.language ?? entry.locale ?? "en");
264
+ return { code, label: LANGUAGE_LABELS[code] ?? code.toUpperCase() };
265
+ });
266
+ i18n = { defaultLocale, locales };
267
+ for (const language of languages) {
268
+ const locale = String(language.language ?? language.locale ?? defaultLocale);
269
+ const context = {
270
+ locale,
271
+ references,
272
+ seenReferences
273
+ };
274
+ const languageTabs = convertContainerToTabs(language, context, "Documentation");
275
+ if (language === defaultLanguage) tabs = languageTabs;
276
+ }
277
+ } else {
278
+ tabs = convertContainerToTabs(navigation, { references, seenReferences }, "Documentation");
279
+ if (tabs.length === 0 && Array.isArray(config.navigation)) {
280
+ const groups = convertGroups(config.navigation, { references, seenReferences }, "Overview");
281
+ if (groups.length > 0) tabs = [{ tab: "Documentation", groups }];
282
+ }
283
+ }
284
+ if (tabs.length === 0) {
285
+ warnings.push({
286
+ code: "unsupported-config",
287
+ message: "Mintlify navigation could not be projected; generated navigation will be used."
288
+ });
289
+ }
290
+ if (!tabs.some((tab) => tab.tab.toLowerCase() === "changelog")) {
291
+ tabs.push({ tab: "Changelog", href: "/changelog" });
292
+ }
293
+ const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
294
+ const redirect = objectValue(value);
295
+ if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
296
+ return [{
297
+ source: redirect.source,
298
+ destination: redirect.destination,
299
+ ...typeof redirect.permanent === "boolean" ? { permanent: redirect.permanent } : {}
300
+ }];
301
+ }) : [];
302
+ return {
303
+ docsConfig: { tabs, ...i18n ? { i18n } : {}, ...redirects.length > 0 ? { redirects } : {} },
304
+ pageReferences: references,
305
+ warnings
306
+ };
307
+ }
308
+ function titleCase(value) {
309
+ return value.split(/[-_]/).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
310
+ }
311
+ function buildNavigationFromPages(pages) {
312
+ const ids = pages.filter((page) => !page.locale || page.locale === "en").map((page) => page.navigationId);
313
+ const ordered = [...new Set(ids)];
314
+ const buckets = /* @__PURE__ */ new Map();
315
+ for (const id of ordered) {
316
+ const segment = id.includes("/") ? id.split("/", 1)[0] : "overview";
317
+ const bucket = buckets.get(segment) ?? [];
318
+ bucket.push(id);
319
+ buckets.set(segment, bucket);
320
+ }
321
+ const groups = [...buckets].map(([segment, pageIds]) => ({
322
+ group: segment === "overview" ? "Overview" : titleCase(segment),
323
+ pages: pageIds
324
+ }));
325
+ return { tabs: [{ tab: "Documentation", groups }] };
326
+ }
327
+ function isDocumentationExtension(filename) {
328
+ return [".md", ".mdx", ".rst", ".txt"].includes(extname(filename).toLowerCase());
329
+ }
330
+
331
+ // src/repository.ts
332
+ var MAX_SOURCE_FILES = 5e3;
333
+ var MAX_PAGE_BYTES = 2e6;
334
+ var MAX_ASSET_BYTES = 25e6;
335
+ var MAX_TOTAL_ASSET_BYTES = 1e8;
336
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
337
+ ".git",
338
+ ".github",
339
+ ".next",
340
+ ".turbo",
341
+ ".vercel",
342
+ ".vscode",
343
+ "node_modules",
344
+ "dist",
345
+ "build",
346
+ "coverage"
347
+ ]);
348
+ var ASSET_DIRECTORIES = /* @__PURE__ */ new Set(["assets", "images", "img", "media", "public", "static"]);
349
+ var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
350
+ ".avif",
351
+ ".bmp",
352
+ ".gif",
353
+ ".ico",
354
+ ".jpeg",
355
+ ".jpg",
356
+ ".mp3",
357
+ ".mp4",
358
+ ".pdf",
359
+ ".png",
360
+ ".svg",
361
+ ".webm",
362
+ ".webp"
363
+ ]);
364
+ var REPOSITORY_ONLY_DOCUMENTS = /* @__PURE__ */ new Set([
365
+ "agents.md",
366
+ "claude.md",
367
+ "code_of_conduct.md",
368
+ "contributing.md",
369
+ "license.md",
370
+ "readme.md",
371
+ "security.md"
372
+ ]);
373
+ var SNIPPET_DIRECTORIES = /* @__PURE__ */ new Set(["snippets", "_snippets", "partials", "_partials"]);
374
+ var OPENAPI_FILENAMES = /* @__PURE__ */ new Set([
375
+ "openapi.json",
376
+ "openapi.yaml",
377
+ "openapi.yml",
378
+ "swagger.json",
379
+ "swagger.yaml",
380
+ "swagger.yml"
381
+ ]);
382
+ function parseGitHubRepositoryUrl(rawUrl) {
383
+ let url;
384
+ try {
385
+ url = new URL(rawUrl);
386
+ } catch {
387
+ throw new Error(`Invalid GitHub URL: ${rawUrl}`);
388
+ }
389
+ if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "github.com") {
390
+ throw new Error("Repository migrations require an https://github.com URL.");
391
+ }
392
+ if (url.username || url.password || url.port) {
393
+ throw new Error("GitHub repository URLs cannot include credentials or custom ports.");
394
+ }
395
+ const segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
396
+ const [owner, rawRepo] = segments;
397
+ const repo = rawRepo?.replace(/\.git$/i, "");
398
+ const safeName = /^[A-Za-z0-9_.-]+$/;
399
+ if (!owner || !repo || !safeName.test(owner) || !safeName.test(repo)) {
400
+ throw new Error("GitHub URL must include a valid owner and repository name.");
401
+ }
402
+ let branch = "HEAD";
403
+ let docsDir = "";
404
+ if (segments[2] === "tree" && segments[3]) {
405
+ branch = segments[3];
406
+ docsDir = segments.slice(4).join("/");
407
+ }
408
+ return {
409
+ owner,
410
+ repo,
411
+ branch,
412
+ docsDir,
413
+ cloneUrl: `https://github.com/${owner}/${repo}.git`
414
+ };
415
+ }
416
+ async function cloneGitHubRepository(source, targetDir) {
417
+ const args = ["clone", "--depth", "1", "--single-branch"];
418
+ if (source.branch !== "HEAD") args.push("--branch", source.branch);
419
+ args.push("--", source.cloneUrl, targetDir);
420
+ await new Promise((resolve2, reject) => {
421
+ const child = spawn("git", args, { stdio: ["ignore", "ignore", "pipe"] });
422
+ let stderr = "";
423
+ child.stderr.setEncoding("utf8");
424
+ child.stderr.on("data", (chunk) => {
425
+ if (stderr.length < 16e3) stderr += chunk;
426
+ });
427
+ child.on("error", reject);
428
+ child.on("close", (code) => {
429
+ if (code === 0) resolve2();
430
+ else reject(new Error(`Failed to clone ${source.owner}/${source.repo}: ${stderr.trim() || `git exited ${code}`}`));
431
+ });
432
+ });
433
+ }
434
+ function detectRepositoryPlatform(repositoryDir) {
435
+ if (existsSync2(resolveWithin(repositoryDir, "mint.json"))) return "mintlify";
436
+ const docsJson = resolveWithin(repositoryDir, "docs.json");
437
+ if (existsSync2(docsJson)) {
438
+ try {
439
+ const config = JSON.parse(readFileSync2(docsJson, "utf8"));
440
+ if (String(config.$schema ?? "").includes("mintlify") || "navigation" in config) return "mintlify";
441
+ if (Array.isArray(config.tabs)) return "thally";
442
+ } catch {
443
+ }
444
+ }
445
+ if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) => existsSync2(resolveWithin(repositoryDir, name)))) return "docusaurus";
446
+ if (existsSync2(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
447
+ if (existsSync2(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
448
+ if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync2(resolveWithin(repositoryDir, name)))) return "starlight";
449
+ if (["pages/_meta.json", "_meta.json"].some((name) => existsSync2(resolveWithin(repositoryDir, name)))) return "nextra";
450
+ return "unknown";
451
+ }
452
+ function containsMarkdown(directory, depth = 0) {
453
+ if (depth > 4) return false;
454
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
455
+ if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
456
+ const path = resolveWithin(directory, entry.name);
457
+ if (entry.isFile() && [".md", ".mdx"].includes(extname2(entry.name).toLowerCase())) return true;
458
+ if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
459
+ }
460
+ return false;
461
+ }
462
+ function detectRepositoryDocsDir(repositoryDir) {
463
+ for (const candidate of ["docs", "documentation", "content", "pages", "src/content", "src/pages", "guide", "guides", ""]) {
464
+ const path = resolveWithin(repositoryDir, candidate);
465
+ if (existsSync2(path) && lstatSync(path).isDirectory() && containsMarkdown(path)) return candidate;
466
+ }
467
+ return "";
468
+ }
469
+ function scanFiles(root) {
470
+ const files = [];
471
+ function visit(directory) {
472
+ if (files.length >= MAX_SOURCE_FILES) return;
473
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
474
+ if (files.length >= MAX_SOURCE_FILES) return;
475
+ if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
476
+ const path = resolveWithin(directory, entry.name);
477
+ if (entry.isDirectory()) visit(path);
478
+ else if (entry.isFile()) files.push({ absolutePath: path, relativePath: relative3(root, path).replace(/\\/g, "/") });
479
+ }
480
+ }
481
+ visit(root);
482
+ return files;
483
+ }
484
+ function normalizedReferenceKey(value) {
485
+ return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "").replace(/\/(?:index|readme)$/i, "").replace(/^(?:index|readme)$/i, "") || "introduction";
486
+ }
487
+ function findOpenApi(files) {
488
+ return files.find((file) => OPENAPI_FILENAMES.has(basename(file.relativePath).toLowerCase())) ?? null;
489
+ }
490
+ function withoutFrontmatter(value) {
491
+ return value.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
492
+ }
493
+ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0) {
494
+ if (depth >= 8) return raw;
495
+ const snippets = /* @__PURE__ */ new Map();
496
+ const withoutImports = raw.replace(
497
+ /^import\s+([A-Z][A-Za-z0-9_]*)\s+from\s+['"]([^'"]+\.mdx?)['"]\s*;?\s*$/gm,
498
+ (_statement, componentName, sourcePath) => {
499
+ try {
500
+ const candidate = sourcePath.startsWith("/") ? resolveWithin(repositoryRoot, sourcePath.replace(/^\/+/, "")) : resolveWithin(dirname2(currentFile), sourcePath);
501
+ resolveWithin(repositoryRoot, relative3(repositoryRoot, candidate));
502
+ if (!existsSync2(candidate) || !lstatSync(candidate).isFile()) throw new Error("file not found");
503
+ const nested = inlineMdxSnippets(
504
+ withoutFrontmatter(readFileSync2(candidate, "utf8")),
505
+ candidate,
506
+ repositoryRoot,
507
+ warnings,
508
+ depth + 1
509
+ );
510
+ snippets.set(componentName, nested);
511
+ return "";
512
+ } catch {
513
+ warnings.push({
514
+ code: "missing-page",
515
+ message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
516
+ source: relative3(repositoryRoot, currentFile).replace(/\\/g, "/")
517
+ });
518
+ snippets.set(componentName, `{/* Missing imported snippet: ${sourcePath} */}`);
519
+ return "";
520
+ }
521
+ }
522
+ );
523
+ let result = withoutImports;
524
+ for (const [componentName, snippet] of snippets) {
525
+ result = result.replace(new RegExp(`<${componentName}(?:\\s[^>]*)?\\s*/>`, "g"), snippet).replace(new RegExp(`<${componentName}(?:\\s[^>]*)?>(?:[\\s\\S]*?)<\\/${componentName}>`, "g"), snippet);
526
+ }
527
+ return result;
528
+ }
529
+ function injectOpenApi(config, filename) {
530
+ const tabs = config.tabs.filter((tab) => !tab.tab.toLowerCase().includes("api"));
531
+ const apiTab = {
532
+ tab: "API Reference",
533
+ api: { source: `/${filename}` }
534
+ };
535
+ const changelog = tabs.findIndex((tab) => tab.tab.toLowerCase() === "changelog");
536
+ if (changelog >= 0) tabs.splice(changelog, 0, apiTab);
537
+ else tabs.push(apiTab);
538
+ return { ...config, tabs };
539
+ }
540
+ function migrateRepository(options) {
541
+ const repositoryDir = options.repositoryDir;
542
+ const platform = options.platform ?? detectRepositoryPlatform(repositoryDir);
543
+ const warnings = [];
544
+ let docsConfig = { tabs: [] };
545
+ const referenceMap = /* @__PURE__ */ new Map();
546
+ const referenceOrder = /* @__PURE__ */ new Map();
547
+ if (platform === "mintlify") {
548
+ try {
549
+ const config = readMintlifyConfig(repositoryDir);
550
+ if (config) {
551
+ const projected = projectMintlifyNavigation(config);
552
+ docsConfig = projected.docsConfig;
553
+ warnings.push(...projected.warnings);
554
+ for (const [index, reference] of projected.pageReferences.entries()) {
555
+ const key = normalizedReferenceKey(reference.ref);
556
+ referenceMap.set(key, reference);
557
+ referenceOrder.set(key, index);
558
+ }
559
+ }
560
+ } catch (error) {
561
+ warnings.push({
562
+ code: "unsupported-config",
563
+ message: `Mintlify config could not be read: ${error instanceof Error ? error.message : String(error)}`
564
+ });
565
+ }
566
+ }
567
+ const configuredDocsDir = options.docsDir ?? (platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir));
568
+ const contentRoot = resolveWithin(repositoryDir, configuredDocsDir);
569
+ if (!existsSync2(contentRoot) || !lstatSync(contentRoot).isDirectory()) {
570
+ throw new Error(`Documentation directory does not exist: ${configuredDocsDir || "."}`);
571
+ }
572
+ const files = scanFiles(contentRoot);
573
+ const pages = [];
574
+ const assets = [];
575
+ const seenPageIds = /* @__PURE__ */ new Set();
576
+ let skipped = 0;
577
+ const localeConfig = docsConfig.i18n;
578
+ const pageFiles = [...files].sort((left, right) => {
579
+ const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
580
+ const rightOrder = referenceOrder.get(normalizedReferenceKey(right.relativePath)) ?? Number.MAX_SAFE_INTEGER;
581
+ return leftOrder - rightOrder || left.relativePath.localeCompare(right.relativePath);
582
+ });
583
+ for (const file of pageFiles) {
584
+ if (!isDocumentationExtension(file.relativePath)) continue;
585
+ if (file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()))) {
586
+ skipped++;
587
+ continue;
588
+ }
589
+ const rootFilename = !file.relativePath.includes("/") ? basename(file.relativePath).toLowerCase() : "";
590
+ if (REPOSITORY_ONLY_DOCUMENTS.has(rootFilename)) {
591
+ skipped++;
592
+ continue;
593
+ }
594
+ if (![".md", ".mdx"].includes(extname2(file.relativePath).toLowerCase())) {
595
+ skipped++;
596
+ warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
597
+ continue;
598
+ }
599
+ const size = lstatSync(file.absolutePath).size;
600
+ if (size > MAX_PAGE_BYTES) {
601
+ skipped++;
602
+ warnings.push({ code: "skipped-file", message: "Page exceeded the 2 MB repository import limit.", source: file.relativePath });
603
+ continue;
604
+ }
605
+ const key = normalizedReferenceKey(file.relativePath);
606
+ const referenced = referenceMap.get(key);
607
+ let locale = referenced?.locale;
608
+ let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath);
609
+ if (!referenced && localeConfig) {
610
+ const prefix = file.relativePath.split("/", 1)[0];
611
+ if (localeConfig.locales.some((entry) => entry.code === prefix)) {
612
+ locale = prefix;
613
+ navigationId = pageIdFromReference(file.relativePath.slice(prefix.length + 1));
614
+ }
615
+ }
616
+ if (!navigationId) {
617
+ skipped++;
618
+ warnings.push({ code: "invalid-page", message: "Page path could not be normalized safely.", source: file.relativePath });
619
+ continue;
620
+ }
621
+ const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
622
+ const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
623
+ if (seenPageIds.has(id)) {
624
+ skipped++;
625
+ warnings.push({ code: "collision", message: `Multiple source files map to ${id}; the first file was kept.`, source: file.relativePath });
626
+ continue;
627
+ }
628
+ const raw = inlineMdxSnippets(
629
+ readFileSync2(file.absolutePath, "utf8"),
630
+ file.absolutePath,
631
+ repositoryDir,
632
+ warnings
633
+ );
634
+ const page = parseMarkdownPage({
635
+ id,
636
+ navigationId,
637
+ ...locale ? { locale } : {},
638
+ raw,
639
+ source: `${options.sourceUrl}#${file.relativePath}`
640
+ });
641
+ if (!page) {
642
+ skipped++;
643
+ continue;
644
+ }
645
+ seenPageIds.add(id);
646
+ pages.push(page);
647
+ }
648
+ const discoveredReferenceKeys = new Set(files.map((file) => normalizedReferenceKey(file.relativePath)));
649
+ for (const [key] of referenceMap) {
650
+ if (!discoveredReferenceKeys.has(key)) {
651
+ warnings.push({ code: "missing-page", message: "A navigation entry did not resolve to a source page.", source: key });
652
+ }
653
+ }
654
+ let totalAssetBytes = 0;
655
+ for (const file of files) {
656
+ const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
657
+ if (!ASSET_DIRECTORIES.has(firstSegment) || !ASSET_EXTENSIONS.has(extname2(file.relativePath).toLowerCase())) continue;
658
+ const size = lstatSync(file.absolutePath).size;
659
+ if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
660
+ warnings.push({ code: "limit-reached", message: "An asset was skipped because the migration asset budget was exhausted.", source: file.relativePath });
661
+ continue;
662
+ }
663
+ const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : file.relativePath);
664
+ if (!assetPath) continue;
665
+ assets.push({ path: assetPath, content: readFileSync2(file.absolutePath) });
666
+ totalAssetBytes += size;
667
+ }
668
+ if (files.length >= MAX_SOURCE_FILES) {
669
+ warnings.push({ code: "limit-reached", message: `Repository discovery stopped at ${MAX_SOURCE_FILES} files.` });
670
+ }
671
+ if (docsConfig.tabs.length === 0) docsConfig = buildNavigationFromPages(pages);
672
+ const openApi = findOpenApi(files);
673
+ if (openApi) {
674
+ const filename = basename(openApi.relativePath);
675
+ if (!assets.some((asset) => asset.path === filename)) {
676
+ assets.push({ path: filename, content: readFileSync2(openApi.absolutePath) });
677
+ }
678
+ docsConfig = injectOpenApi(docsConfig, filename);
679
+ }
680
+ if (pages.length === 0) throw new Error("No importable Markdown or MDX pages were found in the repository.");
681
+ return {
682
+ sourceUrl: options.sourceUrl,
683
+ sourceKind: "repository",
684
+ platform,
685
+ pages,
686
+ assets,
687
+ docsConfig,
688
+ warnings,
689
+ stats: { discovered: files.length, imported: pages.length, skipped }
690
+ };
691
+ }
692
+
693
+ // src/render.ts
694
+ function yamlString(value) {
695
+ return JSON.stringify(value.replace(/\s+/g, " ").trim());
696
+ }
697
+ function renderPage(bundle, page) {
698
+ return [
699
+ "---",
700
+ `title: ${yamlString(page.title)}`,
701
+ `description: ${yamlString(page.description)}`,
702
+ page.keywords.length > 0 ? `keywords: [${page.keywords.map(yamlString).join(", ")}]` : null,
703
+ bundle.sourceKind === "url" ? `source: ${yamlString(page.source)}` : null,
704
+ "---",
705
+ "",
706
+ page.body,
707
+ ""
708
+ ].filter((line) => line !== null).join("\n");
709
+ }
710
+ function mergeMigrationConfig(existing, incoming) {
711
+ const tabs = existing.tabs.filter((tab) => tab.tab.toLowerCase() !== "changelog");
712
+ const names = new Set(tabs.map((tab) => tab.tab.toLowerCase()));
713
+ const seenPages = /* @__PURE__ */ new Set();
714
+ function recordPages(groups) {
715
+ for (const group of groups ?? []) {
716
+ for (const page of group.pages) {
717
+ if (typeof page === "string") seenPages.add(page);
718
+ else recordPages([page]);
719
+ }
720
+ }
721
+ }
722
+ for (const tab of tabs) recordPages(tab.groups);
723
+ function uniqueGroups(groups) {
724
+ return (groups ?? []).flatMap((group) => {
725
+ const pages = group.pages.flatMap((page) => {
726
+ if (typeof page === "string") {
727
+ if (seenPages.has(page)) return [];
728
+ seenPages.add(page);
729
+ return [page];
730
+ }
731
+ const [nested] = uniqueGroups([page]);
732
+ return nested ? [nested] : [];
733
+ });
734
+ return pages.length > 0 ? [{ ...group, pages }] : [];
735
+ });
736
+ }
737
+ for (const tab of incoming.tabs) {
738
+ if (tab.tab.toLowerCase() === "changelog") continue;
739
+ const uniqueTab = { ...tab, ...tab.groups ? { groups: uniqueGroups(tab.groups) } : {} };
740
+ if (tab.groups && uniqueTab.groups?.length === 0 && !tab.href && !tab.api) continue;
741
+ if (!names.has(tab.tab.toLowerCase())) {
742
+ tabs.push(uniqueTab);
743
+ names.add(tab.tab.toLowerCase());
744
+ continue;
745
+ }
746
+ const target = tabs.find((item) => item.tab.toLowerCase() === tab.tab.toLowerCase());
747
+ if (!target || !uniqueTab.groups) continue;
748
+ target.groups = [...target.groups ?? [], ...uniqueTab.groups];
749
+ }
750
+ const changelog = existing.tabs.find((tab) => tab.tab.toLowerCase() === "changelog") ?? incoming.tabs.find((tab) => tab.tab.toLowerCase() === "changelog");
751
+ if (changelog) tabs.push(changelog);
752
+ const i18n = existing.i18n || incoming.i18n ? {
753
+ defaultLocale: existing.i18n?.defaultLocale ?? incoming.i18n?.defaultLocale ?? "en",
754
+ locales: [...new Map(
755
+ [...existing.i18n?.locales ?? [], ...incoming.i18n?.locales ?? []].map((locale) => [locale.code, locale])
756
+ ).values()]
757
+ } : void 0;
758
+ return {
759
+ ...incoming,
760
+ ...existing,
761
+ tabs,
762
+ ...i18n ? { i18n } : {}
763
+ };
764
+ }
765
+ function renderMigrationFiles(bundle, options = {}) {
766
+ const config = options.existingConfig ? mergeMigrationConfig(options.existingConfig, bundle.docsConfig) : bundle.docsConfig;
767
+ return [
768
+ ...bundle.pages.map((page) => ({
769
+ path: `src/content/${page.id}.mdx`,
770
+ content: renderPage(bundle, page)
771
+ })),
772
+ ...bundle.assets.map((asset) => ({ path: `public/${asset.path}`, content: asset.content })),
773
+ { path: "docs.json", content: `${JSON.stringify(config, null, 2)}
774
+ ` }
775
+ ];
776
+ }
777
+
778
+ // src/url.ts
779
+ import { load } from "cheerio";
780
+ import TurndownService from "turndown";
781
+ var DEFAULT_MAX_PAGES = 1e3;
782
+ var DEFAULT_MAX_TOTAL_BYTES = 1e8;
783
+ var MAX_DISCOVERED_URLS = 5e3;
784
+ var MAX_LOCAL_RESPONSE_BYTES = 2e6;
785
+ var MAX_SITEMAP_DOCUMENTS = 25;
786
+ var PORTABLE_MDX_COMPONENTS = /* @__PURE__ */ new Set([
787
+ "Accordion",
788
+ "AccordionGroup",
789
+ "Badge",
790
+ "Callout",
791
+ "Card",
792
+ "CardGroup",
793
+ "Check",
794
+ "CodeGroup",
795
+ "Color",
796
+ "Column",
797
+ "Columns",
798
+ "Danger",
799
+ "Error",
800
+ "Expandable",
801
+ "File",
802
+ "Folder",
803
+ "Frame",
804
+ "Hero",
805
+ "Icon",
806
+ "Info",
807
+ "Latex",
808
+ "Mermaid",
809
+ "Note",
810
+ "Panel",
811
+ "ParamField",
812
+ "Prompt",
813
+ "PromptAssistant",
814
+ "PromptUser",
815
+ "RequestExample",
816
+ "ResponseExample",
817
+ "ResponseField",
818
+ "Step",
819
+ "Steps",
820
+ "Tab",
821
+ "Tabs",
822
+ "Tile",
823
+ "TileGroup",
824
+ "Tip",
825
+ "Tooltip",
826
+ "Tree",
827
+ "Update",
828
+ "View",
829
+ "Warning"
830
+ ]);
831
+ var LOCALE_CODES = /* @__PURE__ */ new Set([
832
+ "ar",
833
+ "cs",
834
+ "da",
835
+ "de",
836
+ "el",
837
+ "es",
838
+ "fi",
839
+ "fr",
840
+ "he",
841
+ "hi",
842
+ "hu",
843
+ "id",
844
+ "it",
845
+ "ja",
846
+ "ko",
847
+ "ms",
848
+ "nl",
849
+ "no",
850
+ "pl",
851
+ "pt",
852
+ "ro",
853
+ "ru",
854
+ "sk",
855
+ "sv",
856
+ "th",
857
+ "tr",
858
+ "uk",
859
+ "vi",
860
+ "zh"
861
+ ]);
862
+ function validateMigrationUrl(value) {
863
+ let url;
864
+ try {
865
+ url = new URL(value);
866
+ } catch {
867
+ throw new Error("Enter a complete documentation URL, including https://.");
868
+ }
869
+ if (!["http:", "https:"].includes(url.protocol)) {
870
+ throw new Error("Documentation imports support only HTTP and HTTPS URLs.");
871
+ }
872
+ if (url.username || url.password) throw new Error("Documentation URLs cannot contain credentials.");
873
+ url.hash = "";
874
+ return url;
875
+ }
876
+ async function readResponseBody(response) {
877
+ const contentLength = Number(response.headers.get("content-length") ?? 0);
878
+ if (contentLength > MAX_LOCAL_RESPONSE_BYTES) throw new Error("A documentation response exceeded the 2 MB import limit.");
879
+ if (!response.body) return "";
880
+ const reader = response.body.getReader();
881
+ const chunks = [];
882
+ let size = 0;
883
+ while (true) {
884
+ const { done, value } = await reader.read();
885
+ if (done) break;
886
+ size += value.byteLength;
887
+ if (size > MAX_LOCAL_RESPONSE_BYTES) {
888
+ await reader.cancel();
889
+ throw new Error("A documentation response exceeded the 2 MB import limit.");
890
+ }
891
+ chunks.push(value);
892
+ }
893
+ const body = new Uint8Array(size);
894
+ let offset = 0;
895
+ for (const chunk of chunks) {
896
+ body.set(chunk, offset);
897
+ offset += chunk.byteLength;
898
+ }
899
+ return new TextDecoder().decode(body);
900
+ }
901
+ var defaultMigrationFetcher = async (url, request) => {
902
+ const response = await fetch(url, {
903
+ headers: {
904
+ Accept: request.accept,
905
+ "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)"
906
+ },
907
+ redirect: "follow",
908
+ signal: AbortSignal.timeout(15e3)
909
+ });
910
+ if (!response.ok) throw new Error(`Documentation server returned ${response.status}.`);
911
+ return {
912
+ finalUrl: new URL(response.url),
913
+ body: await readResponseBody(response),
914
+ contentType: response.headers.get("content-type") ?? "",
915
+ headers: Object.fromEntries(response.headers.entries())
916
+ };
917
+ };
918
+ function docsScopePath(source) {
919
+ const path = source.pathname.replace(/\/+$/, "") || "/";
920
+ if (path === "/") return "/";
921
+ const segments = path.split("/").filter(Boolean);
922
+ return `/${segments.length > 1 ? segments[0] : segments.join("/")}`;
923
+ }
924
+ function isInScope(url, source, scopePath) {
925
+ if (url.origin !== source.origin || !["http:", "https:"].includes(url.protocol)) return false;
926
+ const path = url.pathname.replace(/\/+$/, "") || "/";
927
+ if (scopePath === "/") return true;
928
+ return path === scopePath || path === `${scopePath}.md` || path.startsWith(`${scopePath}/`);
929
+ }
930
+ function normalizeCandidate(value, base, source, scopePath) {
931
+ try {
932
+ const url = new URL(value, base);
933
+ url.hash = "";
934
+ if (!isInScope(url, source, scopePath)) return null;
935
+ if (/\.(?:avif|bmp|gif|ico|jpe?g|mp[34]|pdf|png|svg|webm|webp|zip)$/i.test(url.pathname)) return null;
936
+ return url;
937
+ } catch {
938
+ return null;
939
+ }
940
+ }
941
+ function candidateIdentity(url) {
942
+ const pathname = url.pathname.replace(/\.mdx?$/i, "").replace(/\/+$/, "") || "/";
943
+ return `${url.origin}${pathname}${url.search}`;
944
+ }
945
+ function pageIdForUrl(url, scopePath) {
946
+ let path = url.pathname.replace(/\/+$/, "");
947
+ if (scopePath !== "/") {
948
+ if (path === `${scopePath}.md`) path = "";
949
+ else if (path === scopePath) path = "";
950
+ else if (path.startsWith(`${scopePath}/`)) path = path.slice(scopePath.length + 1);
951
+ }
952
+ return pageIdFromReference(path.replace(/^\/+/, "") || "introduction");
953
+ }
954
+ function detectUrlPlatform(document) {
955
+ const value = `${document.body.slice(0, 2e5)} ${document.headers?.["x-powered-by"] ?? ""}`.toLowerCase();
956
+ if (value.includes("mintlify") || value.includes("__mintlify")) return "mintlify";
957
+ if (value.includes("docusaurus") || value.includes("__docusaurus")) return "docusaurus";
958
+ if (value.includes("gitbook") || value.includes("gitbook.io")) return "gitbook";
959
+ if (value.includes("nextra")) return "nextra";
960
+ if (value.includes("vitepress")) return "vitepress";
961
+ if (value.includes("starlight")) return "starlight";
962
+ return "generic";
963
+ }
964
+ function plainDescription(value) {
965
+ return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_>#-]/g, "").replace(/\s+/g, " ").trim().slice(0, 240);
966
+ }
967
+ function withoutFencedCode(body) {
968
+ let fenceCharacter = "";
969
+ let fenceLength = 0;
970
+ return body.split("\n").map((line) => {
971
+ const match = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
972
+ if (match && !fenceCharacter) {
973
+ fenceCharacter = match[1][0];
974
+ fenceLength = match[1].length;
975
+ return "";
976
+ }
977
+ if (match && match[1][0] === fenceCharacter && match[1].length >= fenceLength && match[2].trim() === "") {
978
+ fenceCharacter = "";
979
+ fenceLength = 0;
980
+ return "";
981
+ }
982
+ return fenceCharacter ? "" : line;
983
+ }).join("\n");
984
+ }
985
+ function sanitizeRemoteMarkdown(body) {
986
+ let fenceCharacter = "";
987
+ let fenceLength = 0;
988
+ const sanitized = body.split("\n").map((line) => {
989
+ const fence = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
990
+ if (fence && !fenceCharacter) {
991
+ fenceCharacter = fence[1][0];
992
+ fenceLength = fence[1].length;
993
+ return line;
994
+ }
995
+ if (fence && fence[1][0] === fenceCharacter && fence[1].length >= fenceLength && fence[2].trim() === "") {
996
+ fenceCharacter = "";
997
+ fenceLength = 0;
998
+ return line;
999
+ }
1000
+ if (fenceCharacter) return line;
1001
+ return line.replace(/<\/?div\b[^>]*>/gi, "").replace(/<img\s+([^>]*?)\/?\s*>/gi, (original, attributes) => {
1002
+ const source = attributes.match(/\bsrc\s*=\s*(['"])(.*?)\1/i)?.[2];
1003
+ const alternative = attributes.match(/\balt\s*=\s*(['"])(.*?)\1/i)?.[2] ?? "";
1004
+ if (!source || /^(?:javascript|data):/i.test(source)) return "";
1005
+ return `![${alternative.replace(/]/g, "\\]")}](${source})`;
1006
+ });
1007
+ }).join("\n");
1008
+ const executable = withoutFencedCode(sanitized).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<(?:https?:\/\/|mailto:)[^>]+>/gi, "").replace(/(`+)[\s\S]*?\1/g, "");
1009
+ if (/^\s*(?:import|export)\s/m.test(executable)) return null;
1010
+ let hasUnsafeExpression = false;
1011
+ const withoutStaticProps = executable.replace(/=\{([^{}\n]*)\}/g, (original, value) => {
1012
+ try {
1013
+ JSON.parse(value);
1014
+ return '=""';
1015
+ } catch {
1016
+ hasUnsafeExpression = true;
1017
+ return original;
1018
+ }
1019
+ }).replace(/\\[{}]/g, "");
1020
+ if (hasUnsafeExpression || /[{}]/.test(withoutStaticProps) || /\bon[A-Z][A-Za-z]*\s*=/g.test(withoutStaticProps) || /\b(?:href|src)\s*=\s*(['"])\s*(?:javascript|data):/gi.test(withoutStaticProps)) return null;
1021
+ for (const match of withoutStaticProps.matchAll(/<\/?([A-Za-z][A-Za-z0-9.]*)(?:\s|>|\/)/g)) {
1022
+ const component = match[1].split(".", 1)[0];
1023
+ if (!PORTABLE_MDX_COMPONENTS.has(component)) return null;
1024
+ }
1025
+ return sanitized;
1026
+ }
1027
+ function migratedHref(href, currentUrl, source, scopePath, importedIds) {
1028
+ if (/^(?:javascript|data|vbscript):/i.test(href)) return "#";
1029
+ if (href.startsWith("#") || /^(?:mailto|tel):/i.test(href)) return href;
1030
+ try {
1031
+ const target = new URL(href, currentUrl);
1032
+ if (target.origin !== source.origin) return href;
1033
+ let path = target.pathname;
1034
+ if (scopePath !== "/" && (path === scopePath || path.startsWith(`${scopePath}/`))) {
1035
+ path = path.slice(scopePath.length);
1036
+ }
1037
+ const id = pageIdFromReference(path.replace(/^\/+/, "") || "introduction");
1038
+ const localizedId = id && LOCALE_CODES.has(id) ? `${id}/introduction` : id;
1039
+ if (localizedId && importedIds.has(localizedId)) {
1040
+ const destination = localizedId === "introduction" ? "/" : `/${localizedId.replace(/\/introduction$/, "")}`;
1041
+ return `${destination}${target.search}${target.hash}`;
1042
+ }
1043
+ return target.toString();
1044
+ } catch {
1045
+ return href;
1046
+ }
1047
+ }
1048
+ function rewriteInternalLinks(body, currentUrl, source, scopePath, importedIds) {
1049
+ const markdown = body.replace(/(?<!!)\[([^\]]+)\]\(([^)\s]+)(\s+['"][^'"]*['"])?\)/g, (original, label, href, title = "") => {
1050
+ const destination = migratedHref(href, currentUrl, source, scopePath, importedIds);
1051
+ return destination === href ? original : `[${label}](${destination}${title})`;
1052
+ });
1053
+ return markdown.replace(/\bhref=(['"])([^'"]+)\1/g, (_original, quote, href) => {
1054
+ const destination = migratedHref(href, currentUrl, source, scopePath, importedIds);
1055
+ return `href=${quote}${destination}${quote}`;
1056
+ });
1057
+ }
1058
+ function remoteMarkdownMetadata(body) {
1059
+ if (!/^---\r?\n/.test(body)) return { content: body };
1060
+ const end = body.slice(4, 65540).search(/\r?\n---(?:\r?\n|$)/);
1061
+ if (end < 0) return { content: body };
1062
+ const frontmatter = body.slice(4, end + 4);
1063
+ const contentStart = end + 4 + body.slice(end + 4).match(/^\r?\n---(?:\r?\n|$)/)[0].length;
1064
+ function scalar(key) {
1065
+ const value = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1].trim();
1066
+ if (!value) return void 0;
1067
+ if (/^['"].*['"]$/.test(value)) {
1068
+ try {
1069
+ const parsed = JSON.parse(value.replace(/^'/, '"').replace(/'$/, '"'));
1070
+ return typeof parsed === "string" ? parsed : void 0;
1071
+ } catch {
1072
+ return value.slice(1, -1);
1073
+ }
1074
+ }
1075
+ return value.slice(0, 500);
1076
+ }
1077
+ return {
1078
+ content: body.slice(contentStart),
1079
+ title: scalar("title"),
1080
+ description: scalar("description")
1081
+ };
1082
+ }
1083
+ function markdownPage(document, id) {
1084
+ const parsed = remoteMarkdownMetadata(document.body);
1085
+ const titleMatch = parsed.content.match(/^#\s+(.+)$/m);
1086
+ const title = parsed.title ?? titleMatch?.[1] ?? id.split("/").at(-1) ?? "Introduction";
1087
+ const raw = [
1088
+ "---",
1089
+ `title: ${JSON.stringify(title)}`,
1090
+ ...parsed.description ? [`description: ${JSON.stringify(parsed.description)}`] : [],
1091
+ "---",
1092
+ "",
1093
+ parsed.content
1094
+ ].join("\n");
1095
+ return parseMarkdownPage({ id, raw, source: document.finalUrl.toString() });
1096
+ }
1097
+ function htmlPage(document, id) {
1098
+ const $ = load(document.body);
1099
+ const links = $('nav a[href], main a[href], article a[href], [role="main"] a[href]').map((_index, element) => $(element).attr("href") ?? "").get().filter(Boolean);
1100
+ const title = $("main h1, article h1, h1").first().text().trim() || $('meta[property="og:title"]').attr("content")?.trim() || $("title").text().split("|")[0].trim();
1101
+ const description = $('meta[name="description"]').attr("content")?.trim() || $('meta[property="og:description"]').attr("content")?.trim();
1102
+ const content = $('main article, article, main, [role="main"]').first();
1103
+ const root = content.length ? content : $("body");
1104
+ root.find("script,style,noscript,nav,header,footer,aside,form,button,svg,iframe,object,embed,link,meta").remove();
1105
+ root.find("a[href]").each((_index, element) => {
1106
+ const href = $(element).attr("href");
1107
+ if (!href) return;
1108
+ try {
1109
+ const resolved = new URL(href, document.finalUrl);
1110
+ if (["http:", "https:", "mailto:", "tel:"].includes(resolved.protocol)) {
1111
+ $(element).attr("href", resolved.toString());
1112
+ } else {
1113
+ $(element).removeAttr("href");
1114
+ }
1115
+ } catch {
1116
+ $(element).removeAttr("href");
1117
+ }
1118
+ });
1119
+ root.find("img[src]").each((_index, element) => {
1120
+ const src = $(element).attr("src");
1121
+ if (!src) return;
1122
+ try {
1123
+ const resolved = new URL(src, document.finalUrl);
1124
+ if (["http:", "https:"].includes(resolved.protocol)) {
1125
+ $(element).attr("src", resolved.toString());
1126
+ } else {
1127
+ $(element).removeAttr("src");
1128
+ }
1129
+ } catch {
1130
+ $(element).removeAttr("src");
1131
+ }
1132
+ });
1133
+ const turndown = new TurndownService({ codeBlockStyle: "fenced", emDelimiter: "_", headingStyle: "atx" });
1134
+ turndown.remove(["script", "style", "noscript"]);
1135
+ turndown.addRule("migration-safe-fenced-code", {
1136
+ filter: (node) => node.nodeName === "PRE",
1137
+ replacement: (_content, node) => {
1138
+ const element = node;
1139
+ const code = element.textContent?.replace(/\n$/, "") ?? "";
1140
+ const language = element.querySelector("code")?.className.match(/(?:language-|lang-)([A-Za-z0-9_+-]+)/)?.[1] ?? "";
1141
+ const longestFence = Math.max(0, ...[...code.matchAll(/`+/g)].map((match) => match[0].length));
1142
+ const fence = "`".repeat(Math.max(3, longestFence + 1));
1143
+ return `
1144
+
1145
+ ${fence}${language}
1146
+ ${code}
1147
+ ${fence}
1148
+
1149
+ `;
1150
+ }
1151
+ });
1152
+ const escape = turndown.escape.bind(turndown);
1153
+ turndown.escape = (text) => escape(text).replace(/[<{]/g, (match) => `\\${match}`);
1154
+ const body = turndown.turndown(root.html() ?? "").trim();
1155
+ if (!title || body.length < 40) return { page: null, links };
1156
+ return {
1157
+ page: {
1158
+ id,
1159
+ navigationId: id,
1160
+ title,
1161
+ description: description || plainDescription(body) || `Documentation imported from ${document.finalUrl.hostname}.`,
1162
+ keywords: [],
1163
+ body,
1164
+ source: document.finalUrl.toString()
1165
+ },
1166
+ links
1167
+ };
1168
+ }
1169
+ function markdownLinks(body) {
1170
+ return [...body.matchAll(/\[[^\]]+\]\(([^)\s]+)(?:\s+['"][^'"]*['"])?\)/g)].map((match) => match[1]);
1171
+ }
1172
+ function sitemapLinks(body) {
1173
+ const $ = load(body, { xmlMode: true });
1174
+ return $("url > loc, sitemap > loc").map((_index, element) => $(element).text().trim()).get().filter(Boolean);
1175
+ }
1176
+ function robotsSitemaps(body) {
1177
+ return body.split("\n").map((line) => line.match(/^\s*Sitemap:\s*(\S+)/i)?.[1]).filter((value) => Boolean(value));
1178
+ }
1179
+ function headerLinks(document) {
1180
+ const links = [];
1181
+ const llms = document.headers?.["x-llms-txt"];
1182
+ if (llms) links.push(llms);
1183
+ const linkHeader = document.headers?.link ?? "";
1184
+ for (const match of linkHeader.matchAll(/<([^>]+)>\s*;[^,]*(?:rel=["']?(?:alternate|llms-txt)["']?|type=["']text\/(?:markdown|plain)["'])/gi)) {
1185
+ links.push(match[1]);
1186
+ }
1187
+ return links;
1188
+ }
1189
+ async function safeFetch(fetcher, url, accept) {
1190
+ try {
1191
+ return await fetcher(url, { accept });
1192
+ } catch {
1193
+ return null;
1194
+ }
1195
+ }
1196
+ async function migrateUrl(options) {
1197
+ const source = validateMigrationUrl(options.sourceUrl);
1198
+ const fetcher = options.fetcher ?? defaultMigrationFetcher;
1199
+ const maxPages = Math.max(1, Math.min(options.maxPages ?? DEFAULT_MAX_PAGES, 1e3));
1200
+ const concurrency = Math.max(1, Math.min(options.concurrency ?? 5, 10));
1201
+ const maxTotalBytes = Math.max(1e6, Math.min(options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES, 5e8));
1202
+ const scopePath = docsScopePath(source);
1203
+ const initial = await fetcher(source, { accept: "text/markdown,text/html,application/xhtml+xml;q=0.9" });
1204
+ if (!isInScope(initial.finalUrl, source, scopePath)) {
1205
+ throw new Error("The documentation URL redirected outside the submitted docs origin or path.");
1206
+ }
1207
+ const platform = detectUrlPlatform(initial);
1208
+ const cache = /* @__PURE__ */ new Map([[source.toString(), initial]]);
1209
+ const queue = [source];
1210
+ const queued = /* @__PURE__ */ new Set([candidateIdentity(source)]);
1211
+ const warnings = [];
1212
+ const fetchedSitemaps = /* @__PURE__ */ new Set();
1213
+ function enqueue(value, base) {
1214
+ if (queued.size >= MAX_DISCOVERED_URLS) return;
1215
+ const url = normalizeCandidate(value, base, source, scopePath);
1216
+ if (!url || queued.has(candidateIdentity(url))) return;
1217
+ queued.add(candidateIdentity(url));
1218
+ queue.push(url);
1219
+ }
1220
+ async function discoverDocumentLinks(document, depth = 0) {
1221
+ const links = sitemapLinks(document.body);
1222
+ for (const link of links) {
1223
+ let url;
1224
+ try {
1225
+ url = new URL(link, document.finalUrl);
1226
+ } catch {
1227
+ continue;
1228
+ }
1229
+ if (depth < 2 && /\.xml(?:$|\?)/i.test(url.pathname)) {
1230
+ if (url.origin !== source.origin || fetchedSitemaps.size >= MAX_SITEMAP_DOCUMENTS || fetchedSitemaps.has(url.toString())) continue;
1231
+ fetchedSitemaps.add(url.toString());
1232
+ const nested = await safeFetch(fetcher, url, "application/xml,text/xml");
1233
+ if (nested && nested.finalUrl.origin === source.origin) await discoverDocumentLinks(nested, depth + 1);
1234
+ } else {
1235
+ enqueue(link, document.finalUrl);
1236
+ }
1237
+ }
1238
+ }
1239
+ for (const link of headerLinks(initial)) {
1240
+ let url;
1241
+ try {
1242
+ url = new URL(link, initial.finalUrl);
1243
+ } catch {
1244
+ continue;
1245
+ }
1246
+ if (url.origin !== source.origin) continue;
1247
+ const document = await safeFetch(fetcher, url, "text/plain,text/markdown");
1248
+ if (!document || document.finalUrl.origin !== source.origin) continue;
1249
+ for (const candidate of markdownLinks(document.body)) enqueue(candidate, document.finalUrl);
1250
+ }
1251
+ const pathRoot = scopePath === "/" ? source.origin : `${source.origin}${scopePath}`;
1252
+ const discoveryUrls = [
1253
+ new URL("/llms.txt", source.origin),
1254
+ new URL(`${pathRoot.replace(source.origin, "")}/llms.txt`, source.origin),
1255
+ new URL("/robots.txt", source.origin),
1256
+ new URL("/sitemap.xml", source.origin),
1257
+ new URL(`${pathRoot.replace(source.origin, "")}/sitemap.xml`, source.origin)
1258
+ ];
1259
+ const uniqueDiscovery = [...new Map(discoveryUrls.map((url) => [url.toString(), url])).values()];
1260
+ for (const url of uniqueDiscovery) {
1261
+ const document = await safeFetch(fetcher, url, "text/plain,application/xml,text/xml;q=0.9");
1262
+ if (!document || document.finalUrl.origin !== source.origin) continue;
1263
+ const isRobots = url.pathname.endsWith("robots.txt");
1264
+ const isSitemap = url.pathname.endsWith("sitemap.xml") || document.contentType.includes("xml");
1265
+ if (isRobots) {
1266
+ for (const link of robotsSitemaps(document.body)) {
1267
+ let sitemapUrl;
1268
+ try {
1269
+ sitemapUrl = new URL(link, document.finalUrl);
1270
+ } catch {
1271
+ continue;
1272
+ }
1273
+ if (sitemapUrl.origin !== source.origin || fetchedSitemaps.size >= MAX_SITEMAP_DOCUMENTS || fetchedSitemaps.has(sitemapUrl.toString())) continue;
1274
+ fetchedSitemaps.add(sitemapUrl.toString());
1275
+ const sitemap = await safeFetch(fetcher, sitemapUrl, "application/xml,text/xml");
1276
+ if (sitemap && sitemap.finalUrl.origin === source.origin) await discoverDocumentLinks(sitemap);
1277
+ }
1278
+ } else if (isSitemap) {
1279
+ await discoverDocumentLinks(document);
1280
+ } else {
1281
+ for (const link of markdownLinks(document.body)) enqueue(link, document.finalUrl);
1282
+ }
1283
+ }
1284
+ const pages = [];
1285
+ const seenIds = /* @__PURE__ */ new Set();
1286
+ const visited = /* @__PURE__ */ new Set();
1287
+ let importedBytes = 0;
1288
+ let isByteBudgetExhausted = false;
1289
+ while (queue.length > 0 && pages.length < maxPages && !isByteBudgetExhausted) {
1290
+ const batch = [];
1291
+ while (queue.length > 0 && batch.length < Math.min(concurrency, maxPages - pages.length)) {
1292
+ const candidate = queue.shift();
1293
+ if (!candidate || visited.has(candidate.toString())) continue;
1294
+ visited.add(candidate.toString());
1295
+ batch.push(candidate);
1296
+ }
1297
+ if (batch.length === 0) continue;
1298
+ const results = await Promise.all(batch.map(async (candidate) => {
1299
+ let document = cache.get(candidate.toString()) ?? await safeFetch(fetcher, candidate, "text/markdown,text/html,application/xhtml+xml;q=0.9");
1300
+ if (!document) {
1301
+ return { candidate, page: null, links: [], failure: true };
1302
+ }
1303
+ if (!isInScope(document.finalUrl, source, scopePath)) {
1304
+ return { candidate, page: null, links: [], failure: false };
1305
+ }
1306
+ const isMarkdown = /(?:markdown|text\/plain)/i.test(document.contentType) || /\.mdx?$/i.test(document.finalUrl.pathname);
1307
+ if (!isMarkdown && !/\.mdx?$/i.test(candidate.pathname)) {
1308
+ const markdownUrl = new URL(candidate);
1309
+ markdownUrl.pathname = `${markdownUrl.pathname.replace(/\/$/, "")}.md`;
1310
+ const markdownDocument = await safeFetch(fetcher, markdownUrl, "text/markdown,text/plain");
1311
+ if (markdownDocument && isInScope(markdownDocument.finalUrl, source, scopePath) && /(?:markdown|text\/plain)/i.test(markdownDocument.contentType)) {
1312
+ document = markdownDocument;
1313
+ }
1314
+ }
1315
+ const isResolvedMarkdown = /(?:markdown|text\/plain)/i.test(document.contentType) || /\.mdx?$/i.test(document.finalUrl.pathname);
1316
+ const sanitizedMarkdown = isResolvedMarkdown ? sanitizeRemoteMarkdown(document.body) : null;
1317
+ if (isResolvedMarkdown && sanitizedMarkdown === null) {
1318
+ const htmlUrl = new URL(candidate);
1319
+ htmlUrl.pathname = htmlUrl.pathname.replace(/\.mdx?$/i, "") || "/";
1320
+ const htmlDocument = await safeFetch(fetcher, htmlUrl, "text/html,application/xhtml+xml");
1321
+ if (htmlDocument && isInScope(htmlDocument.finalUrl, source, scopePath) && /html/i.test(htmlDocument.contentType)) {
1322
+ document = htmlDocument;
1323
+ }
1324
+ } else if (sanitizedMarkdown !== null) {
1325
+ document = { ...document, body: sanitizedMarkdown };
1326
+ }
1327
+ const responseBytes = Buffer.byteLength(document.body, "utf8");
1328
+ if (importedBytes + responseBytes > maxTotalBytes) {
1329
+ isByteBudgetExhausted = true;
1330
+ return { candidate, page: null, links: [], failure: false, budgetExceeded: true };
1331
+ }
1332
+ importedBytes += responseBytes;
1333
+ const id = pageIdForUrl(document.finalUrl, scopePath) ?? pageIdForUrl(candidate, scopePath);
1334
+ if (!id) return { candidate, page: null, links: [], failure: false };
1335
+ if (/(?:markdown|text\/plain)/i.test(document.contentType) || /\.mdx?$/i.test(document.finalUrl.pathname)) {
1336
+ return {
1337
+ candidate,
1338
+ page: markdownPage(document, id),
1339
+ links: markdownLinks(document.body),
1340
+ base: document.finalUrl,
1341
+ failure: false
1342
+ };
1343
+ }
1344
+ const extracted = htmlPage(document, id);
1345
+ return { candidate, page: extracted.page, links: extracted.links, base: document.finalUrl, failure: false };
1346
+ }));
1347
+ for (const result of results) {
1348
+ if (result.failure) {
1349
+ warnings.push({ code: "fetch-failed", message: "A discovered documentation page could not be fetched.", source: result.candidate.toString() });
1350
+ continue;
1351
+ }
1352
+ if (result.budgetExceeded) continue;
1353
+ if (result.page && !seenIds.has(result.page.id) && pages.length < maxPages) {
1354
+ pages.push(result.page);
1355
+ seenIds.add(result.page.id);
1356
+ }
1357
+ for (const link of result.links) enqueue(link, result.base ?? result.candidate);
1358
+ }
1359
+ }
1360
+ if (pages.length === 0) throw new Error("No readable documentation pages were found at that URL.");
1361
+ for (const page of pages) {
1362
+ const [locale, ...navigationSegments] = page.id.split("/");
1363
+ if (LOCALE_CODES.has(locale)) {
1364
+ if (navigationSegments.length === 0) page.id = `${locale}/introduction`;
1365
+ page.locale = locale;
1366
+ page.navigationId = navigationSegments.join("/") || "introduction";
1367
+ }
1368
+ }
1369
+ const importedIds = new Set(pages.map((page) => page.id));
1370
+ for (const page of pages) {
1371
+ page.body = rewriteInternalLinks(page.body, new URL(page.source), source, scopePath, importedIds);
1372
+ }
1373
+ if (isByteBudgetExhausted) {
1374
+ warnings.push({ code: "limit-reached", message: `Import stopped after reaching the ${Math.round(maxTotalBytes / 1e6)} MB content budget.` });
1375
+ } else if (queue.length > 0 || queued.size >= MAX_DISCOVERED_URLS) {
1376
+ warnings.push({ code: "limit-reached", message: `Import stopped after ${pages.length} pages; narrow the URL or raise the caller limit to import more.` });
1377
+ }
1378
+ const docsConfig = buildNavigationFromPages(pages);
1379
+ const locales = [...new Set(pages.map((page) => page.locale).filter((value) => Boolean(value)))];
1380
+ if (locales.length > 0) {
1381
+ docsConfig.i18n = {
1382
+ defaultLocale: "en",
1383
+ locales: [
1384
+ { code: "en", label: "English" },
1385
+ ...locales.filter((locale) => locale !== "en").map((locale) => ({ code: locale, label: locale.toUpperCase() }))
1386
+ ]
1387
+ };
1388
+ }
1389
+ return {
1390
+ sourceUrl: source.toString(),
1391
+ sourceKind: "url",
1392
+ platform,
1393
+ pages,
1394
+ assets: [],
1395
+ docsConfig,
1396
+ warnings,
1397
+ stats: {
1398
+ discovered: visited.size,
1399
+ imported: pages.length,
1400
+ skipped: Math.max(0, visited.size - pages.length)
1401
+ }
1402
+ };
1403
+ }
1404
+ export {
1405
+ buildNavigationFromPages,
1406
+ cloneGitHubRepository,
1407
+ defaultMigrationFetcher,
1408
+ detectRepositoryDocsDir,
1409
+ detectRepositoryPlatform,
1410
+ mergeMigrationConfig,
1411
+ migrateRepository,
1412
+ migrateUrl,
1413
+ normalizeMdx,
1414
+ parseGitHubRepositoryUrl,
1415
+ parseMarkdownPage,
1416
+ projectMintlifyNavigation,
1417
+ readMintlifyConfig,
1418
+ renderMigrationFiles,
1419
+ validateMigrationUrl
1420
+ };
1421
+ //# sourceMappingURL=index.js.map