@thallylabs/migrate 0.2.4 → 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,42 +1035,39 @@ function projectDocusaurusNavigation(input) {
533
1035
  };
534
1036
  }
535
1037
 
536
- // src/frontmatter.ts
537
- import { parse as parseYaml2 } from "yaml";
538
- function parseFrontmatter(raw) {
539
- const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
540
- const opening = /^---([^\r\n]*)\r?\n/.exec(source);
541
- if (!opening || opening[1].startsWith("-")) return { content: source, data: {} };
542
- const language = opening[1].trim().toLowerCase();
543
- const remainder = source.slice(opening[0].length);
544
- const closing = /^---[ \t]*\r?$/m.exec(remainder);
545
- const matter = closing ? remainder.slice(0, closing.index) : remainder;
546
- let content = closing ? remainder.slice(closing.index + closing[0].length) : "";
547
- if (content.startsWith("\r\n")) content = content.slice(2);
548
- else if (content.startsWith("\n")) content = content.slice(1);
549
- if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
550
- return { content, data: {} };
551
- }
552
- const parsed = parseYaml2(matter);
553
- return {
554
- content,
555
- data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
556
- };
557
- }
558
-
559
1038
  // src/mdx.ts
1039
+ import remarkMdx2 from "remark-mdx";
1040
+ import remarkParse2 from "remark-parse";
1041
+ import { unified as unified2 } from "unified";
560
1042
  function titleFromId(id) {
561
1043
  const value = id.split("/").at(-1) ?? id;
562
1044
  if (value === "introduction") return "Introduction";
563
1045
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
564
1046
  }
1047
+ var descriptionParser = unified2().use(remarkParse2).use(remarkMdx2);
565
1048
  function firstParagraph(content) {
566
- for (const block of content.split(/\n\s*\n/)) {
567
- const value = block.replace(/\s+/g, " ").trim();
568
- if (!value || /^(?:#|```|:::|<|import\s|export\s)/.test(value)) continue;
569
- 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 "";
570
1070
  }
571
- return "";
572
1071
  }
573
1072
  function docusaurusAdmonitionTag(kind) {
574
1073
  if (kind === "danger") return "Error";
@@ -646,24 +1145,94 @@ function parseMarkdownPage(input) {
646
1145
  }
647
1146
 
648
1147
  // src/navigation.ts
649
- import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "fs";
650
- 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";
651
1150
  var MAX_MINTLIFY_CONFIG_BYTES = 2e6;
652
1151
  function addMintlifyDirectoryRedirects(config, pages) {
653
- const pageIds = new Set(pages.filter((page) => !page.locale).map((page) => page.navigationId));
1152
+ const pageIds = new Set(pages.map((page) => page.id));
654
1153
  const redirects = [...config.redirects ?? []];
655
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
+ }
656
1162
  for (const page of pages) {
657
- if (page.locale || !/(?:^|\/)(?:overview|introduction)$/.test(page.navigationId)) continue;
658
- const parent = page.navigationId.replace(/\/(?:overview|introduction)$/, "");
1163
+ if (!/\/(?:overview|introduction)$/.test(page.navigationId)) continue;
1164
+ const parent = page.id.replace(/\/(?:overview|introduction)$/, "");
659
1165
  if (!parent || pageIds.has(parent)) continue;
660
1166
  const source = `/${parent}`;
661
- if (redirectSources.has(source)) continue;
662
- 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 });
663
1208
  redirectSources.add(source);
664
1209
  }
665
1210
  return redirects.length > 0 ? { ...config, redirects } : config;
666
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
+ }
667
1236
  var LANGUAGE_LABELS = {
668
1237
  ar: "Arabic",
669
1238
  de: "German",
@@ -717,16 +1286,16 @@ function jsonPointer(root, pointer) {
717
1286
  }, root);
718
1287
  }
719
1288
  function readMintlifyJson(path, repositoryRoot) {
720
- if (!existsSync2(path) || !lstatSync2(path).isFile()) {
721
- 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)}`);
722
1291
  }
723
1292
  const realPath = realpathSync(path);
724
1293
  const realRoot = realpathSync(repositoryRoot);
725
- resolveWithin(realRoot, relative2(realRoot, realPath));
726
- if (lstatSync2(realPath).size > MAX_MINTLIFY_CONFIG_BYTES) {
727
- 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)}`);
728
1297
  }
