@thallylabs/migrate 0.2.3 → 0.2.5

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 CHANGED
@@ -1,18 +1,44 @@
1
1
  // src/repository.ts
2
2
  import { spawn } from "child_process";
3
3
  import {
4
- existsSync as existsSync3,
5
- lstatSync as lstatSync3,
6
- readFileSync as readFileSync3,
4
+ existsSync as existsSync4,
5
+ lstatSync as lstatSync4,
6
+ readFileSync as readFileSync4,
7
7
  readdirSync
8
8
  } from "fs";
9
- import { basename, dirname as dirname2, extname as extname3, relative as relative3, resolve as resolvePath } from "path";
9
+ import { basename, dirname as dirname3, extname as extname4, relative as relative4, resolve as resolvePath } from "path";
10
10
 
11
- // src/docusaurus.ts
11
+ // src/components.ts
12
+ import { createHash } from "crypto";
12
13
  import { existsSync, lstatSync, readFileSync } from "fs";
13
- import { extname, posix } from "path";
14
- import JSON5 from "json5";
14
+ import { dirname, extname, relative as relative2, resolve as resolve2 } from "path";
15
+ import remarkMdx from "remark-mdx";
16
+ import remarkParse from "remark-parse";
17
+ import ts from "typescript";
18
+ import { unified } from "unified";
19
+
20
+ // src/frontmatter.ts
15
21
  import { parse as parseYaml } from "yaml";
22
+ function parseFrontmatter(raw) {
23
+ const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
24
+ const opening = /^---([^\r\n]*)\r?\n/.exec(source);
25
+ if (!opening || opening[1].startsWith("-")) return { content: source, data: {} };
26
+ const language = opening[1].trim().toLowerCase();
27
+ const remainder = source.slice(opening[0].length);
28
+ const closing = /^---[ \t]*\r?$/m.exec(remainder);
29
+ const matter = closing ? remainder.slice(0, closing.index) : remainder;
30
+ let content = closing ? remainder.slice(closing.index + closing[0].length) : "";
31
+ if (content.startsWith("\r\n")) content = content.slice(2);
32
+ else if (content.startsWith("\n")) content = content.slice(1);
33
+ if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
34
+ return { content, data: {} };
35
+ }
36
+ const parsed = parseYaml(matter);
37
+ return {
38
+ content,
39
+ data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
40
+ };
41
+ }
16
42
 
17
43
  // src/path.ts
18
44
  import { isAbsolute, relative, resolve, sep } from "path";
@@ -51,6 +77,18 @@ function pageIdFromReference(value, preserveCase = false) {
51
77
  const segments = baseSegments.map((segment) => slugifySegment(segment, preserveCase)).filter(Boolean);
52
78
  return segments.join("/") || "introduction";
53
79
  }
