@thallylabs/migrate 0.2.4 → 0.2.6
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/README.md +25 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +948 -151
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
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
|
|
5
|
-
lstatSync as
|
|
6
|
-
readFileSync as
|
|
4
|
+
existsSync as existsSync4,
|
|
5
|
+
lstatSync as lstatSync4,
|
|
6
|
+
readFileSync as readFileSync4,
|
|
7
7
|
readdirSync
|
|
8
8
|
} from "fs";
|
|
9
|
-
import { basename, dirname as
|
|
9
|
+
import { basename, dirname as dirname3, extname as extname4, relative as relative4, resolve as resolvePath } from "path";
|
|
10
10
|
|
|
11
|
-
// src/
|
|
11
|
+
// src/components.ts
|
|
12
|
+
import { createHash } from "crypto";
|
|
12
13
|
import { existsSync, lstatSync, readFileSync } from "fs";
|
|
13
|
-
import { extname,
|
|
14
|
-
import
|
|
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 (
|
|
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
|
|
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 (!
|
|
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 (
|
|
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) =>
|
|
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 =
|
|
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 (!
|
|
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) :
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
if (
|
|
569
|
-
|
|
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";
|
|
@@ -605,8 +1104,30 @@ function normalizeDocusaurusAdmonitions(body) {
|
|
|
605
1104
|
return line;
|
|
606
1105
|
}).join("\n");
|
|
607
1106
|
}
|
|
1107
|
+
var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
|
|
1108
|
+
"Tabs",
|
|
1109
|
+
"TabItem",
|
|
1110
|
+
"Link",
|
|
1111
|
+
"DocCardList",
|
|
1112
|
+
"TOCInline"
|
|
1113
|
+
]);
|
|
1114
|
+
function isGlobalDocusaurusImport(line) {
|
|
1115
|
+
const trimmed = line.trim().replace(/;$/, "").trimEnd();
|
|
1116
|
+
if (!trimmed.startsWith("import ")) return false;
|
|
1117
|
+
const separator = trimmed.indexOf(" from ", "import ".length);
|
|
1118
|
+
if (separator < 0) return false;
|
|
1119
|
+
const component = trimmed.slice("import ".length, separator).trim();
|
|
1120
|
+
if (!GLOBAL_DOCUSARUS_COMPONENTS.has(component)) return false;
|
|
1121
|
+
const source = trimmed.slice(separator + " from ".length);
|
|
1122
|
+
if (source.length < 3) return false;
|
|
1123
|
+
const quote = source[0];
|
|
1124
|
+
if (quote !== "'" && quote !== '"' || source.at(-1) !== quote) return false;
|
|
1125
|
+
const moduleName = source.slice(1, -1);
|
|
1126
|
+
return moduleName.startsWith("@theme/") || moduleName.startsWith("@docusaurus/");
|
|
1127
|
+
}
|
|
608
1128
|
function normalizeMdx(body) {
|
|
609
|
-
|
|
1129
|
+
const withoutGlobalImports = body.split("\n").filter((line) => !isGlobalDocusaurusImport(line)).join("\n");
|
|
1130
|
+
return normalizeDocusaurusAdmonitions(withoutGlobalImports).replace(/<TabItem\b([^>]*)>/g, (_match, attributes) => {
|
|
610
1131
|
const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
|
|
611
1132
|
return `<Tab title="${title.replace(/"/g, """)}">`;
|
|
612
1133
|
}).replace(/<\/TabItem>/g, "</Tab>").replace(/<Link\b([^>]*)\bto=(?:"([^"]*)"|'([^']*)')([^>]*)>/g, (_match, before, doubleQuoted, singleQuoted, after) => `<a${before}href="${doubleQuoted ?? singleQuoted}"${after}>`).replace(/<\/Link>/g, "</a>").replace(/<(?:DocCardList|TOCInline)\b[^>]*\/>/g, "").replace(/<!--([\s\S]*?)-->/g, (_match, content) => `{/*${content}*/}`).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
|
|
@@ -646,24 +1167,94 @@ function parseMarkdownPage(input) {
|
|
|
646
1167
|
}
|
|
647
1168
|
|
|
648
1169
|
// src/navigation.ts
|
|
649
|
-
import { existsSync as
|
|
650
|
-
import { dirname, extname as
|
|
1170
|
+
import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, realpathSync } from "fs";
|
|
1171
|
+
import { dirname as dirname2, extname as extname3, relative as relative3 } from "path";
|
|
651
1172
|
var MAX_MINTLIFY_CONFIG_BYTES = 2e6;
|
|
652
1173
|
function addMintlifyDirectoryRedirects(config, pages) {
|
|
653
|
-
const pageIds = new Set(pages.
|
|
1174
|
+
const pageIds = new Set(pages.map((page) => page.id));
|
|
654
1175
|
const redirects = [...config.redirects ?? []];
|
|
655
1176
|
const redirectSources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
|
|
1177
|
+
function hasAuthoredRedirect(source) {
|
|
1178
|
+
if (redirectSources.has(source)) return true;
|
|
1179
|
+
return (config.redirects ?? []).some((redirect) => {
|
|
1180
|
+
const match = redirect.source.match(/^(.*)\/:[A-Za-z_][A-Za-z0-9_]*\*\/?$/);
|
|
1181
|
+
return match ? source === match[1] || source.startsWith(`${match[1]}/`) : false;
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
656
1184
|
for (const page of pages) {
|
|
657
|
-
if (
|
|
658
|
-
const parent = page.
|
|
1185
|
+
if (!/\/(?:overview|introduction)$/.test(page.navigationId)) continue;
|
|
1186
|
+
const parent = page.id.replace(/\/(?:overview|introduction)$/, "");
|
|
659
1187
|
if (!parent || pageIds.has(parent)) continue;
|
|
660
1188
|
const source = `/${parent}`;
|
|
661
|
-
if (
|
|
662
|
-
redirects.push({ source, destination: `/${page.
|
|
1189
|
+
if (hasAuthoredRedirect(source)) continue;
|
|
1190
|
+
redirects.push({ source, destination: `/${page.id}`, permanent: false });
|
|
1191
|
+
redirectSources.add(source);
|
|
1192
|
+
}
|
|
1193
|
+
const navigationOrder = [];
|
|
1194
|
+
function collectPages(nodes) {
|
|
1195
|
+
for (const node of nodes) {
|
|
1196
|
+
if (typeof node === "string") navigationOrder.push(node);
|
|
1197
|
+
else if (!node.hidden) collectPages(node.pages);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
for (const tab of config.tabs) {
|
|
1201
|
+
if (!tab.hidden) collectPages([...tab.pages ?? [], ...tab.groups ?? []]);
|
|
1202
|
+
}
|
|
1203
|
+
const pagesByNavigationId = /* @__PURE__ */ new Map();
|
|
1204
|
+
for (const page of pages) {
|
|
1205
|
+
if (page.hidden) continue;
|
|
1206
|
+
const variants = pagesByNavigationId.get(page.navigationId) ?? [];
|
|
1207
|
+
variants.push(page);
|
|
1208
|
+
pagesByNavigationId.set(page.navigationId, variants);
|
|
1209
|
+
}
|
|
1210
|
+
const firstDescendantByDirectory = /* @__PURE__ */ new Map();
|
|
1211
|
+
for (const navigationId of navigationOrder) {
|
|
1212
|
+
for (const page of pagesByNavigationId.get(navigationId) ?? []) {
|
|
1213
|
+
const segments = page.id.split("/");
|
|
1214
|
+
for (let length = 1; length < segments.length; length++) {
|
|
1215
|
+
const directory = segments.slice(0, length).join("/");
|
|
1216
|
+
if (!firstDescendantByDirectory.has(directory)) firstDescendantByDirectory.set(directory, page);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
for (const redirect of config.redirects ?? []) {
|
|
1221
|
+
const destination = redirect.destination.split(/[?#]/, 1)[0].replace(/\/:[A-Za-z_][A-Za-z0-9_]*\*\/?$/, "");
|
|
1222
|
+
if (!destination.startsWith("/") || destination.startsWith("//") || /[:*()[\]{}]/.test(destination)) continue;
|
|
1223
|
+
const directory = trimEdgeSlashes(destination);
|
|
1224
|
+
if (!directory || pageIds.has(directory)) continue;
|
|
1225
|
+
const source = `/${directory}`;
|
|
1226
|
+
if (hasAuthoredRedirect(source)) continue;
|
|
1227
|
+
const landing = firstDescendantByDirectory.get(directory);
|
|
1228
|
+
if (!landing) continue;
|
|
1229
|
+
redirects.push({ source, destination: `/${landing.id}`, permanent: false });
|
|
663
1230
|
redirectSources.add(source);
|
|
664
1231
|
}
|
|
665
1232
|
return redirects.length > 0 ? { ...config, redirects } : config;
|
|
666
1233
|
}
|
|
1234
|
+
function addMintlifyHomepageRedirects(config, pages) {
|
|
1235
|
+
const defaultPages = new Set(pages.filter((page) => !page.locale || page.locale === config.i18n?.defaultLocale).map((page) => page.navigationId));
|
|
1236
|
+
function firstPage(nodes) {
|
|
1237
|
+
for (const node of nodes) {
|
|
1238
|
+
if (typeof node === "string") {
|
|
1239
|
+
if (defaultPages.has(node)) return node;
|
|
1240
|
+
} else if (!node.hidden) {
|
|
1241
|
+
const page = firstPage(node.pages);
|
|
1242
|
+
if (page) return page;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const homepage = config.tabs.filter((tab) => !tab.hidden).map((tab) => firstPage([...tab.pages ?? [], ...tab.groups ?? []])).find(Boolean);
|
|
1247
|
+
if (!homepage || homepage === "introduction") return config;
|
|
1248
|
+
const redirects = [...config.redirects ?? []];
|
|
1249
|
+
const sources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
|
|
1250
|
+
const roots = ["", ...(config.i18n?.locales ?? []).filter((locale) => locale.code !== config.i18n?.defaultLocale).map((locale) => locale.code)];
|
|
1251
|
+
for (const locale of roots) {
|
|
1252
|
+
const source = locale ? `/${locale}` : "/";
|
|
1253
|
+
if (sources.has(source)) continue;
|
|
1254
|
+
redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: false });
|
|
1255
|
+
}
|
|
1256
|
+
return { ...config, redirects };
|
|
1257
|
+
}
|
|
667
1258
|
var LANGUAGE_LABELS = {
|
|
668
1259
|
ar: "Arabic",
|
|
669
1260
|
de: "German",
|
|
@@ -717,16 +1308,16 @@ function jsonPointer(root, pointer) {
|
|
|
717
1308
|
}, root);
|
|
718
1309
|
}
|
|
719
1310
|
function readMintlifyJson(path, repositoryRoot) {
|
|
720
|
-
if (!
|
|
721
|
-
throw new Error(`Mintlify config reference is not a regular file: ${
|
|
1311
|
+
if (!existsSync3(path) || !lstatSync3(path).isFile()) {
|
|
1312
|
+
throw new Error(`Mintlify config reference is not a regular file: ${relative3(repositoryRoot, path)}`);
|
|
722
1313
|
}
|
|
723
1314
|
const realPath = realpathSync(path);
|
|
724
1315
|
const realRoot = realpathSync(repositoryRoot);
|
|
725
|
-
resolveWithin(realRoot,
|
|
726
|
-
if (
|
|
727
|
-
throw new Error(`Mintlify config reference exceeded ${MAX_MINTLIFY_CONFIG_BYTES / 1e6} MB: ${
|
|
1316
|
+
resolveWithin(realRoot, relative3(realRoot, realPath));
|
|
1317
|
+
if (lstatSync3(realPath).size > MAX_MINTLIFY_CONFIG_BYTES) {
|
|
1318
|
+
throw new Error(`Mintlify config reference exceeded ${MAX_MINTLIFY_CONFIG_BYTES / 1e6} MB: ${relative3(repositoryRoot, path)}`);
|
|
728
1319
|
}
|
|
729
|
-
return JSON.parse(
|
|
1320
|
+
return JSON.parse(readFileSync3(realPath, "utf8"));
|
|
730
1321
|
}
|
|
731
1322
|
function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
|
|
732
1323
|
if (Array.isArray(value)) {
|
|
@@ -736,8 +1327,8 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
|
|
|
736
1327
|
if (!object) return value;
|
|
737
1328
|
if (typeof object.$ref === "string") {
|
|
738
1329
|
const [filePart, pointer = ""] = object.$ref.split("#", 2);
|
|
739
|
-
const referencedFile = filePart ? resolveWithin(
|
|
740
|
-
const relativePath =
|
|
1330
|
+
const referencedFile = filePart ? resolveWithin(dirname2(currentFile), filePart) : currentFile;
|
|
1331
|
+
const relativePath = relative3(repositoryRoot, referencedFile);
|
|
741
1332
|
resolveWithin(repositoryRoot, relativePath);
|
|
742
1333
|
const stackKey = `${referencedFile}#${pointer}`;
|
|
743
1334
|
if (stack.has(stackKey)) throw new Error(`Circular Mintlify $ref: ${object.$ref}`);
|
|
@@ -762,7 +1353,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
|
|
|
762
1353
|
);
|
|
763
1354
|
}
|
|
764
1355
|
function readMintlifyConfig(repositoryRoot) {
|
|
765
|
-
const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(
|
|
1356
|
+
const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync3);
|
|
766
1357
|
if (!configPath) return null;
|
|
767
1358
|
const raw = readMintlifyJson(configPath, repositoryRoot);
|
|
768
1359
|
return resolveJsonReferences(raw, configPath, repositoryRoot, /* @__PURE__ */ new Set());
|
|
@@ -794,8 +1385,7 @@ function containerPresentation(value) {
|
|
|
794
1385
|
};
|
|
795
1386
|
}
|
|
796
1387
|
function registerReference(value, context) {
|
|
797
|
-
const
|
|
798
|
-
const localizedValue = localePrefix && value.replace(/^\/+/, "").startsWith(localePrefix) ? value.replace(/^\/+/, "").slice(localePrefix.length) : value;
|
|
1388
|
+
const localizedValue = context.locale ? mintlifyLocalizedReference(value, context.locale, context.defaultPageIds) : value;
|
|
799
1389
|
const navigationId = normalizePageRef(localizedValue, context.pathPrefix);
|
|
800
1390
|
if (!navigationId) return null;
|
|
801
1391
|
const key = `${context.locale ?? ""}:${value}`;
|
|
@@ -1003,6 +1593,8 @@ function projectedCompatibleConfig(config) {
|
|
|
1003
1593
|
const headingFont = projectedFont(objectValue2(config.fonts)?.heading);
|
|
1004
1594
|
const feedback = objectValue2(config.feedback);
|
|
1005
1595
|
const seo = objectValue2(config.seo);
|
|
1596
|
+
const iconLibrary = objectValue2(config.icons)?.library;
|
|
1597
|
+
const projectedIconLibrary = ["lucide", "fontawesome", "tabler"].includes(String(iconLibrary)) ? String(iconLibrary) : void 0;
|
|
1006
1598
|
const navbar = projectedNavbar(config.navbar);
|
|
1007
1599
|
const globalLinks = projectedGlobalNavigationLinks(objectValue2(config.navigation)?.global);
|
|
1008
1600
|
const navbarLinks = [...new Map(
|
|
@@ -1011,6 +1603,7 @@ function projectedCompatibleConfig(config) {
|
|
|
1011
1603
|
const projectedNavigation = navbarLinks.length > 0 || navbar?.primary ? { ...navbarLinks.length > 0 ? { links: navbarLinks } : {}, ...navbar?.primary ? { primary: navbar.primary } : {} } : void 0;
|
|
1012
1604
|
return {
|
|
1013
1605
|
...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
|
|
1606
|
+
...projectedIconLibrary ? { icons: { library: projectedIconLibrary } } : {},
|
|
1014
1607
|
...bannerContent ? {
|
|
1015
1608
|
banner: {
|
|
1016
1609
|
content: bannerContent,
|
|
@@ -1037,15 +1630,25 @@ function projectMintlifyNavigation(config, options = {}) {
|
|
|
1037
1630
|
const seenReferences = /* @__PURE__ */ new Set();
|
|
1038
1631
|
const warningKeys = /* @__PURE__ */ new Set();
|
|
1039
1632
|
const navigation = objectValue2(config.navigation) ?? config;
|
|
1040
|
-
const languages = Array.isArray(navigation.languages) ? navigation.languages.
|
|
1633
|
+
const languages = Array.isArray(navigation.languages) ? navigation.languages.flatMap((value) => {
|
|
1634
|
+
const language = objectValue2(value);
|
|
1635
|
+
if (!language) return [];
|
|
1636
|
+
try {
|
|
1637
|
+
const code = Intl.getCanonicalLocales(String(language.language ?? language.locale ?? "en"))[0];
|
|
1638
|
+
return [{ ...language, language: code }];
|
|
1639
|
+
} catch {
|
|
1640
|
+
warnings.push({ code: "unsupported-config", message: "A navigation language with an invalid locale code was skipped." });
|
|
1641
|
+
return [];
|
|
1642
|
+
}
|
|
1643
|
+
}) : [];
|
|
1041
1644
|
let tabs = [];
|
|
1042
1645
|
let i18n;
|
|
1043
1646
|
const projectionTrace = {};
|
|
1044
1647
|
if (languages.length > 0) {
|
|
1045
1648
|
const defaultLanguage = languages.find((entry) => entry.default === true) ?? languages[0];
|
|
1046
|
-
const defaultLocale = String(defaultLanguage.language
|
|
1649
|
+
const defaultLocale = String(defaultLanguage.language);
|
|
1047
1650
|
const locales = languages.map((entry) => {
|
|
1048
|
-
const code = String(entry.language
|
|
1651
|
+
const code = String(entry.language);
|
|
1049
1652
|
let label = LANGUAGE_LABELS[code];
|
|
1050
1653
|
try {
|
|
1051
1654
|
label ??= new Intl.DisplayNames(["en"], { type: "language" }).of(code) ?? void 0;
|
|
@@ -1054,10 +1657,12 @@ function projectMintlifyNavigation(config, options = {}) {
|
|
|
1054
1657
|
return { code, label: label ?? code.toUpperCase() };
|
|
1055
1658
|
});
|
|
1056
1659
|
i18n = { defaultLocale, locales };
|
|
1057
|
-
|
|
1058
|
-
|
|
1660
|
+
const defaultPageIds = /* @__PURE__ */ new Set();
|
|
1661
|
+
for (const language of [defaultLanguage, ...languages.filter((entry) => entry !== defaultLanguage)]) {
|
|
1662
|
+
const locale = String(language.language);
|
|
1059
1663
|
const context = {
|
|
1060
1664
|
locale,
|
|
1665
|
+
defaultPageIds,
|
|
1061
1666
|
pathPrefix: options.pathPrefix,
|
|
1062
1667
|
references,
|
|
1063
1668
|
seenReferences,
|
|
@@ -1070,7 +1675,10 @@ function projectMintlifyNavigation(config, options = {}) {
|
|
|
1070
1675
|
"Documentation",
|
|
1071
1676
|
language === defaultLanguage ? projectionTrace : void 0
|
|
1072
1677
|
);
|
|
1073
|
-
if (language === defaultLanguage)
|
|
1678
|
+
if (language === defaultLanguage) {
|
|
1679
|
+
tabs = languageTabs;
|
|
1680
|
+
for (const reference of references) defaultPageIds.add(reference.navigationId);
|
|
1681
|
+
}
|
|
1074
1682
|
}
|
|
1075
1683
|
} else {
|
|
1076
1684
|
const context = { references, seenReferences, warnings, warningKeys, pathPrefix: options.pathPrefix };
|
|
@@ -1133,8 +1741,8 @@ function topLevelLabel(segment) {
|
|
|
1133
1741
|
function groupsWithinSection(section, pageIds, sectionLabel = topLevelLabel(section)) {
|
|
1134
1742
|
const groups = /* @__PURE__ */ new Map();
|
|
1135
1743
|
for (const id of pageIds) {
|
|
1136
|
-
const
|
|
1137
|
-
const nestedSegment =
|
|
1744
|
+
const relative5 = id === "introduction" ? "" : id.startsWith(`${section}/`) ? id.slice(section.length + 1) : id;
|
|
1745
|
+
const nestedSegment = relative5.includes("/") ? relative5.split("/", 1)[0] : "overview";
|
|
1138
1746
|
const group = groups.get(nestedSegment) ?? [];
|
|
1139
1747
|
group.push(id);
|
|
1140
1748
|
groups.set(nestedSegment, group);
|
|
@@ -1218,7 +1826,7 @@ function buildNavigationFromPages(pages, options = {}) {
|
|
|
1218
1826
|
return { tabs: [{ tab: "Documentation", groups }] };
|
|
1219
1827
|
}
|
|
1220
1828
|
function isDocumentationExtension(filename) {
|
|
1221
|
-
return [".md", ".mdx", ".rst", ".txt"].includes(
|
|
1829
|
+
return [".md", ".mdx", ".rst", ".txt"].includes(extname3(filename).toLowerCase());
|
|
1222
1830
|
}
|
|
1223
1831
|
|
|
1224
1832
|
// src/repository.ts
|
|
@@ -1280,16 +1888,16 @@ var DOCUSAURUS_CONFIG_FILENAMES = [
|
|
|
1280
1888
|
function hasDocusaurusConfig(directory) {
|
|
1281
1889
|
return DOCUSAURUS_CONFIG_FILENAMES.some((filename) => {
|
|
1282
1890
|
const path = resolveWithin(directory, filename);
|
|
1283
|
-
return
|
|
1891
|
+
return existsSync4(path) && lstatSync4(path).isFile();
|
|
1284
1892
|
});
|
|
1285
1893
|
}
|
|
1286
1894
|
function hasMintlifyConfig(directory) {
|
|
1287
1895
|
return ["docs.json", "mint.json"].some((filename) => {
|
|
1288
1896
|
const path = resolveWithin(directory, filename);
|
|
1289
|
-
if (!
|
|
1897
|
+
if (!existsSync4(path) || !lstatSync4(path).isFile()) return false;
|
|
1290
1898
|
if (filename === "mint.json") return true;
|
|
1291
1899
|
try {
|
|
1292
|
-
const config = JSON.parse(
|
|
1900
|
+
const config = JSON.parse(readFileSync4(path, "utf8"));
|
|
1293
1901
|
return String(config.$schema ?? "").includes("mintlify") || "navigation" in config;
|
|
1294
1902
|
} catch {
|
|
1295
1903
|
return false;
|
|
@@ -1303,7 +1911,7 @@ function findMintlifyProjectRoot(repositoryDir, docsDir) {
|
|
|
1303
1911
|
const path = resolveWithin(repositoryDir, candidate || ".");
|
|
1304
1912
|
if (hasMintlifyConfig(path)) return path;
|
|
1305
1913
|
if (!candidate) break;
|
|
1306
|
-
const parent =
|
|
1914
|
+
const parent = dirname3(candidate);
|
|
1307
1915
|
candidate = parent === "." ? "" : parent;
|
|
1308
1916
|
}
|
|
1309
1917
|
}
|
|
@@ -1328,7 +1936,7 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
|
|
|
1328
1936
|
const path = resolveWithin(repositoryDir, candidate || ".");
|
|
1329
1937
|
if (hasDocusaurusConfig(path)) return path;
|
|
1330
1938
|
if (!candidate) break;
|
|
1331
|
-
const parent =
|
|
1939
|
+
const parent = dirname3(candidate);
|
|
1332
1940
|
candidate = parent === "." ? "" : parent;
|
|
1333
1941
|
}
|
|
1334
1942
|
}
|
|
@@ -1350,9 +1958,9 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
|
|
|
1350
1958
|
return null;
|
|
1351
1959
|
}
|
|
1352
1960
|
function readDocusaurusConfigSource(projectRoot) {
|
|
1353
|
-
const path = DOCUSAURUS_CONFIG_FILENAMES.map((filename) => resolveWithin(projectRoot, filename)).find((candidate) =>
|
|
1354
|
-
if (!path ||
|
|
1355
|
-
return
|
|
1961
|
+
const path = DOCUSAURUS_CONFIG_FILENAMES.map((filename) => resolveWithin(projectRoot, filename)).find((candidate) => existsSync4(candidate) && lstatSync4(candidate).isFile());
|
|
1962
|
+
if (!path || lstatSync4(path).size > 1e6) return "";
|
|
1963
|
+
return readFileSync4(path, "utf8");
|
|
1356
1964
|
}
|
|
1357
1965
|
function primaryDocusaurusDocsDirectory(projectRoot) {
|
|
1358
1966
|
const source = readDocusaurusConfigSource(projectRoot);
|
|
@@ -1361,7 +1969,7 @@ function primaryDocusaurusDocsDirectory(projectRoot) {
|
|
|
1361
1969
|
}
|
|
1362
1970
|
function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
|
|
1363
1971
|
const source = readDocusaurusConfigSource(projectRoot);
|
|
1364
|
-
const projectRelative =
|
|
1972
|
+
const projectRelative = relative4(repositoryDir, projectRoot).replace(/\\/g, "/");
|
|
1365
1973
|
const plugins = [];
|
|
1366
1974
|
const matcher = /['"]@docusaurus\/plugin-content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
|
|
1367
1975
|
for (const match of source.matchAll(matcher)) {
|
|
@@ -1370,7 +1978,7 @@ function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
|
|
|
1370
1978
|
if (!localPath || !routePrefix) continue;
|
|
1371
1979
|
const docsDir = [projectRelative, localPath].filter(Boolean).join("/");
|
|
1372
1980
|
const absolute = resolveWithin(repositoryDir, docsDir);
|
|
1373
|
-
if (!
|
|
1981
|
+
if (!existsSync4(absolute) || !lstatSync4(absolute).isDirectory()) continue;
|
|
1374
1982
|
plugins.push({ docsDir, routePrefix });
|
|
1375
1983
|
}
|
|
1376
1984
|
return plugins;
|
|
@@ -1413,7 +2021,7 @@ async function cloneGitHubRepository(source, targetDir) {
|
|
|
1413
2021
|
const args = ["clone", "--depth", "1", "--single-branch"];
|
|
1414
2022
|
if (source.branch !== "HEAD") args.push("--branch", source.branch);
|
|
1415
2023
|
args.push("--", source.cloneUrl, targetDir);
|
|
1416
|
-
await new Promise((
|
|
2024
|
+
await new Promise((resolve3, reject) => {
|
|
1417
2025
|
const child = spawn("git", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1418
2026
|
let stderr = "";
|
|
1419
2027
|
child.stderr.setEncoding("utf8");
|
|
@@ -1422,7 +2030,7 @@ async function cloneGitHubRepository(source, targetDir) {
|
|
|
1422
2030
|
});
|
|
1423
2031
|
child.on("error", reject);
|
|
1424
2032
|
child.on("close", (code) => {
|
|
1425
|
-
if (code === 0)
|
|
2033
|
+
if (code === 0) resolve3();
|
|
1426
2034
|
else reject(new Error(`Failed to clone ${source.owner}/${source.repo}: ${stderr.trim() || `git exited ${code}`}`));
|
|
1427
2035
|
});
|
|
1428
2036
|
});
|
|
@@ -1431,19 +2039,19 @@ function detectRepositoryPlatform(repositoryDir, docsDir) {
|
|
|
1431
2039
|
if (findMintlifyProjectRoot(repositoryDir, docsDir)) return "mintlify";
|
|
1432
2040
|
const selectedRoot = docsDir === void 0 ? repositoryDir : resolveWithin(repositoryDir, docsDir || ".");
|
|
1433
2041
|
const docsJson = resolveWithin(selectedRoot, "docs.json");
|
|
1434
|
-
if (
|
|
2042
|
+
if (existsSync4(docsJson)) {
|
|
1435
2043
|
try {
|
|
1436
|
-
const config = JSON.parse(
|
|
2044
|
+
const config = JSON.parse(readFileSync4(docsJson, "utf8"));
|
|
1437
2045
|
if (Array.isArray(config.tabs)) return "thally";
|
|
1438
2046
|
} catch {
|
|
1439
2047
|
}
|
|
1440
2048
|
}
|
|
1441
|
-
if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) =>
|
|
2049
|
+
if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) => existsSync4(resolveWithin(repositoryDir, name)))) return "docusaurus";
|
|
1442
2050
|
if (findDocusaurusProjectRoot(repositoryDir)) return "docusaurus";
|
|
1443
|
-
if (
|
|
1444
|
-
if (
|
|
1445
|
-
if (["astro.config.mjs", "astro.config.ts"].some((name) =>
|
|
1446
|
-
if (["pages/_meta.json", "_meta.json"].some((name) =>
|
|
2051
|
+
if (existsSync4(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
|
|
2052
|
+
if (existsSync4(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
|
|
2053
|
+
if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync4(resolveWithin(repositoryDir, name)))) return "starlight";
|
|
2054
|
+
if (["pages/_meta.json", "_meta.json"].some((name) => existsSync4(resolveWithin(repositoryDir, name)))) return "nextra";
|
|
1447
2055
|
return "unknown";
|
|
1448
2056
|
}
|
|
1449
2057
|
function containsMarkdown(directory, depth = 0) {
|
|
@@ -1451,7 +2059,7 @@ function containsMarkdown(directory, depth = 0) {
|
|
|
1451
2059
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
1452
2060
|
if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
|
|
1453
2061
|
const path = resolveWithin(directory, entry.name);
|
|
1454
|
-
if (entry.isFile() && [".md", ".mdx"].includes(
|
|
2062
|
+
if (entry.isFile() && [".md", ".mdx"].includes(extname4(entry.name).toLowerCase())) return true;
|
|
1455
2063
|
if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
|
|
1456
2064
|
}
|
|
1457
2065
|
return false;
|
|
@@ -1459,7 +2067,7 @@ function containsMarkdown(directory, depth = 0) {
|
|
|
1459
2067
|
function detectRepositoryDocsDir(repositoryDir) {
|
|
1460
2068
|
for (const candidate of ["docs", "documentation", "content", "pages", "src/content", "src/pages", "guide", "guides", ""]) {
|
|
1461
2069
|
const path = resolveWithin(repositoryDir, candidate);
|
|
1462
|
-
if (
|
|
2070
|
+
if (existsSync4(path) && lstatSync4(path).isDirectory() && containsMarkdown(path)) return candidate;
|
|
1463
2071
|
}
|
|
1464
2072
|
return "";
|
|
1465
2073
|
}
|
|
@@ -1472,7 +2080,7 @@ function scanFiles(root) {
|
|
|
1472
2080
|
if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
|
|
1473
2081
|
const path = resolveWithin(directory, entry.name);
|
|
1474
2082
|
if (entry.isDirectory()) visit(path);
|
|
1475
|
-
else if (entry.isFile()) files.push({ absolutePath: path, relativePath:
|
|
2083
|
+
else if (entry.isFile()) files.push({ absolutePath: path, relativePath: relative4(root, path).replace(/\\/g, "/") });
|
|
1476
2084
|
}
|
|
1477
2085
|
}
|
|
1478
2086
|
visit(root);
|
|
@@ -1527,9 +2135,9 @@ function interpolateSnippet(snippet, attributes) {
|
|
|
1527
2135
|
}).join("\n");
|
|
1528
2136
|
}
|
|
1529
2137
|
function resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot) {
|
|
1530
|
-
const repositoryRelative = sourcePath.startsWith("@site/") ?
|
|
2138
|
+
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
2139
|
const candidate = resolveWithin(repositoryRoot, repositoryRelative);
|
|
1532
|
-
resolveWithin(repositoryRoot,
|
|
2140
|
+
resolveWithin(repositoryRoot, relative4(repositoryRoot, candidate));
|
|
1533
2141
|
return candidate;
|
|
1534
2142
|
}
|
|
1535
2143
|
function globalSnippetAliases(files, repositoryRoot, siteRoot) {
|
|
@@ -1543,12 +2151,12 @@ function globalSnippetAliases(files, repositoryRoot, siteRoot) {
|
|
|
1543
2151
|
if (conventionalAlias) aliases.set(conventionalAlias, file.absolutePath);
|
|
1544
2152
|
}
|
|
1545
2153
|
for (const file of files) {
|
|
1546
|
-
if (![".md", ".mdx"].includes(
|
|
1547
|
-
const raw =
|
|
2154
|
+
if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) continue;
|
|
2155
|
+
const raw = readFileSync4(file.absolutePath, "utf8");
|
|
1548
2156
|
for (const match of raw.matchAll(matcher)) {
|
|
1549
2157
|
try {
|
|
1550
2158
|
const candidate = resolveSnippetPath(match[2], file.absolutePath, repositoryRoot, siteRoot);
|
|
1551
|
-
if (
|
|
2159
|
+
if (existsSync4(candidate) && lstatSync4(candidate).isFile()) {
|
|
1552
2160
|
aliases.set(match[1], candidate);
|
|
1553
2161
|
}
|
|
1554
2162
|
} catch {
|
|
@@ -1570,12 +2178,12 @@ function repositoryAssetHref(value, currentFile, siteRoot) {
|
|
|
1570
2178
|
} catch {
|
|
1571
2179
|
return null;
|
|
1572
2180
|
}
|
|
1573
|
-
if (!ASSET_EXTENSIONS.has(
|
|
2181
|
+
if (!ASSET_EXTENSIONS.has(extname4(decodedPath).toLowerCase())) return null;
|
|
1574
2182
|
try {
|
|
1575
|
-
const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(
|
|
1576
|
-
const siteRelative =
|
|
2183
|
+
const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname3(currentFile), decodedPath);
|
|
2184
|
+
const siteRelative = relative4(siteRoot, candidate).replace(/\\/g, "/");
|
|
1577
2185
|
resolveWithin(siteRoot, siteRelative);
|
|
1578
|
-
if (!
|
|
2186
|
+
if (!existsSync4(candidate) || !lstatSync4(candidate).isFile()) return null;
|
|
1579
2187
|
const normalized = normalizeAssetPath(siteRelative);
|
|
1580
2188
|
if (!normalized) return null;
|
|
1581
2189
|
const rewritten = `/${normalized}${suffix}`;
|
|
@@ -1601,9 +2209,9 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
|
|
|
1601
2209
|
(_statement, componentName, sourcePath) => {
|
|
1602
2210
|
try {
|
|
1603
2211
|
const candidate = resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot);
|
|
1604
|
-
if (!
|
|
2212
|
+
if (!existsSync4(candidate) || !lstatSync4(candidate).isFile()) throw new Error("file not found");
|
|
1605
2213
|
const nested = inlineMdxSnippets(
|
|
1606
|
-
withoutFrontmatter(
|
|
2214
|
+
withoutFrontmatter(readFileSync4(candidate, "utf8")),
|
|
1607
2215
|
candidate,
|
|
1608
2216
|
repositoryRoot,
|
|
1609
2217
|
warnings,
|
|
@@ -1617,7 +2225,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
|
|
|
1617
2225
|
warnings.push({
|
|
1618
2226
|
code: "missing-page",
|
|
1619
2227
|
message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
|
|
1620
|
-
source:
|
|
2228
|
+
source: relative4(repositoryRoot, currentFile).replace(/\\/g, "/")
|
|
1621
2229
|
});
|
|
1622
2230
|
snippets.set(componentName, `{/* Missing imported snippet: ${sourcePath} */}`);
|
|
1623
2231
|
return "";
|
|
@@ -1627,7 +2235,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
|
|
|
1627
2235
|
for (const [componentName, candidate] of globalAliases) {
|
|
1628
2236
|
if (snippets.has(componentName) || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
|
|
1629
2237
|
const nested = inlineMdxSnippets(
|
|
1630
|
-
withoutFrontmatter(
|
|
2238
|
+
withoutFrontmatter(readFileSync4(candidate, "utf8")),
|
|
1631
2239
|
candidate,
|
|
1632
2240
|
repositoryRoot,
|
|
1633
2241
|
warnings,
|
|
@@ -1654,6 +2262,11 @@ function injectOpenApi(config, filename) {
|
|
|
1654
2262
|
else tabs.push({ tab: "API Reference", api: { source: `/${filename}` } });
|
|
1655
2263
|
return { ...config, tabs };
|
|
1656
2264
|
}
|
|
2265
|
+
function componentSourceIdentity(sourceUrl, repositoryDir, siteRoot) {
|
|
2266
|
+
const url = new URL(sourceUrl);
|
|
2267
|
+
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)}`;
|
|
2268
|
+
return JSON.stringify([repository, relative4(repositoryDir, siteRoot).replace(/\\/g, "/")]);
|
|
2269
|
+
}
|
|
1657
2270
|
function migrateRepository(options) {
|
|
1658
2271
|
const repositoryDir = options.repositoryDir;
|
|
1659
2272
|
const platform = options.platform ?? detectRepositoryPlatform(repositoryDir, options.docsDir);
|
|
@@ -1663,21 +2276,23 @@ function migrateRepository(options) {
|
|
|
1663
2276
|
if (options.docsDir !== void 0) {
|
|
1664
2277
|
const selected = resolveWithin(repositoryDir, options.docsDir || ".");
|
|
1665
2278
|
if (platform === "docusaurus" && hasDocusaurusConfig(selected)) {
|
|
1666
|
-
const projectRelative =
|
|
2279
|
+
const projectRelative = relative4(repositoryDir, selected).replace(/\\/g, "/");
|
|
1667
2280
|
return [projectRelative, primaryDocusaurusDocsDirectory(selected)].filter(Boolean).join("/");
|
|
1668
2281
|
}
|
|
1669
2282
|
return options.docsDir;
|
|
1670
2283
|
}
|
|
1671
2284
|
if (docusaurusProjectRoot) {
|
|
1672
|
-
const projectRelative =
|
|
2285
|
+
const projectRelative = relative4(repositoryDir, docusaurusProjectRoot).replace(/\\/g, "/");
|
|
1673
2286
|
return [projectRelative, primaryDocusaurusDocsDirectory(docusaurusProjectRoot)].filter(Boolean).join("/");
|
|
1674
2287
|
}
|
|
1675
2288
|
if (platform === "mintlify" && mintlifyProjectRoot) {
|
|
1676
|
-
return
|
|
2289
|
+
return relative4(repositoryDir, mintlifyProjectRoot).replace(/\\/g, "/");
|
|
1677
2290
|
}
|
|
1678
2291
|
return platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir);
|
|
1679
2292
|
})();
|
|
1680
2293
|
const warnings = [];
|
|
2294
|
+
const componentRoot = mintlifyProjectRoot ?? repositoryDir;
|
|
2295
|
+
const componentMigrator = platform === "mintlify" ? createComponentMigrator(componentRoot, warnings, componentSourceIdentity(options.sourceUrl, repositoryDir, componentRoot)) : void 0;
|
|
1681
2296
|
let docsConfig = { tabs: [] };
|
|
1682
2297
|
const referenceMap = /* @__PURE__ */ new Map();
|
|
1683
2298
|
const exactReferenceMap = /* @__PURE__ */ new Map();
|
|
@@ -1694,9 +2309,10 @@ function migrateRepository(options) {
|
|
|
1694
2309
|
warnings.push(...projected.warnings);
|
|
1695
2310
|
for (const [index, reference] of projected.pageReferences.entries()) {
|
|
1696
2311
|
const key = normalizedReferenceKey(reference.ref);
|
|
1697
|
-
referenceMap.set(key, reference);
|
|
1698
|
-
|
|
1699
|
-
|
|
2312
|
+
if (!referenceMap.has(key)) referenceMap.set(key, reference);
|
|
2313
|
+
const exactKey = exactReferenceKey(reference.ref);
|
|
2314
|
+
if (!exactReferenceMap.has(exactKey)) exactReferenceMap.set(exactKey, reference);
|
|
2315
|
+
if (!referenceOrder.has(key)) referenceOrder.set(key, index);
|
|
1700
2316
|
}
|
|
1701
2317
|
}
|
|
1702
2318
|
} catch (error) {
|
|
@@ -1717,7 +2333,7 @@ function migrateRepository(options) {
|
|
|
1717
2333
|
}
|
|
1718
2334
|
}
|
|
1719
2335
|
const contentRoot = resolveWithin(repositoryDir, configuredDocsDir);
|
|
1720
|
-
if (!
|
|
2336
|
+
if (!existsSync4(contentRoot) || !lstatSync4(contentRoot).isDirectory()) {
|
|
1721
2337
|
throw new Error(`Documentation directory does not exist: ${configuredDocsDir || "."}`);
|
|
1722
2338
|
}
|
|
1723
2339
|
const files = scanFiles(contentRoot);
|
|
@@ -1728,6 +2344,8 @@ function migrateRepository(options) {
|
|
|
1728
2344
|
let skipped = 0;
|
|
1729
2345
|
let discovered = files.length;
|
|
1730
2346
|
const localeConfig = docsConfig.i18n;
|
|
2347
|
+
const defaultPageIds = new Set([...referenceMap.values()].filter((reference) => !reference.locale || reference.locale === localeConfig?.defaultLocale).map((reference) => reference.navigationId));
|
|
2348
|
+
const routeAliases = [];
|
|
1731
2349
|
const pageFiles = [...files].sort((left, right) => {
|
|
1732
2350
|
const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
|
|
1733
2351
|
const rightOrder = referenceOrder.get(normalizedReferenceKey(right.relativePath)) ?? Number.MAX_SAFE_INTEGER;
|
|
@@ -1746,12 +2364,12 @@ function migrateRepository(options) {
|
|
|
1746
2364
|
skipped++;
|
|
1747
2365
|
continue;
|
|
1748
2366
|
}
|
|
1749
|
-
if (![".md", ".mdx"].includes(
|
|
2367
|
+
if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) {
|
|
1750
2368
|
skipped++;
|
|
1751
2369
|
warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
|
|
1752
2370
|
continue;
|
|
1753
2371
|
}
|
|
1754
|
-
const size =
|
|
2372
|
+
const size = lstatSync4(file.absolutePath).size;
|
|
1755
2373
|
if (size > MAX_PAGE_BYTES) {
|
|
1756
2374
|
skipped++;
|
|
1757
2375
|
warnings.push({ code: "skipped-file", message: "Page exceeded the 2 MB repository import limit.", source: file.relativePath });
|
|
@@ -1762,10 +2380,12 @@ function migrateRepository(options) {
|
|
|
1762
2380
|
let locale = referenced?.locale;
|
|
1763
2381
|
let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath, platform === "mintlify");
|
|
1764
2382
|
if (!referenced && localeConfig) {
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2383
|
+
for (const entry of localeConfig.locales) {
|
|
2384
|
+
const localized = mintlifyLocalizedReference(file.relativePath, entry.code, defaultPageIds);
|
|
2385
|
+
if (localized === file.relativePath) continue;
|
|
2386
|
+
locale = entry.code;
|
|
2387
|
+
navigationId = pageIdFromReference(localized, platform === "mintlify");
|
|
2388
|
+
break;
|
|
1769
2389
|
}
|
|
1770
2390
|
}
|
|
1771
2391
|
if (!navigationId) {
|
|
@@ -1775,8 +2395,8 @@ function migrateRepository(options) {
|
|
|
1775
2395
|
}
|
|
1776
2396
|
const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
|
|
1777
2397
|
const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
|
|
1778
|
-
|
|
1779
|
-
|
|
2398
|
+
let raw = inlineMdxSnippets(
|
|
2399
|
+
readFileSync4(file.absolutePath, "utf8"),
|
|
1780
2400
|
file.absolutePath,
|
|
1781
2401
|
repositoryDir,
|
|
1782
2402
|
warnings,
|
|
@@ -1784,13 +2404,14 @@ function migrateRepository(options) {
|
|
|
1784
2404
|
mintlifyProjectRoot ?? docusaurusProjectRoot ?? repositoryDir,
|
|
1785
2405
|
snippetAliases
|
|
1786
2406
|
);
|
|
2407
|
+
if (componentMigrator) raw = componentMigrator.transform(raw, file.absolutePath);
|
|
1787
2408
|
let docusaurusDescriptor;
|
|
1788
2409
|
const page = parseMarkdownPage({
|
|
1789
2410
|
id,
|
|
1790
2411
|
navigationId,
|
|
1791
2412
|
...locale ? { locale } : {},
|
|
1792
2413
|
raw,
|
|
1793
|
-
source: `${options.sourceUrl}#${
|
|
2414
|
+
source: `${options.sourceUrl}#${relative4(repositoryDir, file.absolutePath).replace(/\\/g, "/")}`,
|
|
1794
2415
|
...platform === "docusaurus" ? {
|
|
1795
2416
|
resolveIdentity: (frontmatter, fallback) => {
|
|
1796
2417
|
const resolved = resolveDocusaurusPageIdentity(file.relativePath, frontmatter, fallback);
|
|
@@ -1827,6 +2448,12 @@ function migrateRepository(options) {
|
|
|
1827
2448
|
}
|
|
1828
2449
|
seenPageIds.add(page.id);
|
|
1829
2450
|
pages.push(page);
|
|
2451
|
+
if (platform === "mintlify") {
|
|
2452
|
+
const sourcePath = exactReferenceKey(file.relativePath);
|
|
2453
|
+
if (sourcePath !== page.id && /^[A-Za-z0-9_./-]+$/.test(sourcePath)) {
|
|
2454
|
+
routeAliases.push({ source: `/${sourcePath}`, destination: `/${page.id}`, permanent: false });
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
1830
2457
|
if (docusaurusDescriptor) {
|
|
1831
2458
|
docusaurusDescriptors.push({ ...docusaurusDescriptor, title: page.title });
|
|
1832
2459
|
}
|
|
@@ -1840,7 +2467,7 @@ function migrateRepository(options) {
|
|
|
1840
2467
|
const docusaurusAssetRoot = docusaurusProjectRoot ?? repositoryDir;
|
|
1841
2468
|
const repositoryAssets = platform === "docusaurus" && configuredDocsDir && !options.docusaurusSkipAssets ? ["static", "public"].flatMap((directory) => {
|
|
1842
2469
|
const root = resolveWithin(docusaurusAssetRoot, directory);
|
|
1843
|
-
if (!
|
|
2470
|
+
if (!existsSync4(root) || !lstatSync4(root).isDirectory()) return [];
|
|
1844
2471
|
return scanFiles(root).map((file) => ({
|
|
1845
2472
|
...file,
|
|
1846
2473
|
relativePath: `${directory}/${file.relativePath}`
|
|
@@ -1849,9 +2476,9 @@ function migrateRepository(options) {
|
|
|
1849
2476
|
let totalAssetBytes = 0;
|
|
1850
2477
|
for (const file of [...files, ...repositoryAssets]) {
|
|
1851
2478
|
const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
|
|
1852
|
-
if (!ASSET_EXTENSIONS.has(
|
|
2479
|
+
if (!ASSET_EXTENSIONS.has(extname4(file.relativePath).toLowerCase())) continue;
|
|
1853
2480
|
if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment)) continue;
|
|
1854
|
-
const size =
|
|
2481
|
+
const size = lstatSync4(file.absolutePath).size;
|
|
1855
2482
|
if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
|
|
1856
2483
|
warnings.push({ code: "limit-reached", message: "An asset was skipped because the migration asset budget was exhausted.", source: file.relativePath });
|
|
1857
2484
|
continue;
|
|
@@ -1859,7 +2486,7 @@ function migrateRepository(options) {
|
|
|
1859
2486
|
const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
|
|
1860
2487
|
const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
|
|
1861
2488
|
if (!assetPath) continue;
|
|
1862
|
-
assets.push({ path: assetPath, content:
|
|
2489
|
+
assets.push({ path: assetPath, content: readFileSync4(file.absolutePath) });
|
|
1863
2490
|
totalAssetBytes += size;
|
|
1864
2491
|
}
|
|
1865
2492
|
if (files.length >= MAX_SOURCE_FILES) {
|
|
@@ -1893,11 +2520,16 @@ function migrateRepository(options) {
|
|
|
1893
2520
|
if (openApi) {
|
|
1894
2521
|
const filename = basename(openApi.relativePath);
|
|
1895
2522
|
if (!assets.some((asset) => asset.path === filename)) {
|
|
1896
|
-
assets.push({ path: filename, content:
|
|
2523
|
+
assets.push({ path: filename, content: readFileSync4(openApi.absolutePath) });
|
|
1897
2524
|
}
|
|
1898
2525
|
docsConfig = injectOpenApi(docsConfig, filename);
|
|
1899
2526
|
}
|
|
1900
|
-
if (platform === "mintlify")
|
|
2527
|
+
if (platform === "mintlify") {
|
|
2528
|
+
const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source));
|
|
2529
|
+
const aliases = routeAliases.filter((redirect) => !sources.has(redirect.source));
|
|
2530
|
+
if (aliases.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...aliases] };
|
|
2531
|
+
docsConfig = addMintlifyHomepageRedirects(addMintlifyDirectoryRedirects(docsConfig, pages), pages);
|
|
2532
|
+
}
|
|
1901
2533
|
if (platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipPlugins) {
|
|
1902
2534
|
for (const plugin of additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot)) {
|
|
1903
2535
|
if (plugin.docsDir === configuredDocsDir) continue;
|
|
@@ -1938,6 +2570,7 @@ function migrateRepository(options) {
|
|
|
1938
2570
|
platform,
|
|
1939
2571
|
pages,
|
|
1940
2572
|
assets,
|
|
2573
|
+
...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
|
|
1941
2574
|
docsConfig,
|
|
1942
2575
|
...mintlifyConfig ? {
|
|
1943
2576
|
site: {
|
|
@@ -2054,6 +2687,7 @@ function renderMigrationFiles(bundle, options = {}) {
|
|
|
2054
2687
|
content: renderPage(bundle, page)
|
|
2055
2688
|
})),
|
|
2056
2689
|
...bundle.assets.map((asset) => ({ path: `public/${asset.path}`, content: asset.content })),
|
|
2690
|
+
...(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
2691
|
{ path: "docs.json", content: `${JSON.stringify(config, null, 2)}
|
|
2058
2692
|
` }
|
|
2059
2693
|
];
|
|
@@ -2061,7 +2695,10 @@ function renderMigrationFiles(bundle, options = {}) {
|
|
|
2061
2695
|
|
|
2062
2696
|
// src/url.ts
|
|
2063
2697
|
import { load } from "cheerio";
|
|
2698
|
+
import remarkMdx3 from "remark-mdx";
|
|
2699
|
+
import remarkParse3 from "remark-parse";
|
|
2064
2700
|
import TurndownService from "turndown";
|
|
2701
|
+
import { unified as unified3 } from "unified";
|
|
2065
2702
|
import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
|
|
2066
2703
|
var DEFAULT_MAX_PAGES = 1e3;
|
|
2067
2704
|
var DEFAULT_MAX_TOTAL_BYTES = 1e8;
|
|
@@ -2164,6 +2801,8 @@ var LOCALE_CODES = /* @__PURE__ */ new Set([
|
|
|
2164
2801
|
"zh-hant"
|
|
2165
2802
|
]);
|
|
2166
2803
|
var PORTABLE_URL_PROPS = "href|src|img|primaryHref|secondaryHref";
|
|
2804
|
+
var PORTABLE_URL_PROP_NAMES = new Set(PORTABLE_URL_PROPS.split("|"));
|
|
2805
|
+
var remoteMarkdownParser = unified3().use(remarkParse3).use(remarkMdx3);
|
|
2167
2806
|
function validateMigrationUrl(value) {
|
|
2168
2807
|
let url;
|
|
2169
2808
|
try {
|
|
@@ -2293,7 +2932,7 @@ function detectUrlPlatform(document) {
|
|
|
2293
2932
|
const value = `${document.body.slice(0, 2e5)} ${headers}`.toLowerCase();
|
|
2294
2933
|
if (value.includes("__mintlify") || value.includes("/mintlify-assets/") || value.includes("x-mintlify-") || value.includes("/_mintlify/")) return "mintlify";
|
|
2295
2934
|
if (value.includes("docusaurus") || value.includes("__docusaurus")) return "docusaurus";
|
|
2296
|
-
if (value.includes("gitbook")
|
|
2935
|
+
if (value.includes("gitbook")) return "gitbook";
|
|
2297
2936
|
if (value.includes("nextra")) return "nextra";
|
|
2298
2937
|
if (value.includes("vitepress")) return "vitepress";
|
|
2299
2938
|
if (value.includes("starlight")) return "starlight";
|
|
@@ -2432,19 +3071,184 @@ function stripEventHandlerProps(line) {
|
|
|
2432
3071
|
}
|
|
2433
3072
|
return cursor === 0 ? line : `${result}${line.slice(cursor)}`;
|
|
2434
3073
|
}
|
|
2435
|
-
function
|
|
2436
|
-
|
|
2437
|
-
|
|
3074
|
+
function isSafePortableUrl(value) {
|
|
3075
|
+
try {
|
|
3076
|
+
const parsed = new URL(value, "https://relative.thally.invalid");
|
|
3077
|
+
return ["http:", "https:", "mailto:", "tel:"].includes(parsed.protocol);
|
|
3078
|
+
} catch {
|
|
3079
|
+
return false;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
function decodeHtmlAttribute(value) {
|
|
3083
|
+
const document = load(`<i data-thally-value="${value.replace(/"/g, """)}"></i>`);
|
|
3084
|
+
return document("i").attr("data-thally-value") ?? "";
|
|
3085
|
+
}
|
|
3086
|
+
function escapeMarkdownText(value, escapeTableSeparator = false) {
|
|
3087
|
+
let escaped = "";
|
|
3088
|
+
for (const character of value) {
|
|
3089
|
+
if (character === "\\") escaped += "\\\\";
|
|
3090
|
+
else if (character === "[" || character === "]") escaped += `\\${character}`;
|
|
3091
|
+
else if (character === "<") escaped += "<";
|
|
3092
|
+
else if (character === ">") escaped += ">";
|
|
3093
|
+
else if (character === "{") escaped += "{";
|
|
3094
|
+
else if (character === "}") escaped += "}";
|
|
3095
|
+
else if (character === "|" && escapeTableSeparator) escaped += "\\|";
|
|
3096
|
+
else escaped += character;
|
|
3097
|
+
}
|
|
3098
|
+
return escaped;
|
|
3099
|
+
}
|
|
3100
|
+
function markdownLinkDestination(value) {
|
|
3101
|
+
try {
|
|
3102
|
+
return encodeURI(value).replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/&/g, "&");
|
|
3103
|
+
} catch {
|
|
3104
|
+
return null;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
function htmlOpeningTagEnd(value, start) {
|
|
3108
|
+
let quote = "";
|
|
3109
|
+
for (let index = start; index < value.length; index += 1) {
|
|
3110
|
+
const character = value[index];
|
|
3111
|
+
if (quote) {
|
|
3112
|
+
if (character === quote) quote = "";
|
|
3113
|
+
continue;
|
|
3114
|
+
}
|
|
3115
|
+
if (character === '"' || character === "'") {
|
|
3116
|
+
quote = character;
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
3119
|
+
if (character === ">") return index + 1;
|
|
3120
|
+
}
|
|
3121
|
+
return null;
|
|
3122
|
+
}
|
|
3123
|
+
function replacePortableHtmlImages(value) {
|
|
3124
|
+
const matcher = /<img\b/gi;
|
|
3125
|
+
let cursor = 0;
|
|
3126
|
+
let output = "";
|
|
3127
|
+
for (const match of value.matchAll(matcher)) {
|
|
3128
|
+
const start = match.index;
|
|
3129
|
+
if (start < cursor) continue;
|
|
3130
|
+
const end = htmlOpeningTagEnd(value, start + match[0].length);
|
|
3131
|
+
if (end === null) continue;
|
|
3132
|
+
const fragment = load(value.slice(start, end));
|
|
3133
|
+
const image = fragment("img").first();
|
|
3134
|
+
const source = image.attr("src");
|
|
3135
|
+
const destination = source && isSafePortableUrl(source) ? markdownLinkDestination(source) : null;
|
|
3136
|
+
const alternative = escapeMarkdownText(image.attr("alt") ?? "");
|
|
3137
|
+
output += value.slice(cursor, start);
|
|
3138
|
+
output += destination ? `` : alternative;
|
|
3139
|
+
cursor = end;
|
|
3140
|
+
}
|
|
3141
|
+
return cursor === 0 ? value : `${output}${value.slice(cursor)}`;
|
|
3142
|
+
}
|
|
3143
|
+
function replacePortableHtmlAnchors(value) {
|
|
3144
|
+
const matcher = /<a\b/gi;
|
|
3145
|
+
let cursor = 0;
|
|
3146
|
+
let output = "";
|
|
3147
|
+
for (const match of value.matchAll(matcher)) {
|
|
3148
|
+
const start = match.index;
|
|
3149
|
+
if (start < cursor) continue;
|
|
3150
|
+
const openingEnd = htmlOpeningTagEnd(value, start + match[0].length);
|
|
3151
|
+
if (openingEnd === null) continue;
|
|
3152
|
+
const closingMatch = /<\/a\s*>/gi;
|
|
3153
|
+
closingMatch.lastIndex = openingEnd;
|
|
3154
|
+
const closing = closingMatch.exec(value);
|
|
3155
|
+
if (!closing) continue;
|
|
3156
|
+
const end = closing.index + closing[0].length;
|
|
3157
|
+
const fragment = load(value.slice(start, end));
|
|
3158
|
+
const anchor = fragment("a").first();
|
|
3159
|
+
const href = anchor.attr("href");
|
|
3160
|
+
const label = escapeMarkdownText(anchor.text().replace(/\s+/g, " ").trim());
|
|
3161
|
+
const destination = href && isSafePortableUrl(href) ? markdownLinkDestination(href) : null;
|
|
3162
|
+
output += value.slice(cursor, start);
|
|
3163
|
+
output += destination ? `[${label}](${destination})` : label;
|
|
3164
|
+
cursor = end;
|
|
3165
|
+
}
|
|
3166
|
+
return cursor === 0 ? value : `${output}${value.slice(cursor)}`;
|
|
3167
|
+
}
|
|
3168
|
+
function markdownNodeText(node) {
|
|
3169
|
+
if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
|
|
3170
|
+
if (node.type === "image" || node.type === "imageReference") return node.alt ?? "";
|
|
3171
|
+
return (node.children ?? []).map(markdownNodeText).join("");
|
|
3172
|
+
}
|
|
3173
|
+
function sanitizeMarkdownUrls(value) {
|
|
3174
|
+
let root;
|
|
3175
|
+
try {
|
|
3176
|
+
root = remoteMarkdownParser.parse(value);
|
|
3177
|
+
} catch {
|
|
3178
|
+
return null;
|
|
3179
|
+
}
|
|
3180
|
+
let hasUnsafeMdxSyntax = false;
|
|
3181
|
+
const validateMdxSyntax = (node) => {
|
|
3182
|
+
if (node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" || node.type === "mdxjsEsm") {
|
|
3183
|
+
hasUnsafeMdxSyntax = true;
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
if (node.type === "mdxJsxTextElement" || node.type === "mdxJsxFlowElement") {
|
|
3187
|
+
for (const attribute of node.attributes ?? []) {
|
|
3188
|
+
if (attribute.type !== "mdxJsxAttribute" || !attribute.name || /^on[A-Z]/.test(attribute.name)) {
|
|
3189
|
+
hasUnsafeMdxSyntax = true;
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
let staticValue = attribute.value;
|
|
3193
|
+
if (attribute.value && typeof attribute.value === "object") {
|
|
3194
|
+
if (attribute.value.type !== "mdxJsxAttributeValueExpression") {
|
|
3195
|
+
hasUnsafeMdxSyntax = true;
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
try {
|
|
3199
|
+
staticValue = JSON.parse(attribute.value.value);
|
|
3200
|
+
} catch {
|
|
3201
|
+
hasUnsafeMdxSyntax = true;
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
if (PORTABLE_URL_PROP_NAMES.has(attribute.name) && (typeof staticValue !== "string" || !isSafePortableUrl(decodeHtmlAttribute(staticValue)))) {
|
|
3206
|
+
hasUnsafeMdxSyntax = true;
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
for (const child of node.children ?? []) validateMdxSyntax(child);
|
|
3212
|
+
};
|
|
3213
|
+
validateMdxSyntax(root);
|
|
3214
|
+
if (hasUnsafeMdxSyntax) return null;
|
|
3215
|
+
const unsafeDefinitions = /* @__PURE__ */ new Set();
|
|
3216
|
+
const visitDefinitions = (node) => {
|
|
3217
|
+
if (node.type === "definition" && node.identifier && node.url && !isSafePortableUrl(node.url)) unsafeDefinitions.add(node.identifier.toLowerCase());
|
|
3218
|
+
for (const child of node.children ?? []) visitDefinitions(child);
|
|
3219
|
+
};
|
|
3220
|
+
visitDefinitions(root);
|
|
3221
|
+
const replacements = [];
|
|
3222
|
+
const replaceNode = (node, replacement) => {
|
|
3223
|
+
const start = node.position?.start.offset;
|
|
3224
|
+
const end = node.position?.end.offset;
|
|
3225
|
+
if (start === void 0 || end === void 0) return false;
|
|
3226
|
+
replacements.push({ start, end, value: replacement });
|
|
3227
|
+
return true;
|
|
3228
|
+
};
|
|
3229
|
+
const visitUrls = (node) => {
|
|
3230
|
+
const hasUnsafeDirectUrl = (node.type === "link" || node.type === "image") && typeof node.url === "string" && !isSafePortableUrl(node.url);
|
|
3231
|
+
const hasUnsafeReference = (node.type === "linkReference" || node.type === "imageReference") && typeof node.identifier === "string" && unsafeDefinitions.has(node.identifier.toLowerCase());
|
|
3232
|
+
if (hasUnsafeDirectUrl || hasUnsafeReference) {
|
|
3233
|
+
if (replaceNode(node, escapeMarkdownText(markdownNodeText(node)))) return;
|
|
3234
|
+
}
|
|
3235
|
+
if (node.type === "definition" && node.identifier && unsafeDefinitions.has(node.identifier.toLowerCase())) {
|
|
3236
|
+
if (replaceNode(node, "")) return;
|
|
3237
|
+
}
|
|
3238
|
+
for (const child of node.children ?? []) visitUrls(child);
|
|
3239
|
+
};
|
|
3240
|
+
visitUrls(root);
|
|
3241
|
+
return replacements.sort((left, right) => right.start - left.start).reduce((result, replacement) => `${result.slice(0, replacement.start)}${replacement.value}${result.slice(replacement.end)}`, value);
|
|
2438
3242
|
}
|
|
2439
3243
|
function hasUnsafePortableUrlProp(value) {
|
|
2440
3244
|
const quotedProps = new RegExp(`\\b(?:${PORTABLE_URL_PROPS})\\s*=\\s*(['"])([^'"\\r\\n]*)\\1`, "gi");
|
|
2441
3245
|
for (const match of value.matchAll(quotedProps)) {
|
|
2442
|
-
if (
|
|
3246
|
+
if (!isSafePortableUrl(decodeHtmlAttribute(match[2]))) return true;
|
|
2443
3247
|
}
|
|
2444
3248
|
const expressionProps = new RegExp(`\\b(?:${PORTABLE_URL_PROPS})\\s*=\\s*\\{\\s*("(?:\\\\.|[^"\\\\])*")\\s*\\}`, "gi");
|
|
2445
3249
|
for (const match of value.matchAll(expressionProps)) {
|
|
2446
3250
|
try {
|
|
2447
|
-
if (
|
|
3251
|
+
if (!isSafePortableUrl(decodeHtmlAttribute(JSON.parse(match[1])))) return true;
|
|
2448
3252
|
} catch {
|
|
2449
3253
|
return true;
|
|
2450
3254
|
}
|
|
@@ -2452,12 +3256,7 @@ function hasUnsafePortableUrlProp(value) {
|
|
|
2452
3256
|
return false;
|
|
2453
3257
|
}
|
|
2454
3258
|
function portableHtmlToMarkdown(value) {
|
|
2455
|
-
return value.replace(/<span\b[^>]*>|<\/span>/gi, "").replace(/<
|
|
2456
|
-
const href = attributes.match(/\bhref\s*=\s*(['"])(.*?)\1/i)?.[2];
|
|
2457
|
-
const plainLabel = label.replace(/<[^>]+>/g, "").trim();
|
|
2458
|
-
if (!href || hasUnsafeUrlScheme(href)) return plainLabel;
|
|
2459
|
-
return `[${plainLabel.replace(/]/g, "\\]")}](${href})`;
|
|
2460
|
-
}).replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_original, level, content) => {
|
|
3259
|
+
return replacePortableHtmlAnchors(replacePortableHtmlImages(value)).replace(/<span\b[^>]*>|<\/span>/gi, "").replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_original, level, content) => {
|
|
2461
3260
|
return `
|
|
2462
3261
|
${"#".repeat(Number(level))} ${content.trim()}
|
|
2463
3262
|
`;
|
|
@@ -2514,16 +3313,11 @@ function sanitizeRemoteMarkdown(body) {
|
|
|
2514
3313
|
return line;
|
|
2515
3314
|
}
|
|
2516
3315
|
if (fenceCharacter) return line;
|
|
2517
|
-
return portableHtmlToMarkdown(stripEventHandlerProps(line.replace(/!\[([^\]]*)\]\(\s*([^)]+)\)/g, (original, alternative, destination) =>
|
|
2518
|
-
return hasUnsafeUrlScheme(destination.replace(/^<|>$/g, "")) ? alternative : original;
|
|
2519
|
-
}).replace(/<img\s+([^>]*?)\/?\s*>/gi, (original, attributes) => {
|
|
2520
|
-
const source = attributes.match(/\bsrc\s*=\s*(['"])(.*?)\1/i)?.[2];
|
|
2521
|
-
const alternative = attributes.match(/\balt\s*=\s*(['"])(.*?)\1/i)?.[2] ?? "";
|
|
2522
|
-
if (!source || hasUnsafeUrlScheme(source)) return "";
|
|
2523
|
-
return `![${alternative.replace(/]/g, "\\]")}](${source})`;
|
|
2524
|
-
})));
|
|
3316
|
+
return portableHtmlToMarkdown(stripEventHandlerProps(line.replace(/!\[([^\]]*)\]\(\s*([^)]+)\)/g, (original, alternative, destination) => isSafePortableUrl(decodeHtmlAttribute(destination.replace(/^<|>$/g, ""))) ? original : escapeMarkdownText(alternative))));
|
|
2525
3317
|
}).join("\n");
|
|
2526
|
-
const
|
|
3318
|
+
const urlSafeMarkdown = sanitizeMarkdownUrls(sanitized);
|
|
3319
|
+
if (urlSafeMarkdown === null) return null;
|
|
3320
|
+
const executable = withoutFencedCode(urlSafeMarkdown).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<(?:https?:\/\/|mailto:)[^>]+>/gi, "").replace(/(`+)[\s\S]*?\1/g, "");
|
|
2527
3321
|
if (/^\s*(?:import|export)\s/m.test(executable)) return null;
|
|
2528
3322
|
let hasUnsafeExpression = false;
|
|
2529
3323
|
const withoutStaticProps = executable.replace(/=\{([^{}\n]*)\}/g, (original, value) => {
|
|
@@ -2540,7 +3334,7 @@ function sanitizeRemoteMarkdown(body) {
|
|
|
2540
3334
|
const component = match[1].split(".", 1)[0];
|
|
2541
3335
|
if (!PORTABLE_MDX_COMPONENTS.has(component)) return null;
|
|
2542
3336
|
}
|
|
2543
|
-
return
|
|
3337
|
+
return urlSafeMarkdown;
|
|
2544
3338
|
}
|
|
2545
3339
|
function migratedHref(href, currentUrl, source, scopePath, importedIds, sourceHomePageId) {
|
|
2546
3340
|
if (/^(?:javascript|data|vbscript):/i.test(href)) return "#";
|
|
@@ -2827,7 +3621,10 @@ ${tabs.join("\n\n")}
|
|
|
2827
3621
|
filter: "table",
|
|
2828
3622
|
replacement: (_content, node) => {
|
|
2829
3623
|
const fragment = load(node.outerHTML);
|
|
2830
|
-
const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) =>
|
|
3624
|
+
const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) => escapeMarkdownText(
|
|
3625
|
+
fragment(cell).text().replace(/\s+/g, " ").trim(),
|
|
3626
|
+
true
|
|
3627
|
+
))).filter((row) => row.length > 0);
|
|
2831
3628
|
if (rows.length === 0) return "";
|
|
2832
3629
|
const width = Math.max(...rows.map((row) => row.length));
|
|
2833
3630
|
const normalized = rows.map((row) => [...row, ...Array(width - row.length).fill("")]);
|