729
- return JSON.parse(readFileSync2(realPath, "utf8"));
1298
+ return JSON.parse(readFileSync3(realPath, "utf8"));
730
1299
  }
731
1300
  function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
732
1301
  if (Array.isArray(value)) {
@@ -736,8 +1305,8 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
736
1305
  if (!object) return value;
737
1306
  if (typeof object.$ref === "string") {
738
1307
  const [filePart, pointer = ""] = object.$ref.split("#", 2);
739
- const referencedFile = filePart ? resolveWithin(dirname(currentFile), filePart) : currentFile;
740
- const relativePath = relative2(repositoryRoot, referencedFile);
1308
+ const referencedFile = filePart ? resolveWithin(dirname2(currentFile), filePart) : currentFile;
1309
+ const relativePath = relative3(repositoryRoot, referencedFile);
741
1310
  resolveWithin(repositoryRoot, relativePath);
742
1311
  const stackKey = `${referencedFile}#${pointer}`;
743
1312
  if (stack.has(stackKey)) throw new Error(`Circular Mintlify $ref: ${object.$ref}`);
@@ -762,7 +1331,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
762
1331
  );
763
1332
  }
764
1333
  function readMintlifyConfig(repositoryRoot) {
765
- 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);
766
1335
  if (!configPath) return null;
767
1336
  const raw = readMintlifyJson(configPath, repositoryRoot);
768
1337
  return resolveJsonReferences(raw, configPath, repositoryRoot, /* @__PURE__ */ new Set());
@@ -794,8 +1363,7 @@ function containerPresentation(value) {
794
1363
  };
795
1364
  }
796
1365
  function registerReference(value, context) {
797
- const localePrefix = context.locale ? `${context.locale}/` : "";
798
- 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;
799
1367
  const navigationId = normalizePageRef(localizedValue, context.pathPrefix);
800
1368
  if (!navigationId) return null;
801
1369
  const key = `${context.locale ?? ""}:${value}`;
@@ -1037,15 +1605,25 @@ function projectMintlifyNavigation(config, options = {}) {
1037
1605
  const seenReferences = /* @__PURE__ */ new Set();
1038
1606
  const warningKeys = /* @__PURE__ */ new Set();
1039
1607
  const navigation = objectValue2(config.navigation) ?? config;
1040
- 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
+ }) : [];
1041
1619
  let tabs = [];
1042
1620
  let i18n;
1043
1621
  const projectionTrace = {};