80
+ function mintlifyLocalizedReference(value, locale, defaultPageIds = /* @__PURE__ */ new Set()) {
81
+ const reference = value.replace(/^\/+/, "");
82
+ const localePrefix = `${locale.toLowerCase()}/`;
83
+ if (reference.toLowerCase().startsWith(localePrefix)) return reference.slice(localePrefix.length);
84
+ const sectionPrefix = `${locale.toLowerCase()}-`;
85
+ if (reference.toLowerCase().startsWith(sectionPrefix)) {
86
+ const candidate = reference.slice(sectionPrefix.length);
87
+ const candidateId = pageIdFromReference(candidate, true);
88
+ if (candidateId && defaultPageIds.has(candidateId)) return candidate;
89
+ }
90
+ return value;
91
+ }
54
92
  function resolveWithin(root, candidate) {
55
93
  if (isAbsolute(candidate) || candidate.includes("\0")) {
56
94
  throw new Error(`Unsafe migration path: ${candidate}`);
@@ -70,7 +108,471 @@ function normalizeAssetPath(value) {
70
108
  return segments.join("/");
71
109
  }
72
110
 
111
+ // src/components.ts
112
+ var parser = unified().use(remarkParse).use(remarkMdx);
113
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs"]);
114
+ var DATA_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".css", ".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif", ".woff", ".woff2"]);
115
+ var SHARED_IMPORTS = /* @__PURE__ */ new Set(["react", "react/jsx-runtime", "react/jsx-dev-runtime"]);
116
+ var REACT_GLOBALS = /* @__PURE__ */ new Set([
117
+ "useState",
118
+ "useEffect",
119
+ "useLayoutEffect",
120
+ "useMemo",
121
+ "useCallback",
122
+ "useRef",
123
+ "useReducer",
124
+ "useContext",
125
+ "useId",
126
+ "useTransition",
127
+ "useDeferredValue",
128
+ "useImperativeHandle",
129
+ "useSyncExternalStore",
130
+ "useInsertionEffect",
131
+ "useOptimistic",
132
+ "useActionState",
133
+ "use",
134
+ "createContext",
135
+ "forwardRef",
136
+ "memo"
137
+ ]);
138
+ var MAX_COMPONENT_FILES = 300;
139
+ var MAX_COMPONENT_BYTES = 2e7;
140
+ var MAX_FILE_BYTES = 2e6;
141
+ function applyReplacements(source, replacements) {
142
+ return replacements.sort((a, b) => b.start - a.start).reduce(
143
+ (text, edit) => text.slice(0, edit.start) + edit.value + text.slice(edit.end),
144
+ source
145
+ );
146
+ }
147
+ function hash(value) {
148
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
149
+ }
150
+ function portableSpecifier(path) {
151
+ return path.replace(/\.tsx?$/, "");
152
+ }
153
+ function sourceFile(source, filename) {
154
+ return ts.createSourceFile(
155
+ filename,
156
+ source,
157
+ ts.ScriptTarget.Latest,
158
+ true,
159
+ /\.[cm]?ts$/.test(filename) ? ts.ScriptKind.TS : ts.ScriptKind.TSX
160
+ );
161
+ }
162
+ function walk(node, visitor) {
163
+ visitor(node);
164
+ for (const child of node.children ?? []) walk(child, visitor);
165
+ }
166
+ function implicitReactImports(source) {
167
+ const bindings = /* @__PURE__ */ new Set();
168
+ const references = /* @__PURE__ */ new Set();
169
+ function bind(name) {
170
+ if (ts.isIdentifier(name)) bindings.add(name.text);
171
+ else for (const element of name.elements) if (ts.isBindingElement(element)) bind(element.name);
172
+ }
173
+ for (const statement of source.statements) {
174
+ if (ts.isImportDeclaration(statement) && statement.importClause) {
175
+ const clause = statement.importClause;
176
+ if (clause.name) bindings.add(clause.name.text);
177
+ if (clause.namedBindings) {
178
+ if (ts.isNamespaceImport(clause.namedBindings)) bindings.add(clause.namedBindings.name.text);
179
+ else for (const element of clause.namedBindings.elements) bindings.add(element.name.text);
180
+ }
181
+ }
182
+ if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) bind(declaration.name);
183
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) bindings.add(statement.name.text);
184
+ }
185
+ function visit(node) {
186
+ if (ts.isIdentifier(node) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) && !(ts.isPropertyAssignment(node.parent) && node.parent.name === node)) references.add(node.text);
187
+ ts.forEachChild(node, visit);
188
+ }
189
+ visit(source);
190
+ const hooks = [...REACT_GLOBALS].filter((name) => references.has(name) && !bindings.has(name)).sort();
191
+ return [
192
+ references.has("React") && !bindings.has("React") ? "import * as React from 'react';" : "",
193
+ hooks.length ? `import { ${hooks.join(", ")} } from 'react';` : ""
194
+ ].filter(Boolean).join("\n");
195
+ }
196
+ function imports(statement) {
197
+ if (!ts.isStringLiteral(statement.moduleSpecifier) || !statement.importClause) return [];
198
+ const clause = statement.importClause;
199
+ if (clause.isTypeOnly) return [];
200
+ const source = statement.moduleSpecifier.text;
201
+ const bindings = clause.name ? [{ local: clause.name.text, imported: "default", source }] : [];
202
+ if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
203
+ for (const element of clause.namedBindings.elements) {
204
+ if (!element.isTypeOnly) bindings.push({ local: element.name.text, imported: element.propertyName?.text ?? element.name.text, source });
205
+ }
206
+ }
207
+ return bindings;
208
+ }
209
+ function createComponentMigrator(siteRoot, warnings, sourceIdentity) {
210
+ const root = resolve2(siteRoot);
211
+ const destinationRoot = `src/mdx/migrated/${hash(sourceIdentity)}`;
212
+ const copied = /* @__PURE__ */ new Map();
213
+ const registrations = /* @__PURE__ */ new Map();
214
+ let copiedBytes = 0;
215
+ function warn(message, source) {
216
+ warnings.push({ code: "unsupported-config", message, source: relative2(root, source).replace(/\\/g, "/") });
217
+ }
218
+ function checkedFile(path) {
219
+ const local = relative2(root, path);
220
+ resolveWithin(root, local);
221
+ let current = root;
222
+ if (lstatSync(current).isSymbolicLink()) throw new Error("symbolic links are not imported");
223
+ for (const segment of local.split(/[\\/]/)) {
224
+ current = resolveWithin(current, segment);
225
+ if (lstatSync(current).isSymbolicLink()) throw new Error("symbolic links are not imported");
226
+ }
227
+ if (!lstatSync(path).isFile()) throw new Error("dependency is not a regular file");
228
+ return path;
229
+ }
230
+ function resolveDependency(specifier, importer) {
231
+ if (specifier.includes("\\") || specifier.includes("\0") || /[?#]/.test(specifier)) throw new Error("unsupported component dependency path");
232
+ const candidate = specifier.startsWith("/") ? resolveWithin(root, specifier.slice(1)) : resolveWithin(root, relative2(root, resolve2(dirname(importer), specifier)));
233
+ const candidates = [candidate];
234
+ if (!extname(candidate)) {
235
+ candidates.push(...[".tsx", ".jsx", ".ts", ".js", ".mjs", ".json"].map((extension) => candidate + extension));
236
+ candidates.push(...["index.tsx", "index.jsx", "index.ts", "index.js"].map((name) => resolve2(candidate, name)));
237
+ } else if (extname(candidate) === ".js") {
238
+ candidates.push(candidate.slice(0, -3) + ".ts", candidate.slice(0, -3) + ".tsx");
239
+ }
240
+ for (const path of candidates) {
241
+ if (existsSync(path) && lstatSync(path).isFile()) return checkedFile(path);
242
+ }
243
+ throw new Error(`component dependency ${specifier} was not found`);
244
+ }
245
+ function outputPath(path) {
246
+ return `${destinationRoot}/source/${relative2(root, path).replace(/\\/g, "/")}`;
247
+ }
248
+ function copyGraph(entry) {
249
+ const staged = /* @__PURE__ */ new Map();
250
+ let stagedBytes = 0;
251
+ function visit(path) {
252
+ const destination = outputPath(path);
253
+ if (copied.has(destination) || staged.has(destination)) return;
254
+ checkedFile(path);
255
+ const size = lstatSync(path).size;
256
+ if (size > MAX_FILE_BYTES || copiedBytes + stagedBytes + size > MAX_COMPONENT_BYTES || copied.size + staged.size >= MAX_COMPONENT_FILES) throw new Error("component migration budget exceeded");
257
+ const extension = extname(path).toLowerCase();
258
+ if (!CODE_EXTENSIONS.has(extension) && !DATA_EXTENSIONS.has(extension)) throw new Error(`unsupported dependency file type ${extension}`);
259
+ stagedBytes += size;
260
+ const content = readFileSync(path);
261
+ staged.set(destination, { path: destination, content });
262
+ if (!CODE_EXTENSIONS.has(extension)) {
263
+ if (extension === ".css" && /@import\b|url\s*\(/i.test(content.toString("utf8"))) throw new Error("CSS resource dependencies require manual migration");
264
+ return;
265
+ }
266
+ const text = content.toString("utf8");
267
+ const syntax = ts.transpileModule(text, {
268
+ fileName: path,
269
+ reportDiagnostics: true,
270
+ compilerOptions: { jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext }
271
+ });
272
+ if (syntax.diagnostics?.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) throw new Error("component source contains unsupported JavaScript or TypeScript syntax");
273
+ const ast = sourceFile(text, path);
274
+ if (ast.statements.some((statement) => ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression) && statement.expression.text === "use server")) throw new Error("server-only component modules require manual migration");
275
+ const edits = [];
276
+ function dependency(literal) {
277
+ const specifier = literal.text;
278
+ if (SHARED_IMPORTS.has(specifier)) return;
279
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
280
+ throw new Error(`external package ${specifier} requires manual installation and review`);
281
+ }
282
+ const target = resolveDependency(specifier, path);
283
+ visit(target);
284
+ const nextPath = relative2(dirname(destination), outputPath(target)).replace(/\\/g, "/");
285
+ edits.push({ start: literal.getStart(ast), end: literal.end, value: JSON.stringify(portableSpecifier(nextPath.startsWith(".") ? nextPath : `./${nextPath}`)) });
286
+ }
287
+ function inspect(node) {
288
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) dependency(node.moduleSpecifier);
289
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require")) {
290
+ const argument = node.arguments[0];
291
+ if (node.arguments.length !== 1 || !argument || !ts.isStringLiteralLike(argument)) throw new Error("computed component imports require manual migration");
292
+ dependency(argument);
293
+ }
294
+ ts.forEachChild(node, inspect);
295
+ }
296
+ inspect(ast);
297
+ staged.set(destination, { path: destination, content: `'use client';
298
+
299
+ ${implicitReactImports(ast)}
300
+ ${applyReplacements(text, edits)}` });
301
+ }
302
+ visit(entry);
303
+ for (const [path, file] of staged) copied.set(path, file);
304
+ copiedBytes += stagedBytes;
305
+ return outputPath(entry);
306
+ }
307
+ function register(path, imported) {
308
+ const name = `Migrated${hash(`${path}:${imported}`)}`;
309
+ registrations.set(name, { path, imported });
310
+ return name;
311
+ }
312
+ function transform(raw, currentFile) {
313
+ const content = parseFrontmatter(raw).content;
314
+ const frontmatter = raw.slice(0, raw.length - content.length);
315
+ let tree;
316
+ try {
317
+ tree = parser.parse(content);
318
+ } catch {
319
+ warn("Custom component analysis could not parse this MDX; its source was preserved for manual migration.", currentFile);
320
+ return raw;
321
+ }
322
+ const aliases = /* @__PURE__ */ new Map();
323
+ const edits = [];
324
+ const declarations = [];
325
+ const moduleImports = [];
326
+ const sharedImportEdits = [];
327
+ let hasUnsupportedImports = false;
328
+ function hasExpressionReference(names, options = {}) {
329
+ let found = false;
330
+ function inspect2(node) {
331
+ if (ts.isIdentifier(node) && names.has(node.text)) found = true;
332
+ ts.forEachChild(node, inspect2);
333
+ }
334
+ walk(tree, (node) => {
335
+ if (options.excludedNodes?.some((excluded) => node.position?.start.offset !== void 0 && node.position.end.offset !== void 0 && node.position.start.offset >= excluded.position.start.offset && node.position.end.offset <= excluded.position.end.offset)) return;
336
+ if (node.name && [...names].some((name) => node.name.startsWith(`${name}.`) || options.includeDirectTags && node.name === name)) found = true;
337
+ if (options.includeDeclarations !== false && node.type === "mdxjsEsm" && node.value) {
338
+ for (const statement of sourceFile(node.value, "expression.tsx").statements) {
339
+ if (!ts.isImportDeclaration(statement)) inspect2(statement);
340
+ }
341
+ }
342
+ const expressions = [
343
+ ...["mdxFlowExpression", "mdxTextExpression"].includes(node.type) ? [node.value ?? ""] : [],
344
+ ...(node.attributes ?? []).flatMap((attribute) => {
345
+ if (attribute.type === "mdxJsxExpressionAttribute" && typeof attribute.value === "string") return [attribute.value];
346
+ return attribute.value && typeof attribute.value === "object" ? [attribute.value.value ?? ""] : [];
347
+ })
348
+ ];
349
+ for (const expression of expressions) inspect2(sourceFile(expression, "expression.tsx"));
350
+ });
351
+ return found;
352
+ }
353
+ for (const node of tree.children ?? []) {
354
+ if (node.type !== "mdxjsEsm" || node.value === void 0 || node.position?.start.offset === void 0) continue;
355
+ const ast = sourceFile(node.value, "inline.tsx");
356
+ for (const statement of ast.statements) {
357
+ if (!ts.isImportDeclaration(statement)) {
358
+ let inspect2 = function(node2) {
359
+ if (ts.isCallExpression(node2) && (node2.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node2.expression) && node2.expression.text === "require")) unsupportedDependency = true;
360
+ ts.forEachChild(node2, inspect2);
361
+ };
362
+ var inspect = inspect2;
363
+ let unsupportedDependency = ts.isExportDeclaration(statement) && !!statement.moduleSpecifier;
364
+ inspect2(statement);
365
+ if (unsupportedDependency) {
366
+ hasUnsupportedImports = true;
367
+ warn("Inline MDX module dependencies require manual extraction; source was preserved.", currentFile);
368
+ }
369
+ declarations.push({
370
+ start: node.position.start.offset + statement.getStart(ast),
371
+ end: node.position.start.offset + statement.end,
372
+ source: statement.getText(ast)
373
+ });
374
+ continue;
375
+ }
376
+ const bindings = imports(statement);
377
+ const specifier = ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
378
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
379
+ if (SHARED_IMPORTS.has(specifier)) {
380
+ moduleImports.push(statement.getText(ast));
381
+ sharedImportEdits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
382
+ } else {
383
+ hasUnsupportedImports = true;
384
+ warn(`MDX import ${specifier} requires manual package installation or registration; the import was preserved.`, currentFile);
385
+ }
386
+ continue;
387
+ }
388
+ if (/\.mdx?$/.test(specifier)) continue;
389
+ if (!bindings.length || statement.importClause?.namedBindings && ts.isNamespaceImport(statement.importClause.namedBindings)) {
390
+ warn("Namespace or side-effect MDX imports require manual registration; the import was preserved.", currentFile);
391
+ hasUnsupportedImports = true;
392
+ continue;
393
+ }
394
+ if (bindings.some((binding) => !/^[A-Z]/.test(binding.local))) {
395
+ warn("MDX imports containing non-component values require manual migration; the import was preserved.", currentFile);
396
+ hasUnsupportedImports = true;
397
+ continue;
398
+ }
399
+ if (hasExpressionReference(new Set(bindings.map((binding) => binding.local)))) {
400
+ warn("Imported components used in MDX expressions, component props, or member tags require manual migration; the import was preserved.", currentFile);
401
+ hasUnsupportedImports = true;
402
+ continue;
403
+ }
404
+ try {
405
+ const path = copyGraph(resolveDependency(specifier, currentFile));
406
+ for (const binding of bindings) {
407
+ const name = register(path, binding.imported);
408
+ aliases.set(binding.local, name);
409
+ moduleImports.push(`import { ${binding.imported} as ${binding.local} } from ${JSON.stringify(portableSpecifier(`./${relative2(destinationRoot, path).replace(/\\/g, "/")}`))};`);
410
+ }
411
+ edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
412
+ } catch (error) {
413
+ hasUnsupportedImports = true;
414
+ warn(`Custom component was preserved for manual migration: ${error instanceof Error ? error.message : "unsupported dependency"}.`, currentFile);
415
+ }
416
+ }
417
+ }
418
+ const extracted = [];
419
+ for (const candidate of tree.children ?? []) {
420
+ const node = candidate.type === "paragraph" && candidate.children?.length === 1 ? candidate.children[0] : candidate;
421
+ if (!["mdxJsxFlowElement", "mdxJsxTextElement"].includes(node.type) || node.position?.start.offset === void 0 || node.position.end.offset === void 0) continue;
422
+ let interactive = false;
423
+ let supported = true;
424
+ walk(node, (child) => {
425
+ if (child.attributes?.some((attribute) => /^on[A-Z]/.test(attribute.name ?? ""))) interactive = true;
426
+ if (child.name && /^[A-Z]/.test(child.name) && !aliases.has(child.name)) supported = false;
427
+ if (!["mdxJsxFlowElement", "mdxJsxTextElement", "mdxFlowExpression", "mdxTextExpression", "text", "paragraph"].includes(child.type)) supported = false;
428
+ });
429
+ if (!interactive) continue;
430
+ if (!supported || hasUnsupportedImports) {
431
+ warn("Interactive MDX containing Markdown or unregistered components requires manual extraction; source was preserved.", currentFile);
432
+ continue;
433
+ }
434
+ const jsx = content.slice(node.position.start.offset, node.position.end.offset);
435
+ const result = ts.transpileModule(`export default function Inline() { return (${jsx}); }`, {
436
+ fileName: "inline.jsx",
437
+ compilerOptions: { jsx: ts.JsxEmit.Preserve },
438
+ reportDiagnostics: true
439
+ });
440
+ if (result.diagnostics?.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
441
+ warn("Interactive MDX could not be safely extracted as JSX; source was preserved.", currentFile);
442
+ continue;
443
+ }
444
+ extracted.push({ node, name: `Inline${extracted.length}`, jsx });
445
+ }
446
+ if (extracted.length && declarations.length) {
447
+ let bind2 = function(name) {
448
+ if (ts.isIdentifier(name)) declared.add(name.text);
449
+ else for (const element of name.elements) if (ts.isBindingElement(element)) bind2(element.name);
450
+ };
451
+ var bind = bind2;
452
+ const declared = /* @__PURE__ */ new Set();
453
+ for (const declaration of declarations) {
454
+ for (const statement of sourceFile(declaration.source, "declaration.tsx").statements) {
455
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) declared.add(statement.name.text);
456
+ if (ts.isVariableStatement(statement)) for (const variable of statement.declarationList.declarations) bind2(variable.name);
457
+ }
458
+ }
459
+ const hasSharedDeclarations = hasExpressionReference(declared, {
460
+ excludedNodes: extracted.map((entry) => entry.node),
461
+ includeDeclarations: false,
462
+ includeDirectTags: true
463
+ });
464
+ if (hasSharedDeclarations) {
465
+ warn("Interactive MDX shares declarations with remaining page content; source was preserved for manual extraction.", currentFile);
466
+ extracted.length = 0;
467
+ }
468
+ }
469
+ if (extracted.length && (copied.size >= MAX_COMPONENT_FILES || Buffer.byteLength(content) > MAX_FILE_BYTES || copiedBytes + Buffer.byteLength(content) > MAX_COMPONENT_BYTES)) {
470
+ warn("Interactive MDX exceeded the component migration budget; source was preserved.", currentFile);
471
+ extracted.length = 0;
472
+ }
473
+ if (extracted.length) {
474
+ const path = `${destinationRoot}/inline-${hash(relative2(root, currentFile))}.jsx`;
475
+ let declarationSource = [...new Set(declarations.map((entry) => entry.source))].join("\n\n");
476
+ declarationSource = declarationSource.replace(
477
+ /document\.getElementById\(\s*(['"])search-bar-entry\1\s*\)\.click\(\s*\)/g,
478
+ `document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true }))`
479
+ );
480
+ const inlineSource = [
481
+ "'use client';",
482
+ "",
483
+ ...moduleImports,
484
+ "",
485
+ declarationSource,
486
+ "",
487
+ ...extracted.map(({ name, jsx }) => `export function ${name}() {
488
+ return (${jsx});
489
+ }`),
490
+ ""
491
+ ].join("\n");
492
+ copied.set(path, { path, content: `'use client';
493
+ ${implicitReactImports(sourceFile(inlineSource, "inline.jsx"))}
494
+ ${inlineSource}` });
495
+ copiedBytes += Buffer.byteLength(inlineSource);
496
+ for (const { node, name } of extracted) {
497
+ edits.push({ start: node.position.start.offset, end: node.position.end.offset, value: `<${register(path, name)} />` });
498
+ }
499
+ edits.push(...declarations.map(({ start, end }) => ({ start, end, value: "" })), ...sharedImportEdits);
500
+ }
501
+ walk(tree, (node) => {
502
+ const replacement = node.name ? aliases.get(node.name) : void 0;
503
+ if (!replacement || node.position?.start.offset === void 0 || node.position.end.offset === void 0) return;
504
+ const start = node.position.start.offset;
505
+ const end = node.position.end.offset;
506
+ if (extracted.some((item) => start >= item.node.position.start.offset && end <= item.node.position.end.offset)) return;
507
+ const text = content.slice(start, end);
508
+ const opening = text.indexOf(`<${node.name}`);
509
+ if (opening >= 0) edits.push({ start: start + opening + 1, end: start + opening + 1 + node.name.length, value: replacement });
510
+ const closing = text.lastIndexOf(`</${node.name}`);
511
+ if (closing >= 0) edits.push({ start: start + closing + 2, end: start + closing + 2 + node.name.length, value: replacement });
512
+ });
513
+ return frontmatter + applyReplacements(content, edits);
514
+ }
515
+ function files() {
516
+ if (!registrations.size) return [];
517
+ const entries = [...registrations.entries()].sort(([a], [b]) => a.localeCompare(b));
518
+ return [...copied.values(), {
519
+ path: "src/mdx/custom-components.tsx",
520
+ content: [
521
+ "/** Repository components preserved by migration; this registry is customer-owned. */",
522
+ "import type { MDXComponents } from 'mdx/types'",
523
+ ...entries.map(([name, binding]) => {
524
+ const path = portableSpecifier(`./${relative2("src/mdx", binding.path).replace(/\\/g, "/")}`);
525
+ return `import { ${binding.imported} as ${name} } from ${JSON.stringify(path)}`;
526
+ }),
527
+ "",
528
+ "export const customComponents: MDXComponents = {",
529
+ ...entries.map(([name]) => ` ${name},`),
530
+ "}",
531
+ ""
532
+ ].join("\n")
533
+ }];
534
+ }
535
+ return { transform, files };
536
+ }
537
+ function mergeComponentRegistry(existing, incoming) {
538
+ const fingerprint = hash(incoming);
539
+ const name = `MigratedRegistry${fingerprint}`;
540
+ const registryPath = `./migrated-components-${fingerprint}`;
541
+ const source = sourceFile(existing, "custom-components.tsx");
542
+ const alreadyImported = source.statements.some((statement) => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === registryPath);
543
+ let initializer;
544
+ for (const statement of source.statements) {
545
+ if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
546
+ for (const declaration of statement.declarationList.declarations) {
547
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === "customComponents") initializer = declaration.initializer;
548
+ }
549
+ }
550
+ while (initializer && (ts.isAsExpression(initializer) || ts.isSatisfiesExpression(initializer) || ts.isParenthesizedExpression(initializer))) initializer = initializer.expression;
551
+ if (!initializer || !ts.isObjectLiteralExpression(initializer)) {
552
+ throw new Error("Cannot safely merge src/mdx/custom-components.tsx: export customComponents as an object literal before importing components. The existing registry was preserved.");
553
+ }
554
+ const content = alreadyImported ? existing : applyReplacements(existing, [
555
+ {
556
+ start: source.statements.filter((statement) => ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)).at(-1)?.end ?? 0,
557
+ end: source.statements.filter((statement) => ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)).at(-1)?.end ?? 0,
558
+ value: `
559
+ import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
560
+ `
561
+ },
562
+ { start: initializer.getStart(source) + 1, end: initializer.getStart(source) + 1, value: `
563
+ ...${name},` }
564
+ ]);
565
+ return [
566
+ { path: `src/mdx/migrated-components-${fingerprint}.tsx`, content: incoming },
567
+ { path: "src/mdx/custom-components.tsx", content }
568
+ ];
569
+ }
570
+
73
571
  // src/docusaurus.ts
572
+ import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
573
+ import { extname as extname2, posix } from "path";
574
+ import JSON5 from "json5";
575
+ import { parse as parseYaml2 } from "yaml";
74
576
  var MAX_CONFIG_BYTES = 1e6;
75
577
  var CATEGORY_FILENAMES = ["_category_.json", "_category_.yml", "_category_.yaml"];
76
578
  var SIDEBAR_FILENAMES = [
@@ -184,10 +686,10 @@ function rewriteDocusaurusLinks(body, current, descriptors) {
184
686
  }).join("\n");
185
687
  }
186
688
  function readBoundedText(path) {
187
- if (lstatSync(path).size > MAX_CONFIG_BYTES) {
689
+ if (lstatSync2(path).size > MAX_CONFIG_BYTES) {
188
690
  throw new Error("Docusaurus sidebar config exceeded the 1 MB static-parser limit.");
189
691
  }
190
- return readFileSync(path, "utf8");
692
+ return readFileSync2(path, "utf8");
191
693
  }
192
694
  function matchingObjectLiteral(source, start) {
193
695
  let depth = 0;
@@ -342,33 +844,33 @@ function matchingArrayLiteral(source, start) {
342
844
  function configuredSidebarPath(repositoryRoot) {
343
845
  for (const filename of ["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"]) {
344
846
  const path = resolveWithin(repositoryRoot, filename);
345
- if (!existsSync(path)) continue;
847
+ if (!existsSync2(path)) continue;
346
848
  const source = readBoundedText(path);
347
849
  const match = source.match(/\bsidebarPath\s*:\s*(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/);
348
850
  if (!match) continue;
349
851
  const candidate = match[2].replace(/^\.\//, "");
350
852
  const resolved = resolveWithin(repositoryRoot, candidate);
351
- if (existsSync(resolved) && lstatSync(resolved).isFile()) return candidate;
853
+ if (existsSync2(resolved) && lstatSync2(resolved).isFile()) return candidate;
352
854
  }
353
855
  return null;
354
856
  }
355
857
  function readDocusaurusSidebars(repositoryRoot) {
356
858
  const configured = configuredSidebarPath(repositoryRoot);
357
- const sourcePath = [configured, ...SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => existsSync(resolveWithin(repositoryRoot, candidate)));
859
+ const sourcePath = [configured, ...SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => existsSync2(resolveWithin(repositoryRoot, candidate)));
358
860
  if (!sourcePath) return null;
359
861
  const absolutePath = resolveWithin(repositoryRoot, sourcePath);
360
862
  const source = readBoundedText(absolutePath);
361
- const parsed = extname(sourcePath).toLowerCase() === ".json" ? objectValue(JSON5.parse(source)) : parseStaticSidebarModule(source);
863
+ const parsed = extname2(sourcePath).toLowerCase() === ".json" ? objectValue(JSON5.parse(source)) : parseStaticSidebarModule(source);
362
864
  if (!parsed) throw new Error("Docusaurus sidebar config must export an object.");
363
865
  return { config: parsed, sourcePath };
364
866
  }
365
867
  function readCategoryMetadata(contentRoot, directory) {
366
868
  for (const filename of CATEGORY_FILENAMES) {
367
869
  const path = resolveWithin(contentRoot, posix.join(directory, filename));
368
- if (!existsSync(path) || !lstatSync(path).isFile()) continue;
870
+ if (!existsSync2(path) || !lstatSync2(path).isFile()) continue;
369
871
  try {
370
872
  const raw = readBoundedText(path);
371
- const parsed = filename.endsWith(".json") ? JSON5.parse(raw) : parseYaml(raw);
873
+ const parsed = filename.endsWith(".json") ? JSON5.parse(raw) : parseYaml2(raw);
372
874
  const object = objectValue(parsed);
373
875
  if (!object) return {};
374
876
  return {
@@ -533,31 +1035,39 @@ function projectDocusaurusNavigation(input) {
533
1035
  };
534
1036
  }
535
1037
 
536
- // src/frontmatter.ts
537
- import matter from "gray-matter";
538
- var FRONTMATTER_OPTIONS = {
539
- engines: {
540
- javascript: () => ({}),
541
- js: () => ({})
542
- }
543
- };
544
- function parseFrontmatter(raw) {
545
- return matter(raw, FRONTMATTER_OPTIONS);
546
- }
547
-
548
1038
  // src/mdx.ts
1039
+ import remarkMdx2 from "remark-mdx";
1040
+ import remarkParse2 from "remark-parse";
1041
+ import { unified as unified2 } from "unified";
549
1042
  function titleFromId(id) {
550
1043
  const value = id.split("/").at(-1) ?? id;
551
1044
  if (value === "introduction") return "Introduction";
552
1045
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
553
1046
  }
1047
+ var descriptionParser = unified2().use(remarkParse2).use(remarkMdx2);
554
1048
  function firstParagraph(content) {
555
- for (const block of content.split(/\n\s*\n/)) {
556
- const value = block.replace(/\s+/g, " ").trim();
557
- if (!value || /^(?:#|```|:::|<|import\s|export\s)/.test(value)) continue;
558
- return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").slice(0, 240);
1049
+ function plainText(node) {
1050
+ if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
1051
+ if (node.type === "image") return node.alt ?? "";
1052
+ if (node.type === "break") return " ";
1053
+ return (node.children ?? []).map(plainText).join("");
1054
+ }
1055
+ function findParagraph(node) {
1056
+ if (node.type === "paragraph") {
1057
+ const value = plainText(node).replace(/\s+/g, " ").trim();
1058
+ if (value) return value.slice(0, 240);
1059
+ }
1060
+ for (const child of node.children ?? []) {
1061
+ const value = findParagraph(child);
1062
+ if (value) return value;
1063
+ }
1064
+ return "";
1065
+ }
1066
+ try {
1067
+ return findParagraph(descriptionParser.parse(content));
1068
+ } catch {
1069
+ return "";
559
1070
  }
560
- return "";
561
1071
  }
562
1072
  function docusaurusAdmonitionTag(kind) {
563
1073
  if (kind === "danger") return "Error";
@@ -635,24 +1145,94 @@ function parseMarkdownPage(input) {
635
1145
  }
636
1146
 
637
1147
  // src/navigation.ts
638
- import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "fs";
639
- import { dirname, extname as extname2, relative as relative2 } from "path";
1148
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, realpathSync } from "fs";
1149
+ import { dirname as dirname2, extname as extname3, relative as relative3 } from "path";
640
1150
  var MAX_MINTLIFY_CONFIG_BYTES = 2e6;
641
1151
  function addMintlifyDirectoryRedirects(config, pages) {
642
- const pageIds = new Set(pages.filter((page) => !page.locale).map((page) => page.navigationId));
1152
+ const pageIds = new Set(pages.map((page) => page.id));
643
1153
  const redirects = [...config.redirects ?? []];
644
1154
  const redirectSources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
1155
+ function hasAuthoredRedirect(source) {
1156
+ if (redirectSources.has(source)) return true;
1157
+ return (config.redirects ?? []).some((redirect) => {
1158
+ const match = redirect.source.match(/^(.*)\/:[A-Za-z_][A-Za-z0-9_]*\*\/?$/);
1159
+ return match ? source === match[1] || source.startsWith(`${match[1]}/`) : false;
1160
+ });
1161
+ }
645
1162
  for (const page of pages) {
646
- if (page.locale || !/(?:^|\/)(?:overview|introduction)$/.test(page.navigationId)) continue;
647
- const parent = page.navigationId.replace(/\/(?:overview|introduction)$/, "");
1163
+ if (!/\/(?:overview|introduction)$/.test(page.navigationId)) continue;
1164
+ const parent = page.id.replace(/\/(?:overview|introduction)$/, "");
648
1165
  if (!parent || pageIds.has(parent)) continue;
649
1166
  const source = `/${parent}`;
650
- if (redirectSources.has(source)) continue;
651
- redirects.push({ source, destination: `/${page.navigationId}`, permanent: false });
1167
+ if (hasAuthoredRedirect(source)) continue;
1168
+ redirects.push({ source, destination: `/${page.id}`, permanent: false });
1169
+ redirectSources.add(source);
1170
+ }
1171
+ const navigationOrder = [];
1172
+ function collectPages(nodes) {
1173
+ for (const node of nodes) {
1174
+ if (typeof node === "string") navigationOrder.push(node);
1175
+ else if (!node.hidden) collectPages(node.pages);
1176
+ }
1177
+ }
1178
+ for (const tab of config.tabs) {
1179
+ if (!tab.hidden) collectPages([...tab.pages ?? [], ...tab.groups ?? []]);
1180
+ }
1181
+ const pagesByNavigationId = /* @__PURE__ */ new Map();
1182
+ for (const page of pages) {
1183
+ if (page.hidden) continue;
1184
+ const variants = pagesByNavigationId.get(page.navigationId) ?? [];
1185
+ variants.push(page);
1186
+ pagesByNavigationId.set(page.navigationId, variants);
1187
+ }
1188
+ const firstDescendantByDirectory = /* @__PURE__ */ new Map();
1189
+ for (const navigationId of navigationOrder) {
1190
+ for (const page of pagesByNavigationId.get(navigationId) ?? []) {
1191
+ const segments = page.id.split("/");
1192
+ for (let length = 1; length < segments.length; length++) {
1193
+ const directory = segments.slice(0, length).join("/");
1194
+ if (!firstDescendantByDirectory.has(directory)) firstDescendantByDirectory.set(directory, page);
1195
+ }
1196
+ }
1197
+ }
1198
+ for (const redirect of config.redirects ?? []) {
1199
+ const destination = redirect.destination.split(/[?#]/, 1)[0].replace(/\/:[A-Za-z_][A-Za-z0-9_]*\*\/?$/, "");
1200
+ if (!destination.startsWith("/") || destination.startsWith("//") || /[:*()[\]{}]/.test(destination)) continue;
1201
+ const directory = trimEdgeSlashes(destination);
1202
+ if (!directory || pageIds.has(directory)) continue;
1203
+ const source = `/${directory}`;
1204
+ if (hasAuthoredRedirect(source)) continue;
1205
+ const landing = firstDescendantByDirectory.get(directory);
1206
+ if (!landing) continue;
1207
+ redirects.push({ source, destination: `/${landing.id}`, permanent: false });
652
1208
  redirectSources.add(source);
653
1209
  }
654
1210
  return redirects.length > 0 ? { ...config, redirects } : config;
655
1211
  }
1212
+ function addMintlifyHomepageRedirects(config, pages) {
1213
+ const defaultPages = new Set(pages.filter((page) => !page.locale || page.locale === config.i18n?.defaultLocale).map((page) => page.navigationId));
1214
+ function firstPage(nodes) {
1215
+ for (const node of nodes) {
1216
+ if (typeof node === "string") {
1217
+ if (defaultPages.has(node)) return node;
1218
+ } else if (!node.hidden) {
1219
+ const page = firstPage(node.pages);
1220
+ if (page) return page;
1221
+ }
1222
+ }
1223
+ }
1224
+ const homepage = config.tabs.filter((tab) => !tab.hidden).map((tab) => firstPage([...tab.pages ?? [], ...tab.groups ?? []])).find(Boolean);
1225
+ if (!homepage || homepage === "introduction") return config;
1226
+ const redirects = [...config.redirects ?? []];
1227
+ const sources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
1228
+ const roots = ["", ...(config.i18n?.locales ?? []).filter((locale) => locale.code !== config.i18n?.defaultLocale).map((locale) => locale.code)];
1229
+ for (const locale of roots) {
1230
+ const source = locale ? `/${locale}` : "/";
1231
+ if (sources.has(source)) continue;
1232
+ redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: false });
1233
+ }
1234
+ return { ...config, redirects };
1235
+ }
656
1236
  var LANGUAGE_LABELS = {
657
1237
  ar: "Arabic",
658
1238
  de: "German",
@@ -706,16 +1286,16 @@ function jsonPointer(root, pointer) {
706
1286
  }, root);
707
1287
  }
708
1288
  function readMintlifyJson(path, repositoryRoot) {
709
- if (!existsSync2(path) || !lstatSync2(path).isFile()) {
710
- throw new Error(`Mintlify config reference is not a regular file: ${relative2(repositoryRoot, path)}`);
1289
+ if (!existsSync3(path) || !lstatSync3(path).isFile()) {
1290
+ throw new Error(`Mintlify config reference is not a regular file: ${relative3(repositoryRoot, path)}`);
711
1291
  }
712
1292
  const realPath = realpathSync(path);
713
1293
  const realRoot = realpathSync(repositoryRoot);
714
- resolveWithin(realRoot, relative2(realRoot, realPath));
715
- if (lstatSync2(realPath).size > MAX_MINTLIFY_CONFIG_BYTES) {
716
- throw new Error(`Mintlify config reference exceeded ${MAX_MINTLIFY_CONFIG_BYTES / 1e6} MB: ${relative2(repositoryRoot, path)}`);
1294
+ resolveWithin(realRoot, relative3(realRoot, realPath));
1295
+ if (lstatSync3(realPath).size > MAX_MINTLIFY_CONFIG_BYTES) {
1296
+ throw new Error(`Mintlify config reference exceeded ${MAX_MINTLIFY_CONFIG_BYTES / 1e6} MB: ${relative3(repositoryRoot, path)}`);
717
1297
  }
718
- return JSON.parse(readFileSync2(realPath, "utf8"));
1298
+ return JSON.parse(readFileSync3(realPath, "utf8"));
719
1299
  }
720
1300
  function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
721
1301
  if (Array.isArray(value)) {
@@ -725,8 +1305,8 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
725
1305
  if (!object) return value;
726
1306
  if (typeof object.$ref === "string") {
727
1307
  const [filePart, pointer = ""] = object.$ref.split("#", 2);
728
- const referencedFile = filePart ? resolveWithin(dirname(currentFile), filePart) : currentFile;
729
- const relativePath = relative2(repositoryRoot, referencedFile);
1308
+ const referencedFile = filePart ? resolveWithin(dirname2(currentFile), filePart) : currentFile;
1309
+ const relativePath = relative3(repositoryRoot, referencedFile);
730
1310
  resolveWithin(repositoryRoot, relativePath);
731
1311
  const stackKey = `${referencedFile}#${pointer}`;
732
1312
  if (stack.has(stackKey)) throw new Error(`Circular Mintlify $ref: ${object.$ref}`);
@@ -751,7 +1331,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
751
1331
  );
752
1332
  }
753
1333
  function readMintlifyConfig(repositoryRoot) {
754
- const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync2);
1334
+ const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync3);
755
1335
  if (!configPath) return null;
756
1336
  const raw = readMintlifyJson(configPath, repositoryRoot);
757
1337
  return resolveJsonReferences(raw, configPath, repositoryRoot, /* @__PURE__ */ new Set());
@@ -783,8 +1363,7 @@ function containerPresentation(value) {
783
1363
  };
784
1364
  }
785
1365
  function registerReference(value, context) {
786
- const localePrefix = context.locale ? `${context.locale}/` : "";
787
- const localizedValue = localePrefix && value.replace(/^\/+/, "").startsWith(localePrefix) ? value.replace(/^\/+/, "").slice(localePrefix.length) : value;
1366
+ const localizedValue = context.locale ? mintlifyLocalizedReference(value, context.locale, context.defaultPageIds) : value;
788
1367
  const navigationId = normalizePageRef(localizedValue, context.pathPrefix);
789
1368
  if (!navigationId) return null;
790
1369
  const key = `${context.locale ?? ""}:${value}`;
@@ -1026,15 +1605,25 @@ function projectMintlifyNavigation(config, options = {}) {
1026
1605
  const seenReferences = /* @__PURE__ */ new Set();
1027
1606
  const warningKeys = /* @__PURE__ */ new Set();
1028
1607
  const navigation = objectValue2(config.navigation) ?? config;
1029
- const languages = Array.isArray(navigation.languages) ? navigation.languages.map(objectValue2).filter((value) => Boolean(value)) : [];
1608
+ const languages = Array.isArray(navigation.languages) ? navigation.languages.flatMap((value) => {
1609
+ const language = objectValue2(value);
1610
+ if (!language) return [];
1611
+ try {
1612
+ const code = Intl.getCanonicalLocales(String(language.language ?? language.locale ?? "en"))[0];
1613
+ return [{ ...language, language: code }];
1614
+ } catch {
1615
+ warnings.push({ code: "unsupported-config", message: "A navigation language with an invalid locale code was skipped." });
1616
+ return [];
1617
+ }
1618
+ }) : [];
1030
1619
  let tabs = [];
1031
1620
  let i18n;
1032
1621
  const projectionTrace = {};
1033
1622
  if (languages.length > 0) {
1034
1623
  const defaultLanguage = languages.find((entry) => entry.default === true) ?? languages[0];
1035
- const defaultLocale = String(defaultLanguage.language ?? defaultLanguage.locale ?? "en");
1624
+ const defaultLocale = String(defaultLanguage.language);
1036
1625
  const locales = languages.map((entry) => {
1037
- const code = String(entry.language ?? entry.locale ?? "en");
1626
+ const code = String(entry.language);
1038
1627
  let label = LANGUAGE_LABELS[code];
1039
1628
  try {
1040
1629
  label ??= new Intl.DisplayNames(["en"], { type: "language" }).of(code) ?? void 0;
@@ -1043,10 +1632,12 @@ function projectMintlifyNavigation(config, options = {}) {
1043
1632
  return { code, label: label ?? code.toUpperCase() };
1044
1633
  });
1045
1634
  i18n = { defaultLocale, locales };
1046
- for (const language of languages) {
1047
- const locale = String(language.language ?? language.locale ?? defaultLocale);
1635
+ const defaultPageIds = /* @__PURE__ */ new Set();
1636
+ for (const language of [defaultLanguage, ...languages.filter((entry) => entry !== defaultLanguage)]) {
1637
+ const locale = String(language.language);
1048
1638
  const context = {
1049
1639
  locale,
1640
+ defaultPageIds,
1050
1641
  pathPrefix: options.pathPrefix,
1051
1642
  references,
1052
1643
  seenReferences,
@@ -1059,7 +1650,10 @@ function projectMintlifyNavigation(config, options = {}) {
1059
1650
  "Documentation",
1060
1651
  language === defaultLanguage ? projectionTrace : void 0
1061
1652
  );
1062
- if (language === defaultLanguage) tabs = languageTabs;
1653
+ if (language === defaultLanguage) {
1654
+ tabs = languageTabs;
1655
+ for (const reference of references) defaultPageIds.add(reference.navigationId);
1656
+ }
1063
1657
  }
1064
1658
  } else {
1065
1659
  const context = { references, seenReferences, warnings, warningKeys, pathPrefix: options.pathPrefix };
@@ -1122,8 +1716,8 @@ function topLevelLabel(segment) {
1122
1716
  function groupsWithinSection(section, pageIds, sectionLabel = topLevelLabel(section)) {
1123
1717
  const groups = /* @__PURE__ */ new Map();
1124
1718
  for (const id of pageIds) {
1125
- const relative4 = id === "introduction" ? "" : id.startsWith(`${section}/`) ? id.slice(section.length + 1) : id;
1126
- const nestedSegment = relative4.includes("/") ? relative4.split("/", 1)[0] : "overview";
1719
+ const relative5 = id === "introduction" ? "" : id.startsWith(`${section}/`) ? id.slice(section.length + 1) : id;
1720
+ const nestedSegment = relative5.includes("/") ? relative5.split("/", 1)[0] : "overview";
1127
1721
  const group = groups.get(nestedSegment) ?? [];
1128
1722
  group.push(id);
1129
1723
  groups.set(nestedSegment, group);
@@ -1207,7 +1801,7 @@ function buildNavigationFromPages(pages, options = {}) {
1207
1801
  return { tabs: [{ tab: "Documentation", groups }] };
1208
1802
  }
1209
1803
  function isDocumentationExtension(filename) {
1210
- return [".md", ".mdx", ".rst", ".txt"].includes(extname2(filename).toLowerCase());
1804
+ return [".md", ".mdx", ".rst", ".txt"].includes(extname3(filename).toLowerCase());
1211
1805
  }
1212
1806
 
1213
1807
  // src/repository.ts
@@ -1269,16 +1863,16 @@ var DOCUSAURUS_CONFIG_FILENAMES = [
1269
1863
  function hasDocusaurusConfig(directory) {
1270
1864
  return DOCUSAURUS_CONFIG_FILENAMES.some((filename) => {
1271
1865
  const path = resolveWithin(directory, filename);
1272
- return existsSync3(path) && lstatSync3(path).isFile();
1866
+ return existsSync4(path) && lstatSync4(path).isFile();
1273
1867
  });
1274
1868
  }
1275
1869
  function hasMintlifyConfig(directory) {
1276
1870
  return ["docs.json", "mint.json"].some((filename) => {
1277
1871
  const path = resolveWithin(directory, filename);
1278
- if (!existsSync3(path) || !lstatSync3(path).isFile()) return false;
1872
+ if (!existsSync4(path) || !lstatSync4(path).isFile()) return false;
1279
1873
  if (filename === "mint.json") return true;
1280
1874
  try {
1281
- const config = JSON.parse(readFileSync3(path, "utf8"));
1875
+ const config = JSON.parse(readFileSync4(path, "utf8"));
1282
1876
  return String(config.$schema ?? "").includes("mintlify") || "navigation" in config;
1283
1877
  } catch {
1284
1878
  return false;
@@ -1292,7 +1886,7 @@ function findMintlifyProjectRoot(repositoryDir, docsDir) {
1292
1886
  const path = resolveWithin(repositoryDir, candidate || ".");
1293
1887
  if (hasMintlifyConfig(path)) return path;
1294
1888
  if (!candidate) break;
1295
- const parent = dirname2(candidate);
1889
+ const parent = dirname3(candidate);
1296
1890
  candidate = parent === "." ? "" : parent;
1297
1891
  }
1298
1892
  }
@@ -1317,7 +1911,7 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
1317
1911
  const path = resolveWithin(repositoryDir, candidate || ".");
1318
1912
  if (hasDocusaurusConfig(path)) return path;
1319
1913
  if (!candidate) break;
1320
- const parent = dirname2(candidate);
1914
+ const parent = dirname3(candidate);
1321
1915
  candidate = parent === "." ? "" : parent;
1322
1916
  }
1323
1917
  }
@@ -1339,9 +1933,9 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
1339
1933
  return null;
1340
1934
  }
1341
1935
  function readDocusaurusConfigSource(projectRoot) {
1342
- const path = DOCUSAURUS_CONFIG_FILENAMES.map((filename) => resolveWithin(projectRoot, filename)).find((candidate) => existsSync3(candidate) && lstatSync3(candidate).isFile());
1343
- if (!path || lstatSync3(path).size > 1e6) return "";
1344
- return readFileSync3(path, "utf8");
1936
+ const path = DOCUSAURUS_CONFIG_FILENAMES.map((filename) => resolveWithin(projectRoot, filename)).find((candidate) => existsSync4(candidate) && lstatSync4(candidate).isFile());
1937
+ if (!path || lstatSync4(path).size > 1e6) return "";
1938
+ return readFileSync4(path, "utf8");
1345
1939
  }
1346
1940
  function primaryDocusaurusDocsDirectory(projectRoot) {
1347
1941
  const source = readDocusaurusConfigSource(projectRoot);
@@ -1350,7 +1944,7 @@ function primaryDocusaurusDocsDirectory(projectRoot) {
1350
1944
  }
1351
1945
  function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
1352
1946
  const source = readDocusaurusConfigSource(projectRoot);
1353
- const projectRelative = relative3(repositoryDir, projectRoot).replace(/\\/g, "/");
1947
+ const projectRelative = relative4(repositoryDir, projectRoot).replace(/\\/g, "/");
1354
1948
  const plugins = [];
1355
1949
  const matcher = /['"]@docusaurus\/plugin-content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
1356
1950
  for (const match of source.matchAll(matcher)) {
@@ -1359,7 +1953,7 @@ function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
1359
1953
  if (!localPath || !routePrefix) continue;
1360
1954
  const docsDir = [projectRelative, localPath].filter(Boolean).join("/");
1361
1955
  const absolute = resolveWithin(repositoryDir, docsDir);
1362
- if (!existsSync3(absolute) || !lstatSync3(absolute).isDirectory()) continue;
1956
+ if (!existsSync4(absolute) || !lstatSync4(absolute).isDirectory()) continue;
1363
1957
  plugins.push({ docsDir, routePrefix });
1364
1958
  }
1365
1959
  return plugins;
@@ -1402,7 +1996,7 @@ async function cloneGitHubRepository(source, targetDir) {
1402
1996
  const args = ["clone", "--depth", "1", "--single-branch"];
1403
1997
  if (source.branch !== "HEAD") args.push("--branch", source.branch);
1404
1998
  args.push("--", source.cloneUrl, targetDir);
1405
- await new Promise((resolve2, reject) => {
1999
+ await new Promise((resolve3, reject) => {
1406
2000
  const child = spawn("git", args, { stdio: ["ignore", "ignore", "pipe"] });
1407
2001
  let stderr = "";
1408
2002
  child.stderr.setEncoding("utf8");
@@ -1411,7 +2005,7 @@ async function cloneGitHubRepository(source, targetDir) {
1411
2005
  });
1412
2006
  child.on("error", reject);
1413
2007
  child.on("close", (code) => {
1414
- if (code === 0) resolve2();
2008
+ if (code === 0) resolve3();
1415
2009
  else reject(new Error(`Failed to clone ${source.owner}/${source.repo}: ${stderr.trim() || `git exited ${code}`}`));
1416
2010
  });
1417
2011
  });
@@ -1420,19 +2014,19 @@ function detectRepositoryPlatform(repositoryDir, docsDir) {
1420
2014
  if (findMintlifyProjectRoot(repositoryDir, docsDir)) return "mintlify";
1421
2015
  const selectedRoot = docsDir === void 0 ? repositoryDir : resolveWithin(repositoryDir, docsDir || ".");
1422
2016
  const docsJson = resolveWithin(selectedRoot, "docs.json");
1423
- if (existsSync3(docsJson)) {
2017
+ if (existsSync4(docsJson)) {
1424
2018
  try {
1425
- const config = JSON.parse(readFileSync3(docsJson, "utf8"));
2019
+ const config = JSON.parse(readFileSync4(docsJson, "utf8"));
1426
2020
  if (Array.isArray(config.tabs)) return "thally";
1427
2021
  } catch {
1428
2022
  }
1429
2023
  }
1430
- if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "docusaurus";
2024
+ if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) => existsSync4(resolveWithin(repositoryDir, name)))) return "docusaurus";
1431
2025
  if (findDocusaurusProjectRoot(repositoryDir)) return "docusaurus";
1432
- if (existsSync3(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
1433
- if (existsSync3(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
1434
- if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "starlight";
1435
- if (["pages/_meta.json", "_meta.json"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "nextra";
2026
+ if (existsSync4(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
2027
+ if (existsSync4(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
2028
+ if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync4(resolveWithin(repositoryDir, name)))) return "starlight";
2029
+ if (["pages/_meta.json", "_meta.json"].some((name) => existsSync4(resolveWithin(repositoryDir, name)))) return "nextra";
1436
2030
  return "unknown";
1437
2031
  }
1438
2032
  function containsMarkdown(directory, depth = 0) {
@@ -1440,7 +2034,7 @@ function containsMarkdown(directory, depth = 0) {
1440
2034
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
1441
2035
  if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
1442
2036
  const path = resolveWithin(directory, entry.name);
1443
- if (entry.isFile() && [".md", ".mdx"].includes(extname3(entry.name).toLowerCase())) return true;
2037
+ if (entry.isFile() && [".md", ".mdx"].includes(extname4(entry.name).toLowerCase())) return true;
1444
2038
  if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
1445
2039
  }
1446
2040
  return false;
@@ -1448,7 +2042,7 @@ function containsMarkdown(directory, depth = 0) {
1448
2042
  function detectRepositoryDocsDir(repositoryDir) {
1449
2043
  for (const candidate of ["docs", "documentation", "content", "pages", "src/content", "src/pages", "guide", "guides", ""]) {
1450
2044
  const path = resolveWithin(repositoryDir, candidate);
1451
- if (existsSync3(path) && lstatSync3(path).isDirectory() && containsMarkdown(path)) return candidate;
2045
+ if (existsSync4(path) && lstatSync4(path).isDirectory() && containsMarkdown(path)) return candidate;
1452
2046
  }
1453
2047
  return "";
1454
2048
  }
@@ -1461,7 +2055,7 @@ function scanFiles(root) {
1461
2055
  if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
1462
2056
  const path = resolveWithin(directory, entry.name);
1463
2057
  if (entry.isDirectory()) visit(path);
1464
- else if (entry.isFile()) files.push({ absolutePath: path, relativePath: relative3(root, path).replace(/\\/g, "/") });
2058
+ else if (entry.isFile()) files.push({ absolutePath: path, relativePath: relative4(root, path).replace(/\\/g, "/") });
1465
2059
  }
1466
2060
  }
1467
2061
  visit(root);
@@ -1516,9 +2110,9 @@ function interpolateSnippet(snippet, attributes) {
1516
2110
  }).join("\n");
1517
2111
  }
1518
2112
  function resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot) {
1519
- const repositoryRelative = sourcePath.startsWith("@site/") ? relative3(repositoryRoot, resolveWithin(siteRoot, sourcePath.slice("@site/".length))).replace(/\\/g, "/") : sourcePath.startsWith("/") ? relative3(repositoryRoot, resolveWithin(siteRoot, sourcePath.replace(/^\/+/, ""))).replace(/\\/g, "/") : relative3(repositoryRoot, resolvePath(dirname2(currentFile), sourcePath)).replace(/\\/g, "/");
2113
+ const repositoryRelative = sourcePath.startsWith("@site/") ? relative4(repositoryRoot, resolveWithin(siteRoot, sourcePath.slice("@site/".length))).replace(/\\/g, "/") : sourcePath.startsWith("/") ? relative4(repositoryRoot, resolveWithin(siteRoot, sourcePath.replace(/^\/+/, ""))).replace(/\\/g, "/") : relative4(repositoryRoot, resolvePath(dirname3(currentFile), sourcePath)).replace(/\\/g, "/");
1520
2114
  const candidate = resolveWithin(repositoryRoot, repositoryRelative);
1521
- resolveWithin(repositoryRoot, relative3(repositoryRoot, candidate));
2115
+ resolveWithin(repositoryRoot, relative4(repositoryRoot, candidate));
1522
2116
  return candidate;
1523
2117
  }
1524
2118
  function globalSnippetAliases(files, repositoryRoot, siteRoot) {
@@ -1532,12 +2126,12 @@ function globalSnippetAliases(files, repositoryRoot, siteRoot) {
1532
2126
  if (conventionalAlias) aliases.set(conventionalAlias, file.absolutePath);
1533
2127
  }
1534
2128
  for (const file of files) {
1535
- if (![".md", ".mdx"].includes(extname3(file.relativePath).toLowerCase())) continue;
1536
- const raw = readFileSync3(file.absolutePath, "utf8");
2129
+ if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) continue;
2130
+ const raw = readFileSync4(file.absolutePath, "utf8");
1537
2131
  for (const match of raw.matchAll(matcher)) {
1538
2132
  try {
1539
2133
  const candidate = resolveSnippetPath(match[2], file.absolutePath, repositoryRoot, siteRoot);
1540
- if (existsSync3(candidate) && lstatSync3(candidate).isFile()) {
2134
+ if (existsSync4(candidate) && lstatSync4(candidate).isFile()) {
1541
2135
  aliases.set(match[1], candidate);
1542
2136
  }
1543
2137
  } catch {
@@ -1559,12 +2153,12 @@ function repositoryAssetHref(value, currentFile, siteRoot) {
1559
2153
  } catch {
1560
2154
  return null;
1561
2155
  }
1562
- if (!ASSET_EXTENSIONS.has(extname3(decodedPath).toLowerCase())) return null;
2156
+ if (!ASSET_EXTENSIONS.has(extname4(decodedPath).toLowerCase())) return null;
1563
2157
  try {
1564
- const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname2(currentFile), decodedPath);
1565
- const siteRelative = relative3(siteRoot, candidate).replace(/\\/g, "/");
2158
+ const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname3(currentFile), decodedPath);
2159
+ const siteRelative = relative4(siteRoot, candidate).replace(/\\/g, "/");
1566
2160
  resolveWithin(siteRoot, siteRelative);
1567
- if (!existsSync3(candidate) || !lstatSync3(candidate).isFile()) return null;
2161
+ if (!existsSync4(candidate) || !lstatSync4(candidate).isFile()) return null;
1568
2162
  const normalized = normalizeAssetPath(siteRelative);
1569
2163
  if (!normalized) return null;
1570
2164
  const rewritten = `/${normalized}${suffix}`;
@@ -1590,9 +2184,9 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
1590
2184
  (_statement, componentName, sourcePath) => {
1591
2185
  try {
1592
2186
  const candidate = resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot);
1593
- if (!existsSync3(candidate) || !lstatSync3(candidate).isFile()) throw new Error("file not found");
2187
+ if (!existsSync4(candidate) || !lstatSync4(candidate).isFile()) throw new Error("file not found");
1594
2188
  const nested = inlineMdxSnippets(
1595
- withoutFrontmatter(readFileSync3(candidate, "utf8")),
2189
+ withoutFrontmatter(readFileSync4(candidate, "utf8")),
1596
2190
  candidate,
1597
2191
  repositoryRoot,
1598
2192
  warnings,
@@ -1606,7 +2200,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
1606
2200
  warnings.push({
1607
2201
  code: "missing-page",
1608
2202
  message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
1609
- source: relative3(repositoryRoot, currentFile).replace(/\\/g, "/")
2203
+ source: relative4(repositoryRoot, currentFile).replace(/\\/g, "/")
1610
2204
  });
1611
2205
  snippets.set(componentName, `{/* Missing imported snippet: ${sourcePath} */}`);
1612
2206
  return "";
@@ -1616,7 +2210,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
1616
2210
  for (const [componentName, candidate] of globalAliases) {
1617
2211
  if (snippets.has(componentName) || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
1618
2212
  const nested = inlineMdxSnippets(
1619
- withoutFrontmatter(readFileSync3(candidate, "utf8")),
2213
+ withoutFrontmatter(readFileSync4(candidate, "utf8")),
1620
2214
  candidate,
1621
2215
  repositoryRoot,
1622
2216
  warnings,
@@ -1643,6 +2237,11 @@ function injectOpenApi(config, filename) {
1643
2237
  else tabs.push({ tab: "API Reference", api: { source: `/${filename}` } });
1644
2238
  return { ...config, tabs };
1645
2239
  }
2240
+ function componentSourceIdentity(sourceUrl, repositoryDir, siteRoot) {
2241
+ const url = new URL(sourceUrl);
2242
+ const repository = url.hostname.toLowerCase() === "github.com" ? `https://github.com/${url.pathname.split("/").filter(Boolean).slice(0, 2).join("/").replace(/\.git$/i, "").toLowerCase()}` : `${url.origin}${trimTrailingSlashes(url.pathname)}`;
2243
+ return JSON.stringify([repository, relative4(repositoryDir, siteRoot).replace(/\\/g, "/")]);
2244
+ }
1646
2245
  function migrateRepository(options) {
1647
2246
  const repositoryDir = options.repositoryDir;
1648
2247
  const platform = options.platform ?? detectRepositoryPlatform(repositoryDir, options.docsDir);
@@ -1652,21 +2251,23 @@ function migrateRepository(options) {
1652
2251
  if (options.docsDir !== void 0) {
1653
2252
  const selected = resolveWithin(repositoryDir, options.docsDir || ".");
1654
2253
  if (platform === "docusaurus" && hasDocusaurusConfig(selected)) {
1655
- const projectRelative = relative3(repositoryDir, selected).replace(/\\/g, "/");
2254
+ const projectRelative = relative4(repositoryDir, selected).replace(/\\/g, "/");
1656
2255
  return [projectRelative, primaryDocusaurusDocsDirectory(selected)].filter(Boolean).join("/");
1657
2256
  }
1658
2257
  return options.docsDir;
1659
2258
  }
1660
2259
  if (docusaurusProjectRoot) {
1661
- const projectRelative = relative3(repositoryDir, docusaurusProjectRoot).replace(/\\/g, "/");
2260
+ const projectRelative = relative4(repositoryDir, docusaurusProjectRoot).replace(/\\/g, "/");
1662
2261
  return [projectRelative, primaryDocusaurusDocsDirectory(docusaurusProjectRoot)].filter(Boolean).join("/");
1663
2262
  }
1664
2263
  if (platform === "mintlify" && mintlifyProjectRoot) {
1665
- return relative3(repositoryDir, mintlifyProjectRoot).replace(/\\/g, "/");
2264
+ return relative4(repositoryDir, mintlifyProjectRoot).replace(/\\/g, "/");
1666
2265
  }
1667
2266
  return platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir);
1668
2267
  })();
1669
2268
  const warnings = [];
2269
+ const componentRoot = mintlifyProjectRoot ?? repositoryDir;
2270
+ const componentMigrator = platform === "mintlify" ? createComponentMigrator(componentRoot, warnings, componentSourceIdentity(options.sourceUrl, repositoryDir, componentRoot)) : void 0;
1670
2271
  let docsConfig = { tabs: [] };
1671
2272
  const referenceMap = /* @__PURE__ */ new Map();
1672
2273
  const exactReferenceMap = /* @__PURE__ */ new Map();
@@ -1683,9 +2284,10 @@ function migrateRepository(options) {
1683
2284
  warnings.push(...projected.warnings);
1684
2285
  for (const [index, reference] of projected.pageReferences.entries()) {
1685
2286
  const key = normalizedReferenceKey(reference.ref);
1686
- referenceMap.set(key, reference);
1687
- exactReferenceMap.set(exactReferenceKey(reference.ref), reference);
1688
- referenceOrder.set(key, index);
2287
+ if (!referenceMap.has(key)) referenceMap.set(key, reference);
2288
+ const exactKey = exactReferenceKey(reference.ref);
2289
+ if (!exactReferenceMap.has(exactKey)) exactReferenceMap.set(exactKey, reference);
2290
+ if (!referenceOrder.has(key)) referenceOrder.set(key, index);
1689
2291
  }
1690
2292
  }
1691
2293
  } catch (error) {
@@ -1706,7 +2308,7 @@ function migrateRepository(options) {
1706
2308
  }
1707
2309
  }
1708
2310
  const contentRoot = resolveWithin(repositoryDir, configuredDocsDir);
1709
- if (!existsSync3(contentRoot) || !lstatSync3(contentRoot).isDirectory()) {
2311
+ if (!existsSync4(contentRoot) || !lstatSync4(contentRoot).isDirectory()) {
1710
2312
  throw new Error(`Documentation directory does not exist: ${configuredDocsDir || "."}`);
1711
2313
  }
1712
2314
  const files = scanFiles(contentRoot);
@@ -1717,6 +2319,8 @@ function migrateRepository(options) {
1717
2319
  let skipped = 0;
1718
2320
  let discovered = files.length;
1719
2321
  const localeConfig = docsConfig.i18n;
2322
+ const defaultPageIds = new Set([...referenceMap.values()].filter((reference) => !reference.locale || reference.locale === localeConfig?.defaultLocale).map((reference) => reference.navigationId));
2323
+ const routeAliases = [];
1720
2324
  const pageFiles = [...files].sort((left, right) => {
1721
2325
  const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
1722
2326
  const rightOrder = referenceOrder.get(normalizedReferenceKey(right.relativePath)) ?? Number.MAX_SAFE_INTEGER;
@@ -1735,12 +2339,12 @@ function migrateRepository(options) {
1735
2339
  skipped++;
1736
2340
  continue;
1737
2341
  }
1738
- if (![".md", ".mdx"].includes(extname3(file.relativePath).toLowerCase())) {
2342
+ if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) {
1739
2343
  skipped++;
1740
2344
  warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
1741
2345
  continue;
1742
2346
  }
1743
- const size = lstatSync3(file.absolutePath).size;
2347
+ const size = lstatSync4(file.absolutePath).size;
1744
2348
  if (size > MAX_PAGE_BYTES) {
1745
2349
  skipped++;
1746
2350
  warnings.push({ code: "skipped-file", message: "Page exceeded the 2 MB repository import limit.", source: file.relativePath });
@@ -1751,10 +2355,12 @@ function migrateRepository(options) {
1751
2355
  let locale = referenced?.locale;
1752
2356
  let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath, platform === "mintlify");
1753
2357
  if (!referenced && localeConfig) {
1754
- const prefix = file.relativePath.split("/", 1)[0];
1755
- if (localeConfig.locales.some((entry) => entry.code === prefix)) {
1756
- locale = prefix;
1757
- navigationId = pageIdFromReference(file.relativePath.slice(prefix.length + 1), platform === "mintlify");
2358
+ for (const entry of localeConfig.locales) {
2359
+ const localized = mintlifyLocalizedReference(file.relativePath, entry.code, defaultPageIds);
2360
+ if (localized === file.relativePath) continue;
2361
+ locale = entry.code;
2362
+ navigationId = pageIdFromReference(localized, platform === "mintlify");
2363
+ break;
1758
2364
  }
1759
2365
  }
1760
2366
  if (!navigationId) {
@@ -1764,8 +2370,8 @@ function migrateRepository(options) {
1764
2370
  }
1765
2371
  const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
1766
2372
  const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
1767
- const raw = inlineMdxSnippets(
1768
- readFileSync3(file.absolutePath, "utf8"),
2373
+ let raw = inlineMdxSnippets(
2374
+ readFileSync4(file.absolutePath, "utf8"),
1769
2375
  file.absolutePath,
1770
2376
  repositoryDir,
1771
2377
  warnings,
@@ -1773,13 +2379,14 @@ function migrateRepository(options) {
1773
2379
  mintlifyProjectRoot ?? docusaurusProjectRoot ?? repositoryDir,
1774
2380
  snippetAliases
1775
2381
  );
2382
+ if (componentMigrator) raw = componentMigrator.transform(raw, file.absolutePath);
1776
2383
  let docusaurusDescriptor;
1777
2384
  const page = parseMarkdownPage({
1778
2385
  id,
1779
2386
  navigationId,
1780
2387
  ...locale ? { locale } : {},
1781
2388
  raw,
1782
- source: `${options.sourceUrl}#${relative3(repositoryDir, file.absolutePath).replace(/\\/g, "/")}`,
2389
+ source: `${options.sourceUrl}#${relative4(repositoryDir, file.absolutePath).replace(/\\/g, "/")}`,
1783
2390
  ...platform === "docusaurus" ? {
1784
2391
  resolveIdentity: (frontmatter, fallback) => {
1785
2392
  const resolved = resolveDocusaurusPageIdentity(file.relativePath, frontmatter, fallback);
@@ -1816,6 +2423,12 @@ function migrateRepository(options) {
1816
2423
  }
1817
2424
  seenPageIds.add(page.id);
1818
2425
  pages.push(page);
2426
+ if (platform === "mintlify") {
2427
+ const sourcePath = exactReferenceKey(file.relativePath);
2428
+ if (sourcePath !== page.id && /^[A-Za-z0-9_./-]+$/.test(sourcePath)) {
2429
+ routeAliases.push({ source: `/${sourcePath}`, destination: `/${page.id}`, permanent: false });
2430
+ }
2431
+ }
1819
2432
  if (docusaurusDescriptor) {
1820
2433
  docusaurusDescriptors.push({ ...docusaurusDescriptor, title: page.title });
1821
2434
  }
@@ -1829,7 +2442,7 @@ function migrateRepository(options) {
1829
2442
  const docusaurusAssetRoot = docusaurusProjectRoot ?? repositoryDir;
1830
2443
  const repositoryAssets = platform === "docusaurus" && configuredDocsDir && !options.docusaurusSkipAssets ? ["static", "public"].flatMap((directory) => {
1831
2444
  const root = resolveWithin(docusaurusAssetRoot, directory);
1832
- if (!existsSync3(root) || !lstatSync3(root).isDirectory()) return [];
2445
+ if (!existsSync4(root) || !lstatSync4(root).isDirectory()) return [];
1833
2446
  return scanFiles(root).map((file) => ({
1834
2447
  ...file,
1835
2448
  relativePath: `${directory}/${file.relativePath}`
@@ -1838,9 +2451,9 @@ function migrateRepository(options) {
1838
2451
  let totalAssetBytes = 0;
1839
2452
  for (const file of [...files, ...repositoryAssets]) {
1840
2453
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
1841
- if (!ASSET_EXTENSIONS.has(extname3(file.relativePath).toLowerCase())) continue;
2454
+ if (!ASSET_EXTENSIONS.has(extname4(file.relativePath).toLowerCase())) continue;
1842
2455
  if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment)) continue;
1843
- const size = lstatSync3(file.absolutePath).size;
2456
+ const size = lstatSync4(file.absolutePath).size;
1844
2457
  if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
1845
2458
  warnings.push({ code: "limit-reached", message: "An asset was skipped because the migration asset budget was exhausted.", source: file.relativePath });
1846
2459
  continue;
@@ -1848,7 +2461,7 @@ function migrateRepository(options) {
1848
2461
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
1849
2462
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
1850
2463
  if (!assetPath) continue;
1851
- assets.push({ path: assetPath, content: readFileSync3(file.absolutePath) });
2464
+ assets.push({ path: assetPath, content: readFileSync4(file.absolutePath) });
1852
2465
  totalAssetBytes += size;
1853
2466
  }
1854
2467
  if (files.length >= MAX_SOURCE_FILES) {
@@ -1882,11 +2495,16 @@ function migrateRepository(options) {
1882
2495
  if (openApi) {
1883
2496
  const filename = basename(openApi.relativePath);
1884
2497
  if (!assets.some((asset) => asset.path === filename)) {
1885
- assets.push({ path: filename, content: readFileSync3(openApi.absolutePath) });
2498
+ assets.push({ path: filename, content: readFileSync4(openApi.absolutePath) });
1886
2499
  }
1887
2500
  docsConfig = injectOpenApi(docsConfig, filename);
1888
2501
  }
1889
- if (platform === "mintlify") docsConfig = addMintlifyDirectoryRedirects(docsConfig, pages);
2502
+ if (platform === "mintlify") {
2503
+ const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source));
2504
+ const aliases = routeAliases.filter((redirect) => !sources.has(redirect.source));
2505
+ if (aliases.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...aliases] };
2506
+ docsConfig = addMintlifyHomepageRedirects(addMintlifyDirectoryRedirects(docsConfig, pages), pages);
2507
+ }
1890
2508
  if (platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipPlugins) {
1891
2509
  for (const plugin of additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot)) {
1892
2510
  if (plugin.docsDir === configuredDocsDir) continue;
@@ -1927,6 +2545,7 @@ function migrateRepository(options) {
1927
2545
  platform,
1928
2546
  pages,
1929
2547
  assets,
2548
+ ...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
1930
2549
  docsConfig,
1931
2550
  ...mintlifyConfig ? {
1932
2551
  site: {
@@ -2043,6 +2662,7 @@ function renderMigrationFiles(bundle, options = {}) {
2043
2662
  content: renderPage(bundle, page)
2044
2663
  })),
2045
2664
  ...bundle.assets.map((asset) => ({ path: `public/${asset.path}`, content: asset.content })),
2665
+ ...(bundle.componentFiles ?? []).flatMap((file) => file.path === "src/mdx/custom-components.tsx" && options.existingComponentRegistry !== void 0 && typeof file.content === "string" ? mergeComponentRegistry(options.existingComponentRegistry, file.content) : [file]),
2046
2666
  { path: "docs.json", content: `${JSON.stringify(config, null, 2)}
2047
2667
  ` }
2048
2668
  ];
@@ -2051,7 +2671,7 @@ function renderMigrationFiles(bundle, options = {}) {
2051
2671
  // src/url.ts
2052
2672
  import { load } from "cheerio";
2053
2673
  import TurndownService from "turndown";
2054
- import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
2674
+ import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
2055
2675
  var DEFAULT_MAX_PAGES = 1e3;
2056
2676
  var DEFAULT_MAX_TOTAL_BYTES = 1e8;
2057
2677
  var MAX_DISCOVERED_URLS = 5e3;
@@ -2613,7 +3233,7 @@ function extractEmbeddedOpenApi(content) {
2613
3233
  const match = fencePattern.exec(content);
2614
3234
  if (!match) return { content };
2615
3235
  try {
2616
- const parsed = parseYaml2(match[5], { maxAliasCount: 50 });
3236
+ const parsed = parseYaml3(match[5], { maxAliasCount: 50 });
2617
3237
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { content };
2618
3238
  const document = parsed;
2619
3239
  if (typeof document.openapi !== "string" && typeof document.swagger !== "string") return { content };