1044
1622
  if (languages.length > 0) {
1045
1623
  const defaultLanguage = languages.find((entry) => entry.default === true) ?? languages[0];
1046
- const defaultLocale = String(defaultLanguage.language ?? defaultLanguage.locale ?? "en");
1624
+ const defaultLocale = String(defaultLanguage.language);
1047
1625
  const locales = languages.map((entry) => {
1048
- const code = String(entry.language ?? entry.locale ?? "en");
1626
+ const code = String(entry.language);
1049
1627
  let label = LANGUAGE_LABELS[code];
1050
1628
  try {
1051
1629
  label ??= new Intl.DisplayNames(["en"], { type: "language" }).of(code) ?? void 0;
@@ -1054,10 +1632,12 @@ function projectMintlifyNavigation(config, options = {}) {
1054
1632
  return { code, label: label ?? code.toUpperCase() };
1055
1633
  });
1056
1634
  i18n = { defaultLocale, locales };
1057
- for (const language of languages) {
1058
- 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);
1059
1638
  const context = {
1060
1639
  locale,
1640
+ defaultPageIds,
1061
1641
  pathPrefix: options.pathPrefix,
1062
1642
  references,
1063
1643
  seenReferences,
@@ -1070,7 +1650,10 @@ function projectMintlifyNavigation(config, options = {}) {
1070
1650
  "Documentation",
1071
1651
  language === defaultLanguage ? projectionTrace : void 0
1072
1652
  );
1073
- if (language === defaultLanguage) tabs = languageTabs;
1653
+ if (language === defaultLanguage) {
1654
+ tabs = languageTabs;
1655
+ for (const reference of references) defaultPageIds.add(reference.navigationId);
1656
+ }
1074
1657
  }
1075
1658
  } else {
1076
1659
  const context = { references, seenReferences, warnings, warningKeys, pathPrefix: options.pathPrefix };
@@ -1133,8 +1716,8 @@ function topLevelLabel(segment) {
1133
1716
  function groupsWithinSection(section, pageIds, sectionLabel = topLevelLabel(section)) {
1134
1717
  const groups = /* @__PURE__ */ new Map();
1135
1718
  for (const id of pageIds) {
1136
- const relative4 = id === "introduction" ? "" : id.startsWith(`${section}/`) ? id.slice(section.length + 1) : id;
1137
- 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";
1138
1721
  const group = groups.get(nestedSegment) ?? [];
1139
1722
  group.push(id);
1140
1723
  groups.set(nestedSegment, group);
@@ -1218,7 +1801,7 @@ function buildNavigationFromPages(pages, options = {}) {
1218
1801
  return { tabs: [{ tab: "Documentation", groups }] };
1219
1802
  }
1220
1803
  function isDocumentationExtension(filename) {
1221
- return [".md", ".mdx", ".rst", ".txt"].includes(extname2(filename).toLowerCase());
1804
+ return [".md", ".mdx", ".rst", ".txt"].includes(extname3(filename).toLowerCase());
1222
1805
  }
1223
1806
 
1224
1807
  // src/repository.ts
@@ -1280,16 +1863,16 @@ var DOCUSAURUS_CONFIG_FILENAMES = [
1280
1863
  function hasDocusaurusConfig(directory) {
1281
1864
  return DOCUSAURUS_CONFIG_FILENAMES.some((filename) => {
1282
1865
  const path = resolveWithin(directory, filename);
1283
- return existsSync3(path) && lstatSync3(path).isFile();
1866
+ return existsSync4(path) && lstatSync4(path).isFile();
1284
1867
  });
1285
1868
  }
1286
1869
  function hasMintlifyConfig(directory) {
1287
1870
  return ["docs.json", "mint.json"].some((filename) => {
1288
1871
  const path = resolveWithin(directory, filename);
1289
- if (!existsSync3(path) || !lstatSync3(path).isFile()) return false;
1872
+ if (!existsSync4(path) || !lstatSync4(path).isFile()) return false;
1290
1873
  if (filename === "mint.json") return true;
1291
1874
  try {
1292
- const config = JSON.parse(readFileSync3(path, "utf8"));
1875
+ const config = JSON.parse(readFileSync4(path, "utf8"));
1293
1876
  return String(config.$schema ?? "").includes("mintlify") || "navigation" in config;
1294
1877
  } catch {
1295
1878
  return false;
@@ -1303,7 +1886,7 @@ function findMintlifyProjectRoot(repositoryDir, docsDir) {
1303
1886
  const path = resolveWithin(repositoryDir, candidate || ".");
1304
1887
  if (hasMintlifyConfig(path)) return path;
1305
1888
  if (!candidate) break;
1306
- const parent = dirname2(candidate);
1889
+ const parent = dirname3(candidate);
1307
1890
  candidate = parent === "." ? "" : parent;
1308
1891
  }
1309
1892
  }
@@ -1328,7 +1911,7 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
1328
1911
  const path = resolveWithin(repositoryDir, candidate || ".");
1329
1912
  if (hasDocusaurusConfig(path)) return path;
1330
1913
  if (!candidate) break;
1331
- const parent = dirname2(candidate);
1914
+ const parent = dirname3(candidate);
1332
1915
  candidate = parent === "." ? "" : parent;
1333
1916
  }
1334
1917
  }
@@ -1350,9 +1933,9 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
1350
1933
  return null;
1351
1934
  }
1352
1935
  function readDocusaurusConfigSource(projectRoot) {
1353
- const path = DOCUSAURUS_CONFIG_FILENAMES.map((filename) => resolveWithin(projectRoot, filename)).find((candidate) => existsSync3(candidate) && lstatSync3(candidate).isFile());
1354
- if (!path || lstatSync3(path).size > 1e6) return "";
1355
- 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");
1356
1939
  }
1357
1940
  function primaryDocusaurusDocsDirectory(projectRoot) {
1358
1941
  const source = readDocusaurusConfigSource(projectRoot);
@@ -1361,7 +1944,7 @@ function primaryDocusaurusDocsDirectory(projectRoot) {
1361
1944
  }
1362
1945
  function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
1363
1946
  const source = readDocusaurusConfigSource(projectRoot);
1364
- const projectRelative = relative3(repositoryDir, projectRoot).replace(/\\/g, "/");
1947
+ const projectRelative = relative4(repositoryDir, projectRoot).replace(/\\/g, "/");
1365
1948
  const plugins = [];
1366
1949
  const matcher = /['"]@docusaurus\/plugin-content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
1367
1950
  for (const match of source.matchAll(matcher)) {
@@ -1370,7 +1953,7 @@ function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
1370
1953
  if (!localPath || !routePrefix) continue;
1371
1954
  const docsDir = [projectRelative, localPath].filter(Boolean).join("/");
1372
1955
  const absolute = resolveWithin(repositoryDir, docsDir);
1373
- if (!existsSync3(absolute) || !lstatSync3(absolute).isDirectory()) continue;
1956
+ if (!existsSync4(absolute) || !lstatSync4(absolute).isDirectory()) continue;
1374
1957
  plugins.push({ docsDir, routePrefix });
1375
1958
  }
1376
1959
  return plugins;
@@ -1413,7 +1996,7 @@ async function cloneGitHubRepository(source, targetDir) {
1413
1996
  const args = ["clone", "--depth", "1", "--single-branch"];
1414
1997
  if (source.branch !== "HEAD") args.push("--branch", source.branch);
1415
1998
  args.push("--", source.cloneUrl, targetDir);
1416
- await new Promise((resolve2, reject) => {
1999
+ await new Promise((resolve3, reject) => {
1417
2000
  const child = spawn("git", args, { stdio: ["ignore", "ignore", "pipe"] });
1418
2001
  let stderr = "";
1419
2002
  child.stderr.setEncoding("utf8");
@@ -1422,7 +2005,7 @@ async function cloneGitHubRepository(source, targetDir) {
1422
2005
  });
1423
2006
  child.on("error", reject);
1424
2007
  child.on("close", (code) => {
1425
- if (code === 0) resolve2();
2008
+ if (code === 0) resolve3();
1426
2009
  else reject(new Error(`Failed to clone ${source.owner}/${source.repo}: ${stderr.trim() || `git exited ${code}`}`));
1427
2010
  });
1428
2011
  });
@@ -1431,19 +2014,19 @@ function detectRepositoryPlatform(repositoryDir, docsDir) {
1431
2014
  if (findMintlifyProjectRoot(repositoryDir, docsDir)) return "mintlify";
1432
2015
  const selectedRoot = docsDir === void 0 ? repositoryDir : resolveWithin(repositoryDir, docsDir || ".");
1433
2016
  const docsJson = resolveWithin(selectedRoot, "docs.json");
1434
- if (existsSync3(docsJson)) {
2017
+ if (existsSync4(docsJson)) {
1435
2018
  try {
1436
- const config = JSON.parse(readFileSync3(docsJson, "utf8"));
2019
+ const config = JSON.parse(readFileSync4(docsJson, "utf8"));
1437
2020
  if (Array.isArray(config.tabs)) return "thally";
1438
2021
  } catch {
1439
2022
  }
1440
2023
  }
1441
- 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";
1442
2025
  if (findDocusaurusProjectRoot(repositoryDir)) return "docusaurus";
1443
- if (existsSync3(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
1444
- if (existsSync3(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
1445
- if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "starlight";
1446
- 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";
1447
2030
  return "unknown";
1448
2031
  }
1449
2032
  function containsMarkdown(directory, depth = 0) {
@@ -1451,7 +2034,7 @@ function containsMarkdown(directory, depth = 0) {
1451
2034
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
1452
2035
  if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
1453
2036
  const path = resolveWithin(directory, entry.name);
1454
- 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;
1455
2038
  if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
1456
2039
  }
1457
2040
  return false;
@@ -1459,7 +2042,7 @@ function containsMarkdown(directory, depth = 0) {
1459
2042
  function detectRepositoryDocsDir(repositoryDir) {
1460
2043
  for (const candidate of ["docs", "documentation", "content", "pages", "src/content", "src/pages", "guide", "guides", ""]) {
1461
2044
  const path = resolveWithin(repositoryDir, candidate);
1462
- if (existsSync3(path) && lstatSync3(path).isDirectory() && containsMarkdown(path)) return candidate;
2045
+ if (existsSync4(path) && lstatSync4(path).isDirectory() && containsMarkdown(path)) return candidate;
1463
2046
  }
1464
2047
  return "";
1465
2048
  }
@@ -1472,7 +2055,7 @@ function scanFiles(root) {
1472
2055
  if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
1473
2056
  const path = resolveWithin(directory, entry.name);
1474
2057
  if (entry.isDirectory()) visit(path);
1475
- 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, "/") });
1476
2059
  }
1477
2060
  }
1478
2061
  visit(root);
@@ -1527,9 +2110,9 @@ function interpolateSnippet(snippet, attributes) {
1527
2110
  }).join("\n");
1528
2111
  }
1529
2112
  function resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot) {
1530
- 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, "/");
1531
2114
  const candidate = resolveWithin(repositoryRoot, repositoryRelative);
1532
- resolveWithin(repositoryRoot, relative3(repositoryRoot, candidate));
2115
+ resolveWithin(repositoryRoot, relative4(repositoryRoot, candidate));
1533
2116
  return candidate;
1534
2117
  }
1535
2118
  function globalSnippetAliases(files, repositoryRoot, siteRoot) {
@@ -1543,12 +2126,12 @@ function globalSnippetAliases(files, repositoryRoot, siteRoot) {
1543
2126
  if (conventionalAlias) aliases.set(conventionalAlias, file.absolutePath);
1544
2127
  }
1545
2128
  for (const file of files) {
1546
- if (![".md", ".mdx"].includes(extname3(file.relativePath).toLowerCase())) continue;
1547
- const raw = readFileSync3(file.absolutePath, "utf8");
2129
+ if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) continue;
2130
+ const raw = readFileSync4(file.absolutePath, "utf8");
1548
2131
  for (const match of raw.matchAll(matcher)) {
1549
2132
  try {
1550
2133
  const candidate = resolveSnippetPath(match[2], file.absolutePath, repositoryRoot, siteRoot);
1551
- if (existsSync3(candidate) && lstatSync3(candidate).isFile()) {
2134
+ if (existsSync4(candidate) && lstatSync4(candidate).isFile()) {
1552
2135
  aliases.set(match[1], candidate);
1553
2136
  }
1554
2137
  } catch {
@@ -1570,12 +2153,12 @@ function repositoryAssetHref(value, currentFile, siteRoot) {
1570
2153
  } catch {
1571
2154
  return null;
1572
2155
  }
1573
- if (!ASSET_EXTENSIONS.has(extname3(decodedPath).toLowerCase())) return null;
2156
+ if (!ASSET_EXTENSIONS.has(extname4(decodedPath).toLowerCase())) return null;
1574
2157
  try {
1575
- const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname2(currentFile), decodedPath);
1576
- 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, "/");
1577
2160
  resolveWithin(siteRoot, siteRelative);
1578
- if (!existsSync3(candidate) || !lstatSync3(candidate).isFile()) return null;
2161
+ if (!existsSync4(candidate) || !lstatSync4(candidate).isFile()) return null;
1579
2162
  const normalized = normalizeAssetPath(siteRelative);
1580
2163
  if (!normalized) return null;
1581
2164
  const rewritten = `/${normalized}${suffix}`;
@@ -1601,9 +2184,9 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
1601
2184
  (_statement, componentName, sourcePath) => {
1602
2185
  try {
1603
2186
  const candidate = resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot);
1604
- 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");
1605
2188
  const nested = inlineMdxSnippets(
1606
- withoutFrontmatter(readFileSync3(candidate, "utf8")),
2189
+ withoutFrontmatter(readFileSync4(candidate, "utf8")),
1607
2190
  candidate,
1608
2191
  repositoryRoot,
1609
2192
  warnings,
@@ -1617,7 +2200,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
1617
2200
  warnings.push({
1618
2201
  code: "missing-page",
1619
2202
  message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
1620
- source: relative3(repositoryRoot, currentFile).replace(/\\/g, "/")
2203
+ source: relative4(repositoryRoot, currentFile).replace(/\\/g, "/")
1621
2204
  });
1622
2205
  snippets.set(componentName, `{/* Missing imported snippet: ${sourcePath} */}`);
1623
2206
  return "";
@@ -1627,7 +2210,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
1627
2210
  for (const [componentName, candidate] of globalAliases) {
1628
2211
  if (snippets.has(componentName) || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
1629
2212
  const nested = inlineMdxSnippets(
1630
- withoutFrontmatter(readFileSync3(candidate, "utf8")),
2213
+ withoutFrontmatter(readFileSync4(candidate, "utf8")),
1631
2214
  candidate,
1632
2215
  repositoryRoot,
1633
2216
  warnings,
@@ -1654,6 +2237,11 @@ function injectOpenApi(config, filename) {
1654
2237
  else tabs.push({ tab: "API Reference", api: { source: `/${filename}` } });
1655
2238
  return { ...config, tabs };
1656
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
+ }
1657
2245
  function migrateRepository(options) {
1658
2246
  const repositoryDir = options.repositoryDir;
1659
2247
  const platform = options.platform ?? detectRepositoryPlatform(repositoryDir, options.docsDir);
@@ -1663,21 +2251,23 @@ function migrateRepository(options) {
1663
2251
  if (options.docsDir !== void 0) {
1664
2252
  const selected = resolveWithin(repositoryDir, options.docsDir || ".");
1665
2253
  if (platform === "docusaurus" && hasDocusaurusConfig(selected)) {
1666
- const projectRelative = relative3(repositoryDir, selected).replace(/\\/g, "/");
2254
+ const projectRelative = relative4(repositoryDir, selected).replace(/\\/g, "/");
1667
2255
  return [projectRelative, primaryDocusaurusDocsDirectory(selected)].filter(Boolean).join("/");
1668
2256
  }
1669
2257
  return options.docsDir;
1670
2258
  }
1671
2259
  if (docusaurusProjectRoot) {
1672
- const projectRelative = relative3(repositoryDir, docusaurusProjectRoot).replace(/\\/g, "/");
2260
+ const projectRelative = relative4(repositoryDir, docusaurusProjectRoot).replace(/\\/g, "/");
1673
2261
  return [projectRelative, primaryDocusaurusDocsDirectory(docusaurusProjectRoot)].filter(Boolean).join("/");
1674
2262
  }
1675
2263
  if (platform === "mintlify" && mintlifyProjectRoot) {
1676
- return relative3(repositoryDir, mintlifyProjectRoot).replace(/\\/g, "/");
2264
+ return relative4(repositoryDir, mintlifyProjectRoot).replace(/\\/g, "/");
1677
2265
  }
1678
2266
  return platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir);
1679
2267
  })();
1680
2268
  const warnings = [];
2269
+ const componentRoot = mintlifyProjectRoot ?? repositoryDir;
2270
+ const componentMigrator = platform === "mintlify" ? createComponentMigrator(componentRoot, warnings, componentSourceIdentity(options.sourceUrl, repositoryDir, componentRoot)) : void 0;
1681
2271
  let docsConfig = { tabs: [] };
1682
2272
  const referenceMap = /* @__PURE__ */ new Map();
1683
2273
  const exactReferenceMap = /* @__PURE__ */ new Map();
@@ -1694,9 +2284,10 @@ function migrateRepository(options) {
1694
2284
  warnings.push(...projected.warnings);
1695
2285
  for (const [index, reference] of projected.pageReferences.entries()) {
1696
2286
  const key = normalizedReferenceKey(reference.ref);
1697
- referenceMap.set(key, reference);
1698
- exactReferenceMap.set(exactReferenceKey(reference.ref), reference);
1699
- 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);
1700
2291
  }
1701
2292
  }
1702
2293
  } catch (error) {
@@ -1717,7 +2308,7 @@ function migrateRepository(options) {
1717
2308
  }
1718
2309
  }
1719
2310
  const contentRoot = resolveWithin(repositoryDir, configuredDocsDir);
1720
- if (!existsSync3(contentRoot) || !lstatSync3(contentRoot).isDirectory()) {
2311
+ if (!existsSync4(contentRoot) || !lstatSync4(contentRoot).isDirectory()) {
1721
2312
  throw new Error(`Documentation directory does not exist: ${configuredDocsDir || "."}`);
1722
2313
  }
1723
2314
  const files = scanFiles(contentRoot);
@@ -1728,6 +2319,8 @@ function migrateRepository(options) {
1728
2319
  let skipped = 0;
1729
2320
  let discovered = files.length;
1730
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 = [];
1731
2324
  const pageFiles = [...files].sort((left, right) => {
1732
2325
  const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
1733
2326
  const rightOrder = referenceOrder.get(normalizedReferenceKey(right.relativePath)) ?? Number.MAX_SAFE_INTEGER;
@@ -1746,12 +2339,12 @@ function migrateRepository(options) {
1746
2339
  skipped++;
1747
2340
  continue;
1748
2341
  }
1749
- if (![".md", ".mdx"].includes(extname3(file.relativePath).toLowerCase())) {
2342
+ if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) {
1750
2343
  skipped++;
1751
2344
  warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
1752
2345
  continue;
1753
2346
  }
1754
- const size = lstatSync3(file.absolutePath).size;
2347
+ const size = lstatSync4(file.absolutePath).size;
1755
2348
  if (size > MAX_PAGE_BYTES) {
1756
2349
  skipped++;
1757
2350
  warnings.push({ code: "skipped-file", message: "Page exceeded the 2 MB repository import limit.", source: file.relativePath });
@@ -1762,10 +2355,12 @@ function migrateRepository(options) {
1762
2355
  let locale = referenced?.locale;
1763
2356
  let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath, platform === "mintlify");
1764
2357
  if (!referenced && localeConfig) {
1765
- const prefix = file.relativePath.split("/", 1)[0];
1766
- if (localeConfig.locales.some((entry) => entry.code === prefix)) {
1767
- locale = prefix;
1768
- 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;
1769
2364
  }
1770
2365
  }
1771
2366
  if (!navigationId) {
@@ -1775,8 +2370,8 @@ function migrateRepository(options) {
1775
2370
  }
1776
2371
  const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
1777
2372
  const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
1778
- const raw = inlineMdxSnippets(
1779
- readFileSync3(file.absolutePath, "utf8"),
2373
+ let raw = inlineMdxSnippets(
2374
+ readFileSync4(file.absolutePath, "utf8"),
1780
2375
  file.absolutePath,
1781
2376
  repositoryDir,
1782
2377
  warnings,
@@ -1784,13 +2379,14 @@ function migrateRepository(options) {
1784
2379
  mintlifyProjectRoot ?? docusaurusProjectRoot ?? repositoryDir,
1785
2380
  snippetAliases
1786
2381
  );
2382
+ if (componentMigrator) raw = componentMigrator.transform(raw, file.absolutePath);
1787
2383
  let docusaurusDescriptor;
1788
2384
  const page = parseMarkdownPage({
1789
2385
  id,
1790
2386
  navigationId,
1791
2387
  ...locale ? { locale } : {},
1792
2388
  raw,
1793
- source: `${options.sourceUrl}#${relative3(repositoryDir, file.absolutePath).replace(/\\/g, "/")}`,
2389
+ source: `${options.sourceUrl}#${relative4(repositoryDir, file.absolutePath).replace(/\\/g, "/")}`,
1794
2390
  ...platform === "docusaurus" ? {
1795
2391
  resolveIdentity: (frontmatter, fallback) => {
1796
2392
  const resolved = resolveDocusaurusPageIdentity(file.relativePath, frontmatter, fallback);
@@ -1827,6 +2423,12 @@ function migrateRepository(options) {
1827
2423
  }
1828
2424
  seenPageIds.add(page.id);
1829
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
+ }
1830
2432
  if (docusaurusDescriptor) {
1831
2433
  docusaurusDescriptors.push({ ...docusaurusDescriptor, title: page.title });
1832
2434
  }
@@ -1840,7 +2442,7 @@ function migrateRepository(options) {
1840
2442
  const docusaurusAssetRoot = docusaurusProjectRoot ?? repositoryDir;
1841
2443
  const repositoryAssets = platform === "docusaurus" && configuredDocsDir && !options.docusaurusSkipAssets ? ["static", "public"].flatMap((directory) => {
1842
2444
  const root = resolveWithin(docusaurusAssetRoot, directory);
1843
- if (!existsSync3(root) || !lstatSync3(root).isDirectory()) return [];
2445
+ if (!existsSync4(root) || !lstatSync4(root).isDirectory()) return [];
1844
2446
  return scanFiles(root).map((file) => ({
1845
2447
  ...file,
1846
2448
  relativePath: `${directory}/${file.relativePath}`
@@ -1849,9 +2451,9 @@ function migrateRepository(options) {
1849
2451
  let totalAssetBytes = 0;
1850
2452
  for (const file of [...files, ...repositoryAssets]) {
1851
2453
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
1852
- if (!ASSET_EXTENSIONS.has(extname3(file.relativePath).toLowerCase())) continue;
2454
+ if (!ASSET_EXTENSIONS.has(extname4(file.relativePath).toLowerCase())) continue;
1853
2455
  if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment)) continue;
1854
- const size = lstatSync3(file.absolutePath).size;
2456
+ const size = lstatSync4(file.absolutePath).size;
1855
2457
  if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
1856
2458
  warnings.push({ code: "limit-reached", message: "An asset was skipped because the migration asset budget was exhausted.", source: file.relativePath });
1857
2459
  continue;
@@ -1859,7 +2461,7 @@ function migrateRepository(options) {
1859
2461
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
1860
2462
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
1861
2463
  if (!assetPath) continue;
1862
- assets.push({ path: assetPath, content: readFileSync3(file.absolutePath) });
2464
+ assets.push({ path: assetPath, content: readFileSync4(file.absolutePath) });
1863
2465
  totalAssetBytes += size;
1864
2466
  }
1865
2467
  if (files.length >= MAX_SOURCE_FILES) {
@@ -1893,11 +2495,16 @@ function migrateRepository(options) {
1893
2495
  if (openApi) {
1894
2496
  const filename = basename(openApi.relativePath);
1895
2497
  if (!assets.some((asset) => asset.path === filename)) {
1896
- assets.push({ path: filename, content: readFileSync3(openApi.absolutePath) });
2498
+ assets.push({ path: filename, content: readFileSync4(openApi.absolutePath) });
1897
2499
  }
1898
2500
  docsConfig = injectOpenApi(docsConfig, filename);
1899
2501
  }
1900
- 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
+ }
1901
2508
  if (platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipPlugins) {
1902
2509
  for (const plugin of additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot)) {
1903
2510
  if (plugin.docsDir === configuredDocsDir) continue;
@@ -1938,6 +2545,7 @@ function migrateRepository(options) {
1938
2545
  platform,
1939
2546
  pages,
1940
2547
  assets,
2548
+ ...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
1941
2549
  docsConfig,
1942
2550
  ...mintlifyConfig ? {
1943
2551
  site: {
@@ -2054,6 +2662,7 @@ function renderMigrationFiles(bundle, options = {}) {
2054
2662
  content: renderPage(bundle, page)
2055
2663
  })),
2056
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]),
2057
2666
  { path: "docs.json", content: `${JSON.stringify(config, null, 2)}
2058
2667
  ` }
2059
2668
  ];