@fumadocs/cli 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-BVuNjYXU.js +100 -0
- package/dist/ai-BVuNjYXU.js.map +1 -0
- package/dist/all-aUHxtoXL.js +27 -0
- package/dist/all-aUHxtoXL.js.map +1 -0
- package/dist/codemod/index.d.ts +2 -0
- package/dist/codemod/index.js +2 -0
- package/dist/codemod-auol-p4R.js +404 -0
- package/dist/codemod-auol-p4R.js.map +1 -0
- package/dist/config-CT2jj39D.d.ts +52 -0
- package/dist/config-CT2jj39D.d.ts.map +1 -0
- package/dist/config-D6uv73fs.js +94 -0
- package/dist/config-D6uv73fs.js.map +1 -0
- package/dist/config.d.ts +2 -52
- package/dist/config.js +1 -72
- package/dist/docs-DeIEl_Kt.js +738 -0
- package/dist/docs-DeIEl_Kt.js.map +1 -0
- package/dist/epub-CnQFJh09.js +52 -0
- package/dist/epub-CnQFJh09.js.map +1 -0
- package/dist/features/ai.d.ts +25 -0
- package/dist/features/ai.d.ts.map +1 -0
- package/dist/features/ai.js +2 -0
- package/dist/features/all.d.ts +5 -0
- package/dist/features/all.d.ts.map +1 -0
- package/dist/features/all.js +2 -0
- package/dist/features/docs/index.d.ts +10 -0
- package/dist/features/docs/index.d.ts.map +1 -0
- package/dist/features/docs/index.js +2 -0
- package/dist/features/epub.d.ts +5 -0
- package/dist/features/epub.d.ts.map +1 -0
- package/dist/features/epub.js +2 -0
- package/dist/features/feedback.d.ts +5 -0
- package/dist/features/feedback.d.ts.map +1 -0
- package/dist/features/feedback.js +2 -0
- package/dist/features/index.d.ts +2 -0
- package/dist/features/index.js +2 -0
- package/dist/features/lint.d.ts +7 -0
- package/dist/features/lint.d.ts.map +1 -0
- package/dist/features/lint.js +2 -0
- package/dist/features/llms.d.ts +27 -0
- package/dist/features/llms.d.ts.map +1 -0
- package/dist/features/llms.js +2 -0
- package/dist/features/mcp.d.ts +9 -0
- package/dist/features/mcp.d.ts.map +1 -0
- package/dist/features/mcp.js +2 -0
- package/dist/features/og.d.ts +8 -0
- package/dist/features/og.d.ts.map +1 -0
- package/dist/features/og.js +2 -0
- package/dist/features/search.d.ts +84 -0
- package/dist/features/search.d.ts.map +1 -0
- package/dist/features/search.js +2 -0
- package/dist/features/utils.d.ts +2 -0
- package/dist/features/utils.js +2 -0
- package/dist/features/webmcp.d.ts +12 -0
- package/dist/features/webmcp.d.ts.map +1 -0
- package/dist/features/webmcp.js +2 -0
- package/dist/features-C1kf2s8j.js +132 -0
- package/dist/features-C1kf2s8j.js.map +1 -0
- package/dist/feedback-COmWtc5C.js +37 -0
- package/dist/feedback-COmWtc5C.js.map +1 -0
- package/dist/fs-D6J9dOEx.js +14 -0
- package/dist/fs-D6J9dOEx.js.map +1 -0
- package/dist/index-BCVGQxn9.d.ts +64 -0
- package/dist/index-BCVGQxn9.d.ts.map +1 -0
- package/dist/index-CR9T40ox.d.ts +76 -0
- package/dist/index-CR9T40ox.d.ts.map +1 -0
- package/dist/index-Ck0EPdgu.d.ts +126 -0
- package/dist/index-Ck0EPdgu.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +198 -124
- package/dist/index.js.map +1 -1
- package/dist/{installer-Chjj_R2z.js → installer-D0Obb2Md.js} +21 -8
- package/dist/installer-D0Obb2Md.js.map +1 -0
- package/dist/lint-zkXUULgO.js +137 -0
- package/dist/lint-zkXUULgO.js.map +1 -0
- package/dist/llms-CONd8Vnn.js +228 -0
- package/dist/llms-CONd8Vnn.js.map +1 -0
- package/dist/mcp-BT8BLZZ-.js +67 -0
- package/dist/mcp-BT8BLZZ-.js.map +1 -0
- package/dist/og-DRDOyQ_t.js +142 -0
- package/dist/og-DRDOyQ_t.js.map +1 -0
- package/dist/project/index.d.ts +2 -0
- package/dist/project/index.js +2 -0
- package/dist/project-DwRVTm4p.js +110 -0
- package/dist/project-DwRVTm4p.js.map +1 -0
- package/dist/registry/installer.d.ts +4 -5
- package/dist/registry/installer.d.ts.map +1 -1
- package/dist/registry/installer.js +1 -1
- package/dist/schema.json +1 -1
- package/dist/search-iphnSVVY.js +381 -0
- package/dist/search-iphnSVVY.js.map +1 -0
- package/dist/utils-B1Xy4l_a.d.ts +43 -0
- package/dist/utils-B1Xy4l_a.d.ts.map +1 -0
- package/dist/utils-DbMe7h6c.js +98 -0
- package/dist/utils-DbMe7h6c.js.map +1 -0
- package/dist/webmcp-C2PM_9md.js +135 -0
- package/dist/webmcp-C2PM_9md.js.map +1 -0
- package/package.json +27 -7
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/installer-Chjj_R2z.js.map +0 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { c as addNextRewrites, n as enableProcessedMarkdown, p as addImport, r as addTanstackPrerender, t as addProxyMatcher } from "./codemod-auol-p4R.js";
|
|
2
|
+
import { a as routeModule, i as reactRouterTypes, n as findNextProxy, r as formatRoute, t as docs } from "./docs-DeIEl_Kt.js";
|
|
3
|
+
import { a as reactFramework, c as requiresMarkdown, d as sourceRef, n as addPageUrl, s as registerReactRouterRoutes, t as addExport, u as sharedRoute } from "./utils-DbMe7h6c.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
//#region src/features/llms.ts
|
|
6
|
+
/** runtime content sources expose raw Markdown as `page.data.content`, Fumadocs MDX processes it */
|
|
7
|
+
const docsLlms = ({ dynamic, ref }) => `
|
|
8
|
+
export const docsLlms = llms(${ref}, {
|
|
9
|
+
renderPage: ${dynamic ? "(page)" : "async (page)"} => \`# \${page.data.title} (\${page.url})
|
|
10
|
+
|
|
11
|
+
\${${dynamic ? "page.data.content" : "await page.data.getText('processed')"}}\`,
|
|
12
|
+
});`;
|
|
13
|
+
/** the Markdown URL of a page, `/llms.mdx/docs/<slugs>/content.md` */
|
|
14
|
+
const markdownUrl = (i18n) => `
|
|
15
|
+
const getContentUrl = createGetUrl(docsContentRoute${i18n ? ", i18n" : ""});
|
|
16
|
+
|
|
17
|
+
export function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) {
|
|
18
|
+
const segments = [...page.slugs, 'content.md'];
|
|
19
|
+
|
|
20
|
+
return { segments, url: getContentUrl(segments, page.locale) };
|
|
21
|
+
}`;
|
|
22
|
+
/** TanStack Start serves Markdown with a `.md` suffix under the docs route */
|
|
23
|
+
const tanstackMarkdownUrl = (i18n) => `
|
|
24
|
+
const getDocsUrl = createGetUrl(docsRoute${i18n ? ", i18n" : ""});
|
|
25
|
+
|
|
26
|
+
export function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) {
|
|
27
|
+
const segments = [...page.slugs];
|
|
28
|
+
if (segments.length === 0) {
|
|
29
|
+
segments.push('index.md');
|
|
30
|
+
} else {
|
|
31
|
+
segments[segments.length - 1] += '.md';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { segments, url: getDocsUrl(segments, page.locale) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @returns page slugs */
|
|
38
|
+
export function decodeMarkdownUrl(segments: string[]) {
|
|
39
|
+
if (segments.length === 0) return [];
|
|
40
|
+
|
|
41
|
+
const out = [...segments];
|
|
42
|
+
out[out.length - 1] = out[out.length - 1].replace(/\\.md$/, '');
|
|
43
|
+
if (out.length === 1 && out[0] === 'index') out.pop();
|
|
44
|
+
return out;
|
|
45
|
+
}`;
|
|
46
|
+
const imports = `import { docsLlms } from '@/lib/source';`;
|
|
47
|
+
const pageImports = ({ ref }) => `import { docsLlms, ${ref} } from '@/lib/source';`;
|
|
48
|
+
const markdown = `new Response(await docsLlms.page(page), {
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'text/markdown',
|
|
51
|
+
},
|
|
52
|
+
})`;
|
|
53
|
+
/** the per-page Markdown route, the only one reading route params */
|
|
54
|
+
const markdownTemplates = {
|
|
55
|
+
next: (route, i18n, src) => `${pageImports(src)}
|
|
56
|
+
import { notFound } from 'next/navigation';
|
|
57
|
+
|
|
58
|
+
export const revalidate = false;
|
|
59
|
+
|
|
60
|
+
export async function GET(_req: Request, { params }: RouteContext<'${route.path}'>) {
|
|
61
|
+
const { slug${i18n ? ", lang" : ""} } = await params;
|
|
62
|
+
// remove the appended "content.md", \`/docs/index.md\` is rewritten to the root page
|
|
63
|
+
const slugs = slug?.slice(0, -1) ?? [];
|
|
64
|
+
if (slugs.at(-1) === 'index') slugs.pop();
|
|
65
|
+
const page = ${src.resolved}.getPage(slugs${i18n ? ", lang" : ""});
|
|
66
|
+
if (!page) notFound();
|
|
67
|
+
|
|
68
|
+
return ${markdown};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export ${src.dynamic ? "async " : ""}function generateStaticParams() {
|
|
72
|
+
return ${src.resolved}.generateParams().map((item) => ({
|
|
73
|
+
...item,
|
|
74
|
+
slug: [...item.slug, 'content.md'],
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
"react-router": (route, i18n, src) => `${reactRouterTypes(route)}
|
|
79
|
+
${pageImports(src)}
|
|
80
|
+
|
|
81
|
+
export async function loader({ params }: Route.LoaderArgs) {
|
|
82
|
+
const slugs = params['*'].split('/').filter((v) => v.length > 0);
|
|
83
|
+
// remove the appended "content.md"
|
|
84
|
+
slugs.pop();
|
|
85
|
+
const page = ${src.resolved}.getPage(slugs${i18n ? ", params.lang" : ""});
|
|
86
|
+
if (!page) return new Response('not found', { status: 404 });
|
|
87
|
+
|
|
88
|
+
return ${markdown};
|
|
89
|
+
}
|
|
90
|
+
`,
|
|
91
|
+
"tanstack-start": (route, i18n, src) => `import { createFileRoute, notFound } from '@tanstack/react-router';
|
|
92
|
+
import { decodeMarkdownUrl } from '@/lib/shared';
|
|
93
|
+
${pageImports(src)}
|
|
94
|
+
|
|
95
|
+
export const Route = createFileRoute('${route.path}')({
|
|
96
|
+
server: {
|
|
97
|
+
handlers: {
|
|
98
|
+
GET: async ({ params }) => {
|
|
99
|
+
const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []);
|
|
100
|
+
const page = ${src.resolved}.getPage(slugs${i18n ? ", params.lang" : ""});
|
|
101
|
+
if (!page) throw notFound();
|
|
102
|
+
|
|
103
|
+
return ${markdown.replaceAll("\n ", "\n ")};
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
`,
|
|
109
|
+
waku: (route, i18n, src) => `${pageImports(src)}
|
|
110
|
+
import type { ApiContext } from 'waku/router';
|
|
111
|
+
import { unstable_notFound } from 'waku/router/server';
|
|
112
|
+
|
|
113
|
+
export async function GET(_: Request, { params }: ApiContext<'${route.path}'>) {
|
|
114
|
+
const page = ${src.resolved}.getPage(params.slug${i18n ? ", params.lang" : ""});
|
|
115
|
+
if (!page) unstable_notFound();
|
|
116
|
+
|
|
117
|
+
return ${markdown};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function getConfig() {
|
|
121
|
+
return {
|
|
122
|
+
render: 'static' as const,
|
|
123
|
+
staticPaths: ${src.resolved}.generateParams().map((item) => ${i18n ? "[item.lang, ...item.slug]" : "item.slug"}),
|
|
124
|
+
} as const;
|
|
125
|
+
}
|
|
126
|
+
`
|
|
127
|
+
};
|
|
128
|
+
function templates(framework, routes, i18n, src) {
|
|
129
|
+
const index = (body) => ({
|
|
130
|
+
imports,
|
|
131
|
+
body,
|
|
132
|
+
revalidate: true,
|
|
133
|
+
static: true
|
|
134
|
+
});
|
|
135
|
+
return [
|
|
136
|
+
[routes.index, routeModule(framework, routes.index, index("new Response(await docsLlms.index())"))],
|
|
137
|
+
[routes.full, routeModule(framework, routes.full, index("new Response(await docsLlms.full())"))],
|
|
138
|
+
[routes.markdown, markdownTemplates[framework](routes.markdown, i18n, src)]
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
function llmsRoutes(framework, i18n, docsRoute, contentRoute) {
|
|
142
|
+
const format = (route) => formatRoute(route, framework, i18n);
|
|
143
|
+
return {
|
|
144
|
+
index: format({ segments: ["llms.txt"] }),
|
|
145
|
+
full: format({ segments: ["llms-full.txt"] }),
|
|
146
|
+
markdown: format(framework === "tanstack-start" ? {
|
|
147
|
+
segments: [docsRoute, "{$}.md"],
|
|
148
|
+
locale: true
|
|
149
|
+
} : {
|
|
150
|
+
segments: [contentRoute, {
|
|
151
|
+
param: "slug",
|
|
152
|
+
catchAll: true,
|
|
153
|
+
optional: true,
|
|
154
|
+
suffix: "content.md"
|
|
155
|
+
}],
|
|
156
|
+
locale: true
|
|
157
|
+
})
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const llms = {
|
|
161
|
+
id: "llms",
|
|
162
|
+
title: "LLM Routes",
|
|
163
|
+
description: "serve docs as Markdown for LLMs: llms.txt, llms-full.txt and per-page Markdown",
|
|
164
|
+
requires: [docs],
|
|
165
|
+
supports: requiresMarkdown,
|
|
166
|
+
async apply(ctx) {
|
|
167
|
+
const { baseDir, i18n, source, configFile } = ctx.project;
|
|
168
|
+
const framework = reactFramework(ctx.project);
|
|
169
|
+
const sourceFile = path.join(baseDir, "lib/source.ts");
|
|
170
|
+
const src = sourceRef(ctx.project.source.dynamic);
|
|
171
|
+
if (source.collections) await ctx.source(source.collections, (file) => {
|
|
172
|
+
if (!enableProcessedMarkdown(file)) throw new Error(`cannot find \`defineDocs()\` in ${source.collections}`);
|
|
173
|
+
});
|
|
174
|
+
if (await addExport(ctx, sourceFile, "docsLlms", docsLlms(src))) await ctx.source(sourceFile, (file) => addImport(file, {
|
|
175
|
+
from: "fumadocs-core/source",
|
|
176
|
+
named: ["llms"]
|
|
177
|
+
}));
|
|
178
|
+
const docsRoute = source.baseUrl.replace(/\/$/, "");
|
|
179
|
+
let contentRoute = `/llms.mdx${docsRoute}`;
|
|
180
|
+
if (framework === "tanstack-start") {
|
|
181
|
+
await sharedRoute(ctx, "docsRoute", source.baseUrl);
|
|
182
|
+
await addPageUrl(ctx, "getPageMarkdownUrl", tanstackMarkdownUrl(i18n !== null));
|
|
183
|
+
} else {
|
|
184
|
+
contentRoute = await sharedRoute(ctx, "docsContentRoute", contentRoute);
|
|
185
|
+
await addPageUrl(ctx, "getPageMarkdownUrl", markdownUrl(i18n !== null));
|
|
186
|
+
}
|
|
187
|
+
const routes = llmsRoutes(framework, i18n, docsRoute, contentRoute);
|
|
188
|
+
for (const [route, content] of templates(framework, routes, i18n !== null, src)) await ctx.write(path.join(baseDir, route.file), content);
|
|
189
|
+
if (framework === "react-router") await registerReactRouterRoutes(ctx, [
|
|
190
|
+
routes.index,
|
|
191
|
+
routes.full,
|
|
192
|
+
routes.markdown
|
|
193
|
+
]);
|
|
194
|
+
else if (framework === "tanstack-start" && ctx.project.static && configFile) await ctx.source(configFile, (file) => addTanstackPrerender(file, [routes.index.path, routes.full.path]));
|
|
195
|
+
else if (framework === "next") {
|
|
196
|
+
if (i18n) await extendNextProxy(ctx, routes.markdown);
|
|
197
|
+
await configureMarkdownUrl(ctx, docsRoute, routes.markdown);
|
|
198
|
+
}
|
|
199
|
+
if (ctx.project.static && framework !== "next") ctx.note("Per-page Markdown routes are dynamic, add the pages to your prerender config to serve them statically.");
|
|
200
|
+
if (framework === "react-router" || framework === "waku") ctx.note("To serve pages with a `.md` suffix, rewrite `/docs/*.md` to the `llms.mdx` route in a middleware, see https://fumadocs.dev/docs/integrations/llms#md-extension.");
|
|
201
|
+
ctx.note("Add page actions to your docs page with `MarkdownCopyButton` and `ViewOptionsPopover` from 'fumadocs-ui/layouts/docs/page', the Markdown URL of a page is `getPageMarkdownUrl(page).url` from '@/lib/shared'.");
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
/** `/docs/*.md` serves the Markdown of a page, like TanStack Start's `.md` route */
|
|
205
|
+
const markdownRewrite = (docsRoute, markdown, i18n) => ({
|
|
206
|
+
source: `${i18n ? "/:lang" : ""}${docsRoute}/:slug*.md`,
|
|
207
|
+
destination: `${markdown.pattern}/content.md`
|
|
208
|
+
});
|
|
209
|
+
async function configureMarkdownUrl(ctx, docsRoute, markdown) {
|
|
210
|
+
const { configFile, i18n } = ctx.project;
|
|
211
|
+
const rewrite = markdownRewrite(docsRoute, markdown, i18n !== null);
|
|
212
|
+
if (!(configFile !== void 0 && await ctx.source(configFile, (file) => {
|
|
213
|
+
if (file.code.includes("llms.mdx")) return;
|
|
214
|
+
if (!addNextRewrites(file, [rewrite])) throw new Error("cannot add rewrites");
|
|
215
|
+
}).catch(() => false))) ctx.note(`Rewrite \`${rewrite.source}\` to \`${rewrite.destination}\` in your Next.js config to serve pages with a \`.md\` suffix.`);
|
|
216
|
+
}
|
|
217
|
+
/** routes under `[lang]` need the i18n middleware to resolve the locale */
|
|
218
|
+
async function extendNextProxy(ctx, route) {
|
|
219
|
+
const patterns = [route.pattern.replace("/:lang", ""), route.pattern];
|
|
220
|
+
const proxy = await findNextProxy(ctx.project);
|
|
221
|
+
if (!(proxy !== void 0 && await ctx.source(proxy, (file) => {
|
|
222
|
+
if (!addProxyMatcher(file, patterns)) throw new Error("cannot find the matcher");
|
|
223
|
+
}).catch(() => false))) ctx.note(`Add ${patterns.map((p) => `\`${p}\``).join(", ")} to the matcher of your i18n middleware.`);
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
export { markdownRewrite as a, templates as c, llmsRoutes as i, extendNextProxy as n, markdownUrl as o, llms as r, tanstackMarkdownUrl as s, docsLlms as t };
|
|
227
|
+
|
|
228
|
+
//# sourceMappingURL=llms-CONd8Vnn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llms-CONd8Vnn.js","names":[],"sources":["../src/features/llms.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Feature, FeatureContext } from '@/features';\nimport type { I18nInfo, ReactFramework } from '@/project';\nimport {\n type FormattedRoute,\n formatRoute,\n reactRouterTypes,\n routeModule,\n type RouteDescriptor,\n} from '@/project/route';\nimport {\n addImport,\n addNextRewrites,\n addProxyMatcher,\n addTanstackPrerender,\n enableProcessedMarkdown,\n} from '@/codemod';\nimport { docs, findNextProxy } from './docs';\nimport {\n addExport,\n addPageUrl,\n reactFramework,\n type SourceRef,\n sourceRef,\n registerReactRouterRoutes,\n requiresMarkdown,\n sharedRoute,\n} from './utils';\n\n/** runtime content sources expose raw Markdown as `page.data.content`, Fumadocs MDX processes it */\nexport const docsLlms = ({ dynamic, ref }: SourceRef) => `\nexport const docsLlms = llms(${ref}, {\n renderPage: ${dynamic ? '(page)' : 'async (page)'} => \\`# \\${page.data.title} (\\${page.url})\n\n\\${${dynamic ? 'page.data.content' : \"await page.data.getText('processed')\"}}\\`,\n});`;\n\n/** the Markdown URL of a page, `/llms.mdx/docs/<slugs>/content.md` */\nexport const markdownUrl = (i18n: boolean) => `\nconst getContentUrl = createGetUrl(docsContentRoute${i18n ? ', i18n' : ''});\n\nexport function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) {\n const segments = [...page.slugs, 'content.md'];\n\n return { segments, url: getContentUrl(segments, page.locale) };\n}`;\n\n/** TanStack Start serves Markdown with a `.md` suffix under the docs route */\nexport const tanstackMarkdownUrl = (i18n: boolean) => `\nconst getDocsUrl = createGetUrl(docsRoute${i18n ? ', i18n' : ''});\n\nexport function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) {\n const segments = [...page.slugs];\n if (segments.length === 0) {\n segments.push('index.md');\n } else {\n segments[segments.length - 1] += '.md';\n }\n\n return { segments, url: getDocsUrl(segments, page.locale) };\n}\n\n/** @returns page slugs */\nexport function decodeMarkdownUrl(segments: string[]) {\n if (segments.length === 0) return [];\n\n const out = [...segments];\n out[out.length - 1] = out[out.length - 1].replace(/\\\\.md$/, '');\n if (out.length === 1 && out[0] === 'index') out.pop();\n return out;\n}`;\n\nconst imports = `import { docsLlms } from '@/lib/source';`;\nconst pageImports = ({ ref }: SourceRef) => `import { docsLlms, ${ref} } from '@/lib/source';`;\nconst markdown = `new Response(await docsLlms.page(page), {\n headers: {\n 'Content-Type': 'text/markdown',\n },\n })`;\n\ninterface Routes {\n index: FormattedRoute;\n full: FormattedRoute;\n markdown: FormattedRoute;\n}\n\ntype Template = (route: FormattedRoute, i18n: boolean, src: SourceRef) => string;\n\n/** the per-page Markdown route, the only one reading route params */\nconst markdownTemplates: Record<ReactFramework, Template> = {\n next: (route, i18n, src) => `${pageImports(src)}\nimport { notFound } from 'next/navigation';\n\nexport const revalidate = false;\n\nexport async function GET(_req: Request, { params }: RouteContext<'${route.path}'>) {\n const { slug${i18n ? ', lang' : ''} } = await params;\n // remove the appended \"content.md\", \\`/docs/index.md\\` is rewritten to the root page\n const slugs = slug?.slice(0, -1) ?? [];\n if (slugs.at(-1) === 'index') slugs.pop();\n const page = ${src.resolved}.getPage(slugs${i18n ? ', lang' : ''});\n if (!page) notFound();\n\n return ${markdown};\n}\n\nexport ${src.dynamic ? 'async ' : ''}function generateStaticParams() {\n return ${src.resolved}.generateParams().map((item) => ({\n ...item,\n slug: [...item.slug, 'content.md'],\n }));\n}\n`,\n 'react-router': (route, i18n, src) => `${reactRouterTypes(route)}\n${pageImports(src)}\n\nexport async function loader({ params }: Route.LoaderArgs) {\n const slugs = params['*'].split('/').filter((v) => v.length > 0);\n // remove the appended \"content.md\"\n slugs.pop();\n const page = ${src.resolved}.getPage(slugs${i18n ? ', params.lang' : ''});\n if (!page) return new Response('not found', { status: 404 });\n\n return ${markdown};\n}\n`,\n 'tanstack-start': (\n route,\n i18n,\n src,\n ) => `import { createFileRoute, notFound } from '@tanstack/react-router';\nimport { decodeMarkdownUrl } from '@/lib/shared';\n${pageImports(src)}\n\nexport const Route = createFileRoute('${route.path}')({\n server: {\n handlers: {\n GET: async ({ params }) => {\n const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []);\n const page = ${src.resolved}.getPage(slugs${i18n ? ', params.lang' : ''});\n if (!page) throw notFound();\n\n return ${markdown.replaceAll('\\n ', '\\n ')};\n },\n },\n },\n});\n`,\n waku: (route, i18n, src) => `${pageImports(src)}\nimport type { ApiContext } from 'waku/router';\nimport { unstable_notFound } from 'waku/router/server';\n\nexport async function GET(_: Request, { params }: ApiContext<'${route.path}'>) {\n const page = ${src.resolved}.getPage(params.slug${i18n ? ', params.lang' : ''});\n if (!page) unstable_notFound();\n\n return ${markdown};\n}\n\nexport async function getConfig() {\n return {\n render: 'static' as const,\n staticPaths: ${src.resolved}.generateParams().map((item) => ${i18n ? '[item.lang, ...item.slug]' : 'item.slug'}),\n } as const;\n}\n`,\n};\n\nexport function templates(\n framework: ReactFramework,\n routes: Routes,\n i18n: boolean,\n src: SourceRef,\n): [route: FormattedRoute, content: string][] {\n const index = (body: string) => ({ imports, body, revalidate: true, static: true });\n\n return [\n [\n routes.index,\n routeModule(framework, routes.index, index('new Response(await docsLlms.index())')),\n ],\n [\n routes.full,\n routeModule(framework, routes.full, index('new Response(await docsLlms.full())')),\n ],\n [routes.markdown, markdownTemplates[framework](routes.markdown, i18n, src)],\n ];\n}\n\nexport function llmsRoutes(\n framework: ReactFramework,\n i18n: I18nInfo | null,\n docsRoute: string,\n contentRoute: string,\n): Routes {\n const format = (route: RouteDescriptor) => formatRoute(route, framework, i18n);\n return {\n index: format({ segments: ['llms.txt'] }),\n full: format({ segments: ['llms-full.txt'] }),\n markdown: format(\n framework === 'tanstack-start'\n ? { segments: [docsRoute, '{$}.md'], locale: true }\n : {\n segments: [\n contentRoute,\n { param: 'slug', catchAll: true, optional: true, suffix: 'content.md' },\n ],\n locale: true,\n },\n ),\n };\n}\n\nexport const llms: Feature = {\n id: 'llms',\n title: 'LLM Routes',\n description: 'serve docs as Markdown for LLMs: llms.txt, llms-full.txt and per-page Markdown',\n requires: [docs],\n supports: requiresMarkdown,\n async apply(ctx) {\n const { baseDir, i18n, source, configFile } = ctx.project;\n const framework = reactFramework(ctx.project);\n const sourceFile = path.join(baseDir, 'lib/source.ts');\n const src = sourceRef(ctx.project.source.dynamic);\n // runtime content sources already give raw Markdown\n if (source.collections) {\n await ctx.source(source.collections, (file) => {\n if (!enableProcessedMarkdown(file))\n throw new Error(`cannot find \\`defineDocs()\\` in ${source.collections}`);\n });\n }\n if (await addExport(ctx, sourceFile, 'docsLlms', docsLlms(src))) {\n await ctx.source(sourceFile, (file) =>\n addImport(file, { from: 'fumadocs-core/source', named: ['llms'] }),\n );\n }\n\n const docsRoute = source.baseUrl.replace(/\\/$/, '');\n let contentRoute = `/llms.mdx${docsRoute}`;\n if (framework === 'tanstack-start') {\n await sharedRoute(ctx, 'docsRoute', source.baseUrl);\n await addPageUrl(ctx, 'getPageMarkdownUrl', tanstackMarkdownUrl(i18n !== null));\n } else {\n contentRoute = await sharedRoute(ctx, 'docsContentRoute', contentRoute);\n await addPageUrl(ctx, 'getPageMarkdownUrl', markdownUrl(i18n !== null));\n }\n const routes = llmsRoutes(framework, i18n, docsRoute, contentRoute);\n for (const [route, content] of templates(framework, routes, i18n !== null, src))\n await ctx.write(path.join(baseDir, route.file), content);\n\n if (framework === 'react-router') {\n await registerReactRouterRoutes(ctx, [routes.index, routes.full, routes.markdown]);\n } else if (framework === 'tanstack-start' && ctx.project.static && configFile) {\n // SPA mode only serves prerendered routes\n await ctx.source(configFile, (file) =>\n addTanstackPrerender(file, [routes.index.path, routes.full.path]),\n );\n } else if (framework === 'next') {\n if (i18n) await extendNextProxy(ctx, routes.markdown);\n await configureMarkdownUrl(ctx, docsRoute, routes.markdown);\n }\n if (ctx.project.static && framework !== 'next') {\n ctx.note(\n 'Per-page Markdown routes are dynamic, add the pages to your prerender config to serve them statically.',\n );\n }\n if (framework === 'react-router' || framework === 'waku') {\n ctx.note(\n 'To serve pages with a `.md` suffix, rewrite `/docs/*.md` to the `llms.mdx` route in a middleware, see https://fumadocs.dev/docs/integrations/llms#md-extension.',\n );\n }\n ctx.note(\n \"Add page actions to your docs page with `MarkdownCopyButton` and `ViewOptionsPopover` from 'fumadocs-ui/layouts/docs/page', the Markdown URL of a page is `getPageMarkdownUrl(page).url` from '@/lib/shared'.\",\n );\n },\n};\n\n/** `/docs/*.md` serves the Markdown of a page, like TanStack Start's `.md` route */\nexport const markdownRewrite = (docsRoute: string, markdown: FormattedRoute, i18n: boolean) => ({\n source: `${i18n ? '/:lang' : ''}${docsRoute}/:slug*.md`,\n destination: `${markdown.pattern}/content.md`,\n});\n\nasync function configureMarkdownUrl(\n ctx: FeatureContext,\n docsRoute: string,\n markdown: FormattedRoute,\n) {\n const { configFile, i18n } = ctx.project;\n const rewrite = markdownRewrite(docsRoute, markdown, i18n !== null);\n const edited =\n configFile !== undefined &&\n (await ctx\n .source(configFile, (file) => {\n if (file.code.includes('llms.mdx')) return;\n if (!addNextRewrites(file, [rewrite])) throw new Error('cannot add rewrites');\n })\n .catch(() => false));\n\n if (!edited) {\n ctx.note(\n `Rewrite \\`${rewrite.source}\\` to \\`${rewrite.destination}\\` in your Next.js config to serve pages with a \\`.md\\` suffix.`,\n );\n }\n}\n\n/** routes under `[lang]` need the i18n middleware to resolve the locale */\nexport async function extendNextProxy(ctx: FeatureContext, route: FormattedRoute) {\n const patterns = [route.pattern.replace('/:lang', ''), route.pattern];\n const proxy = await findNextProxy(ctx.project);\n const edited =\n proxy !== undefined &&\n (await ctx\n .source(proxy, (file) => {\n if (!addProxyMatcher(file, patterns)) throw new Error('cannot find the matcher');\n })\n .catch(() => false));\n if (!edited)\n ctx.note(\n `Add ${patterns.map((p) => `\\`${p}\\``).join(', ')} to the matcher of your i18n middleware.`,\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { i as addReactRouterPrerenderArray } from "./codemod-auol-p4R.js";
|
|
2
|
+
import { a as routeModule, r as formatRoute } from "./docs-DeIEl_Kt.js";
|
|
3
|
+
import { a as reactFramework, c as requiresMarkdown, d as sourceRef, s as registerReactRouterRoutes } from "./utils-DbMe7h6c.js";
|
|
4
|
+
import { r as llms } from "./llms-CONd8Vnn.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
//#region src/features/mcp.ts
|
|
7
|
+
const server = ({ ref }) => `import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
|
|
8
|
+
import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp';
|
|
9
|
+
import { createFromSource } from 'fumadocs-core/search/server';
|
|
10
|
+
import { docsLlms, ${ref} } from '@/lib/source';
|
|
11
|
+
|
|
12
|
+
const handler = createMcpHandler(() => {
|
|
13
|
+
const mcp = new McpServer({
|
|
14
|
+
name: 'docs',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
registerSourceTools(mcp, ${ref}, docsLlms);
|
|
19
|
+
registerSearchTool(mcp, createFromSource(${ref}));
|
|
20
|
+
|
|
21
|
+
return mcp;
|
|
22
|
+
});
|
|
23
|
+
`;
|
|
24
|
+
const templates = (framework, route, src) => routeModule(framework, route, {
|
|
25
|
+
imports: server(src),
|
|
26
|
+
body: "handler.fetch(request)",
|
|
27
|
+
request: true,
|
|
28
|
+
methods: [
|
|
29
|
+
"GET",
|
|
30
|
+
"POST",
|
|
31
|
+
"DELETE"
|
|
32
|
+
]
|
|
33
|
+
});
|
|
34
|
+
const mcpRoute = (framework) => formatRoute({ segments: ["api/mcp"] }, framework, null);
|
|
35
|
+
const mcp = {
|
|
36
|
+
id: "mcp",
|
|
37
|
+
title: "MCP Server",
|
|
38
|
+
description: "a MCP server for AI agents to search and read your docs, at /api/mcp",
|
|
39
|
+
requires: [llms],
|
|
40
|
+
supports: (project) => project.static ? "MCP requires a server at runtime" : requiresMarkdown(project),
|
|
41
|
+
async apply(ctx) {
|
|
42
|
+
const { baseDir } = ctx.project;
|
|
43
|
+
const framework = reactFramework(ctx.project);
|
|
44
|
+
ctx.addDependencies({
|
|
45
|
+
"@modelcontextprotocol/server": null,
|
|
46
|
+
zod: null
|
|
47
|
+
});
|
|
48
|
+
const route = mcpRoute(framework);
|
|
49
|
+
await ctx.write(path.join(baseDir, route.file), templates(framework, route, sourceRef(ctx.project.source.dynamic)));
|
|
50
|
+
if (framework === "react-router") {
|
|
51
|
+
await registerReactRouterRoutes(ctx, [route]);
|
|
52
|
+
await ctx.source("react-router.config.ts", (file) => {
|
|
53
|
+
addReactRouterPrerenderArray(file, "excluded", [`/${route.path}`]);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
ctx.note(`Connect AI agents to your docs with the MCP server:
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"docs": { "url": "https://your-site.com/api/mcp" }
|
|
60
|
+
}
|
|
61
|
+
}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
export { mcpRoute as n, templates as r, mcp as t };
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=mcp-BT8BLZZ-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-BT8BLZZ-.js","names":[],"sources":["../src/features/mcp.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Feature } from '@/features';\nimport type { ReactFramework } from '@/project';\nimport { type FormattedRoute, formatRoute, routeModule } from '@/project/route';\nimport { addReactRouterPrerenderArray } from '@/codemod';\nimport { llms } from './llms';\nimport {\n reactFramework,\n registerReactRouterRoutes,\n requiresMarkdown,\n type SourceRef,\n sourceRef,\n} from './utils';\n\nconst server = ({\n ref,\n}: SourceRef) => `import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';\nimport { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp';\nimport { createFromSource } from 'fumadocs-core/search/server';\nimport { docsLlms, ${ref} } from '@/lib/source';\n\nconst handler = createMcpHandler(() => {\n const mcp = new McpServer({\n name: 'docs',\n version: '1.0.0',\n });\n\n registerSourceTools(mcp, ${ref}, docsLlms);\n registerSearchTool(mcp, createFromSource(${ref}));\n\n return mcp;\n});\n`;\n\nexport const templates = (framework: ReactFramework, route: FormattedRoute, src: SourceRef) =>\n routeModule(framework, route, {\n imports: server(src),\n body: 'handler.fetch(request)',\n request: true,\n methods: ['GET', 'POST', 'DELETE'],\n });\n\nexport const mcpRoute = (framework: ReactFramework) =>\n formatRoute({ segments: ['api/mcp'] }, framework, null);\n\nexport const mcp: Feature = {\n id: 'mcp',\n title: 'MCP Server',\n description: 'a MCP server for AI agents to search and read your docs, at /api/mcp',\n requires: [llms],\n supports: (project) =>\n project.static ? 'MCP requires a server at runtime' : requiresMarkdown(project),\n async apply(ctx) {\n const { baseDir } = ctx.project;\n const framework = reactFramework(ctx.project);\n ctx.addDependencies({ '@modelcontextprotocol/server': null, zod: null });\n\n const route = mcpRoute(framework);\n await ctx.write(\n path.join(baseDir, route.file),\n templates(framework, route, sourceRef(ctx.project.source.dynamic)),\n );\n\n if (framework === 'react-router') {\n await registerReactRouterRoutes(ctx, [route]);\n await ctx.source('react-router.config.ts', (file) => {\n addReactRouterPrerenderArray(file, 'excluded', [`/${route.path}`]);\n });\n }\n\n ctx.note(`Connect AI agents to your docs with the MCP server:\n{\n \"mcpServers\": {\n \"docs\": { \"url\": \"https://your-site.com/api/mcp\" }\n }\n}`);\n },\n};\n"],"mappings":";;;;;;AAcA,MAAM,UAAU,EACd,UACe;;;qBAGI,IAAI;;;;;;;;6BAQI,IAAI;6CACY,IAAI;;;;;AAMjD,MAAa,aAAa,WAA2B,OAAuB,QAC1E,YAAY,WAAW,OAAO;CAC5B,SAAS,OAAO,GAAG;CACnB,MAAM;CACN,SAAS;CACT,SAAS;EAAC;EAAO;EAAQ;CAAQ;AACnC,CAAC;AAEH,MAAa,YAAY,cACvB,YAAY,EAAE,UAAU,CAAC,SAAS,EAAE,GAAG,WAAW,IAAI;AAExD,MAAa,MAAe;CAC1B,IAAI;CACJ,OAAO;CACP,aAAa;CACb,UAAU,CAAC,IAAI;CACf,WAAW,YACT,QAAQ,SAAS,qCAAqC,iBAAiB,OAAO;CAChF,MAAM,MAAM,KAAK;EACf,MAAM,EAAE,YAAY,IAAI;EACxB,MAAM,YAAY,eAAe,IAAI,OAAO;EAC5C,IAAI,gBAAgB;GAAE,gCAAgC;GAAM,KAAK;EAAK,CAAC;EAEvE,MAAM,QAAQ,SAAS,SAAS;EAChC,MAAM,IAAI,MACR,KAAK,KAAK,SAAS,MAAM,IAAI,GAC7B,UAAU,WAAW,OAAO,UAAU,IAAI,QAAQ,OAAO,OAAO,CAAC,CACnE;EAEA,IAAI,cAAc,gBAAgB;GAChC,MAAM,0BAA0B,KAAK,CAAC,KAAK,CAAC;GAC5C,MAAM,IAAI,OAAO,2BAA2B,SAAS;IACnD,6BAA6B,MAAM,YAAY,CAAC,IAAI,MAAM,MAAM,CAAC;GACnE,CAAC;EACH;EAEA,IAAI,KAAK;;;;;EAKX;CACA;AACF"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { S as getProperty, f as addElements, u as getConfigObject } from "./codemod-auol-p4R.js";
|
|
2
|
+
import { i as reactRouterTypes, r as formatRoute, t as docs } from "./docs-DeIEl_Kt.js";
|
|
3
|
+
import { a as reactFramework, d as sourceRef, n as addPageUrl, s as registerReactRouterRoutes, u as sharedRoute } from "./utils-DbMe7h6c.js";
|
|
4
|
+
import { n as extendNextProxy } from "./llms-CONd8Vnn.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
//#region src/features/og.ts
|
|
7
|
+
/** the image URL of a page, `/og/docs/<slugs>/image.png` */
|
|
8
|
+
const imageUrl = (ext, i18n) => `
|
|
9
|
+
const getImageUrl = createGetUrl(docsImageRoute${i18n ? ", i18n" : ""});
|
|
10
|
+
|
|
11
|
+
export function getPageImageUrl(page: { slugs: string[]; locale?: string }) {
|
|
12
|
+
const segments = [...page.slugs, 'image.${ext}'];
|
|
13
|
+
|
|
14
|
+
return { segments, url: getImageUrl(segments, page.locale) };
|
|
15
|
+
}`;
|
|
16
|
+
const imports = (engine) => `import { generateOGImage } from 'fumadocs-ui/og${engine === "takumi" ? "/takumi" : ""}';`;
|
|
17
|
+
const image = (engine) => `generateOGImage({
|
|
18
|
+
title: page.data.title,
|
|
19
|
+
description: page.data.description,
|
|
20
|
+
site: 'My App',${engine === "takumi" ? "\n format: 'webp'," : ""}
|
|
21
|
+
})`;
|
|
22
|
+
const ext = (engine) => engine === "takumi" ? "webp" : "png";
|
|
23
|
+
const templates = {
|
|
24
|
+
next: (route, engine, i18n, src) => `import { ${src.ref} } from '@/lib/source';
|
|
25
|
+
import { notFound } from 'next/navigation';
|
|
26
|
+
${imports(engine)}
|
|
27
|
+
|
|
28
|
+
export const revalidate = false;
|
|
29
|
+
|
|
30
|
+
export async function GET(_req: Request, { params }: RouteContext<'${route.path}'>) {
|
|
31
|
+
const { slug${i18n ? ", lang" : ""} } = await params;
|
|
32
|
+
const page = ${src.resolved}.getPage(slug.slice(0, -1)${i18n ? ", lang" : ""});
|
|
33
|
+
if (!page) notFound();
|
|
34
|
+
|
|
35
|
+
return ${image(engine)};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export ${src.dynamic ? "async " : ""}function generateStaticParams() {
|
|
39
|
+
return ${src.resolved}.generateParams().map((item) => ({
|
|
40
|
+
...item,
|
|
41
|
+
slug: [...item.slug, 'image.${ext(engine)}'],
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
`,
|
|
45
|
+
"react-router": (route, engine, i18n, src) => `${reactRouterTypes(route)}
|
|
46
|
+
import { ${src.ref} } from '@/lib/source';
|
|
47
|
+
${imports(engine)}
|
|
48
|
+
|
|
49
|
+
export async function loader({ params }: Route.LoaderArgs) {
|
|
50
|
+
const slugs = params['*'].split('/').filter((v) => v.length > 0);
|
|
51
|
+
const page = ${src.resolved}.getPage(slugs.slice(0, -1)${i18n ? ", params.lang" : ""});
|
|
52
|
+
if (!page) throw new Response(undefined, { status: 404 });
|
|
53
|
+
|
|
54
|
+
return ${image(engine)};
|
|
55
|
+
}
|
|
56
|
+
`,
|
|
57
|
+
"tanstack-start": (route, engine, i18n, src) => `import { createFileRoute } from '@tanstack/react-router';
|
|
58
|
+
import { ${src.ref} } from '@/lib/source';
|
|
59
|
+
${imports(engine)}
|
|
60
|
+
|
|
61
|
+
export const Route = createFileRoute('${route.path}')({
|
|
62
|
+
server: {
|
|
63
|
+
handlers: {
|
|
64
|
+
GET: async ({ params }) => {
|
|
65
|
+
const slugs = params._splat?.split('/') ?? [];
|
|
66
|
+
const page = ${src.resolved}.getPage(slugs.slice(0, -1)${i18n ? ", params.lang" : ""});
|
|
67
|
+
if (!page) return new Response(undefined, { status: 404 });
|
|
68
|
+
|
|
69
|
+
return ${image(engine).replaceAll("\n ", "\n ")};
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
`,
|
|
75
|
+
waku: (route, engine, i18n, src) => `import { ${src.ref} } from '@/lib/source';
|
|
76
|
+
import type { ApiContext } from 'waku/router';
|
|
77
|
+
${imports(engine)}
|
|
78
|
+
|
|
79
|
+
export async function GET(_: Request, { params }: ApiContext<'${route.path}'>) {
|
|
80
|
+
const page = ${src.resolved}.getPage(params.slug${i18n ? ", params.lang" : ""});
|
|
81
|
+
if (!page) return new Response(undefined, { status: 404 });
|
|
82
|
+
|
|
83
|
+
return ${image(engine)};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function getConfig() {
|
|
87
|
+
return {
|
|
88
|
+
render: 'static' as const,
|
|
89
|
+
staticPaths: ${src.resolved}.generateParams().map((item) => ${i18n ? "[item.lang, ...item.slug]" : "item.slug"}),
|
|
90
|
+
} as const;
|
|
91
|
+
}
|
|
92
|
+
`
|
|
93
|
+
};
|
|
94
|
+
const og = {
|
|
95
|
+
id: "og",
|
|
96
|
+
title: "OG Image",
|
|
97
|
+
description: "generate Open Graph images for docs pages",
|
|
98
|
+
requires: [docs],
|
|
99
|
+
options: { engine: {
|
|
100
|
+
message: "Choose an image engine",
|
|
101
|
+
choices: [{
|
|
102
|
+
value: "takumi",
|
|
103
|
+
label: "Takumi",
|
|
104
|
+
hint: "outputs WebP, framework-agnostic"
|
|
105
|
+
}, {
|
|
106
|
+
value: "next-og",
|
|
107
|
+
label: "next/og",
|
|
108
|
+
hint: "Next.js built-in solution"
|
|
109
|
+
}]
|
|
110
|
+
} },
|
|
111
|
+
async apply(ctx, { engine }) {
|
|
112
|
+
const { baseDir, configFile, i18n, source } = ctx.project;
|
|
113
|
+
const framework = reactFramework(ctx.project);
|
|
114
|
+
if (engine === "next-og" && framework !== "next") throw new Error("next/og is only available on Next.js");
|
|
115
|
+
if (engine === "takumi") ctx.addDependencies({ "takumi-js": null });
|
|
116
|
+
const shared = path.join(baseDir, "lib/shared.ts");
|
|
117
|
+
const imageRoute = await sharedRoute(ctx, "docsImageRoute", `/og${source.baseUrl.replace(/\/$/, "")}`);
|
|
118
|
+
if (!await addPageUrl(ctx, "getPageImageUrl", imageUrl(ext(engine), i18n !== null)) && engine === "takumi") await ctx.source(shared, (file) => {
|
|
119
|
+
file.s.replaceAll("image.png", "image.webp");
|
|
120
|
+
});
|
|
121
|
+
const route = formatRoute({
|
|
122
|
+
segments: [imageRoute, {
|
|
123
|
+
param: "slug",
|
|
124
|
+
catchAll: true,
|
|
125
|
+
suffix: `image.${ext(engine)}`
|
|
126
|
+
}],
|
|
127
|
+
locale: true
|
|
128
|
+
}, framework, i18n, "tsx");
|
|
129
|
+
await ctx.write(path.join(baseDir, route.file), templates[framework](route, engine, i18n !== null, sourceRef(ctx.project.source.dynamic)));
|
|
130
|
+
if (framework === "react-router") await registerReactRouterRoutes(ctx, [route]);
|
|
131
|
+
if (framework === "next" && engine === "takumi" && configFile) await ctx.source(configFile, (file) => {
|
|
132
|
+
const config = getConfigObject(file);
|
|
133
|
+
if (config && !getProperty(config, "serverExternalPackages")) addElements(file, config, ["serverExternalPackages: ['@takumi-rs/core']"]);
|
|
134
|
+
});
|
|
135
|
+
if (framework === "next" && i18n) await extendNextProxy(ctx, route);
|
|
136
|
+
ctx.note("The image URL of a page is `getPageImageUrl(page).url` from '@/lib/shared', reference it in the page metadata (e.g. `og:image`).");
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
//#endregion
|
|
140
|
+
export { og as t };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=og-DRDOyQ_t.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"og-DRDOyQ_t.js","names":[],"sources":["../src/features/og.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Feature } from '@/features';\nimport type { ReactFramework } from '@/project';\nimport { type FormattedRoute, formatRoute, reactRouterTypes } from '@/project/route';\nimport { addElements, getConfigObject, getProperty } from '@/codemod';\nimport { docs } from './docs';\nimport { extendNextProxy } from './llms';\nimport {\n addPageUrl,\n reactFramework,\n registerReactRouterRoutes,\n sharedRoute,\n type SourceRef,\n sourceRef,\n} from './utils';\n\ntype Engine = 'takumi' | 'next-og';\n\n/** the image URL of a page, `/og/docs/<slugs>/image.png` */\nconst imageUrl = (ext: string, i18n: boolean) => `\nconst getImageUrl = createGetUrl(docsImageRoute${i18n ? ', i18n' : ''});\n\nexport function getPageImageUrl(page: { slugs: string[]; locale?: string }) {\n const segments = [...page.slugs, 'image.${ext}'];\n\n return { segments, url: getImageUrl(segments, page.locale) };\n}`;\n\nconst imports = (engine: Engine) =>\n `import { generateOGImage } from 'fumadocs-ui/og${engine === 'takumi' ? '/takumi' : ''}';`;\n\nconst image = (engine: Engine) => `generateOGImage({\n title: page.data.title,\n description: page.data.description,\n site: 'My App',${engine === 'takumi' ? \"\\n format: 'webp',\" : ''}\n })`;\n\nconst ext = (engine: Engine) => (engine === 'takumi' ? 'webp' : 'png');\n\ntype Template = (route: FormattedRoute, engine: Engine, i18n: boolean, src: SourceRef) => string;\n\nconst templates: Record<ReactFramework, Template> = {\n next: (route, engine, i18n, src) => `import { ${src.ref} } from '@/lib/source';\nimport { notFound } from 'next/navigation';\n${imports(engine)}\n\nexport const revalidate = false;\n\nexport async function GET(_req: Request, { params }: RouteContext<'${route.path}'>) {\n const { slug${i18n ? ', lang' : ''} } = await params;\n const page = ${src.resolved}.getPage(slug.slice(0, -1)${i18n ? ', lang' : ''});\n if (!page) notFound();\n\n return ${image(engine)};\n}\n\nexport ${src.dynamic ? 'async ' : ''}function generateStaticParams() {\n return ${src.resolved}.generateParams().map((item) => ({\n ...item,\n slug: [...item.slug, 'image.${ext(engine)}'],\n }));\n}\n`,\n 'react-router': (route, engine, i18n, src) => `${reactRouterTypes(route)}\nimport { ${src.ref} } from '@/lib/source';\n${imports(engine)}\n\nexport async function loader({ params }: Route.LoaderArgs) {\n const slugs = params['*'].split('/').filter((v) => v.length > 0);\n const page = ${src.resolved}.getPage(slugs.slice(0, -1)${i18n ? ', params.lang' : ''});\n if (!page) throw new Response(undefined, { status: 404 });\n\n return ${image(engine)};\n}\n`,\n 'tanstack-start': (\n route,\n engine,\n i18n,\n src,\n ) => `import { createFileRoute } from '@tanstack/react-router';\nimport { ${src.ref} } from '@/lib/source';\n${imports(engine)}\n\nexport const Route = createFileRoute('${route.path}')({\n server: {\n handlers: {\n GET: async ({ params }) => {\n const slugs = params._splat?.split('/') ?? [];\n const page = ${src.resolved}.getPage(slugs.slice(0, -1)${i18n ? ', params.lang' : ''});\n if (!page) return new Response(undefined, { status: 404 });\n\n return ${image(engine).replaceAll('\\n ', '\\n ')};\n },\n },\n },\n});\n`,\n waku: (route, engine, i18n, src) => `import { ${src.ref} } from '@/lib/source';\nimport type { ApiContext } from 'waku/router';\n${imports(engine)}\n\nexport async function GET(_: Request, { params }: ApiContext<'${route.path}'>) {\n const page = ${src.resolved}.getPage(params.slug${i18n ? ', params.lang' : ''});\n if (!page) return new Response(undefined, { status: 404 });\n\n return ${image(engine)};\n}\n\nexport async function getConfig() {\n return {\n render: 'static' as const,\n staticPaths: ${src.resolved}.generateParams().map((item) => ${i18n ? '[item.lang, ...item.slug]' : 'item.slug'}),\n } as const;\n}\n`,\n};\n\nexport const og: Feature<{ engine: Engine }> = {\n id: 'og',\n title: 'OG Image',\n description: 'generate Open Graph images for docs pages',\n requires: [docs],\n options: {\n engine: {\n message: 'Choose an image engine',\n choices: [\n { value: 'takumi', label: 'Takumi', hint: 'outputs WebP, framework-agnostic' },\n { value: 'next-og', label: 'next/og', hint: 'Next.js built-in solution' },\n ],\n },\n },\n async apply(ctx, { engine }) {\n const { baseDir, configFile, i18n, source } = ctx.project;\n const framework = reactFramework(ctx.project);\n if (engine === 'next-og' && framework !== 'next')\n throw new Error('next/og is only available on Next.js');\n if (engine === 'takumi') ctx.addDependencies({ 'takumi-js': null });\n\n const shared = path.join(baseDir, 'lib/shared.ts');\n const imageRoute = await sharedRoute(\n ctx,\n 'docsImageRoute',\n `/og${source.baseUrl.replace(/\\/$/, '')}`,\n );\n const added = await addPageUrl(ctx, 'getPageImageUrl', imageUrl(ext(engine), i18n !== null));\n if (!added && engine === 'takumi') {\n await ctx.source(shared, (file) => {\n file.s.replaceAll('image.png', 'image.webp');\n });\n }\n\n const route = formatRoute(\n {\n segments: [imageRoute, { param: 'slug', catchAll: true, suffix: `image.${ext(engine)}` }],\n locale: true,\n },\n framework,\n i18n,\n 'tsx',\n );\n await ctx.write(\n path.join(baseDir, route.file),\n templates[framework](route, engine, i18n !== null, sourceRef(ctx.project.source.dynamic)),\n );\n\n if (framework === 'react-router') await registerReactRouterRoutes(ctx, [route]);\n if (framework === 'next' && engine === 'takumi' && configFile) {\n await ctx.source(configFile, (file) => {\n const config = getConfigObject(file);\n if (config && !getProperty(config, 'serverExternalPackages'))\n addElements(file, config, [\"serverExternalPackages: ['@takumi-rs/core']\"]);\n });\n }\n if (framework === 'next' && i18n) await extendNextProxy(ctx, route);\n\n ctx.note(\n \"The image URL of a page is `getPageImageUrl(page).url` from '@/lib/shared', reference it in the page metadata (e.g. `og:image`).\",\n );\n },\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as Project, c as detectFramework, d as loadProject, i as PackageManager, l as frameworks, n as FrameworkInfo, o as ReactFramework, r as I18nInfo, s as SourceInfo, t as Framework, u as isSupportedFramework } from "../index-BCVGQxn9.js";
|
|
2
|
+
export { Framework, FrameworkInfo, I18nInfo, PackageManager, Project, ReactFramework, SourceInfo, detectFramework, frameworks, isSupportedFramework, loadProject };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { t as exists } from "./fs-D6J9dOEx.js";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { detectFramework as detectFramework$1, detectPackageManager } from "fuma-cli/detect";
|
|
5
|
+
//#region src/project/index.ts
|
|
6
|
+
const frameworks = {
|
|
7
|
+
next: {
|
|
8
|
+
provider: "fumadocs-ui/provider/next",
|
|
9
|
+
rootFile: "app/layout.tsx",
|
|
10
|
+
routesDir: "app",
|
|
11
|
+
configFiles: [
|
|
12
|
+
"next.config.mjs",
|
|
13
|
+
"next.config.ts",
|
|
14
|
+
"next.config.js"
|
|
15
|
+
],
|
|
16
|
+
isStatic: (config) => /output:\s*['"]export['"]/.test(config)
|
|
17
|
+
},
|
|
18
|
+
"react-router": {
|
|
19
|
+
provider: "fumadocs-ui/provider/react-router",
|
|
20
|
+
rootFile: "root.tsx",
|
|
21
|
+
routesDir: "routes",
|
|
22
|
+
configFiles: ["vite.config.ts"]
|
|
23
|
+
},
|
|
24
|
+
"tanstack-start": {
|
|
25
|
+
provider: "fumadocs-ui/provider/tanstack",
|
|
26
|
+
rootFile: "routes/__root.tsx",
|
|
27
|
+
routesDir: "routes",
|
|
28
|
+
configFiles: ["vite.config.ts"],
|
|
29
|
+
isStatic: (config) => /\bspa:\s*\{/.test(config)
|
|
30
|
+
},
|
|
31
|
+
waku: {
|
|
32
|
+
provider: "fumadocs-ui/provider/waku",
|
|
33
|
+
rootFile: "pages/_root.tsx",
|
|
34
|
+
routesDir: "pages",
|
|
35
|
+
configFiles: ["waku.config.ts"]
|
|
36
|
+
},
|
|
37
|
+
astro: {
|
|
38
|
+
provider: "fumadocs-ui/provider/astro",
|
|
39
|
+
rootFile: "layouts/Layout.astro",
|
|
40
|
+
routesDir: "pages",
|
|
41
|
+
configFiles: ["astro.config.mjs", "astro.config.ts"]
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function isSupportedFramework(v) {
|
|
45
|
+
return v in frameworks;
|
|
46
|
+
}
|
|
47
|
+
async function loadProject(config, cwd = process.cwd(), framework = assertFramework(config.framework)) {
|
|
48
|
+
const info = frameworks[framework];
|
|
49
|
+
const [packageJson, pm] = await Promise.all([fs.readFile(path.join(cwd, "package.json"), "utf-8").then((content) => JSON.parse(content)).catch(() => ({})), detectPackageManager({ cwd })]);
|
|
50
|
+
let configFile;
|
|
51
|
+
let configContent;
|
|
52
|
+
for (const file of info.configFiles) if (await exists(path.join(cwd, file))) {
|
|
53
|
+
configFile = file;
|
|
54
|
+
configContent = await fs.readFile(path.join(cwd, file), "utf-8");
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
let isStatic = false;
|
|
58
|
+
if (framework === "react-router") {
|
|
59
|
+
const content = await fs.readFile(path.join(cwd, "react-router.config.ts"), "utf-8").catch(() => "");
|
|
60
|
+
isStatic = /ssr:\s*false/.test(content);
|
|
61
|
+
} else if (configContent && info.isStatic) isStatic = info.isStatic(configContent);
|
|
62
|
+
const read = (file) => fs.readFile(path.join(cwd, file), "utf-8").catch(() => null);
|
|
63
|
+
const i18nConfig = await read(path.join(config.baseDir, "lib/i18n.ts"));
|
|
64
|
+
return {
|
|
65
|
+
cwd,
|
|
66
|
+
framework,
|
|
67
|
+
info,
|
|
68
|
+
static: isStatic,
|
|
69
|
+
i18n: i18nConfig === null ? null : { optionalLocale: /hideLocale:\s*['"]default-locale['"]/.test(i18nConfig) },
|
|
70
|
+
packageManager: pm?.name ?? "npm",
|
|
71
|
+
baseDir: config.baseDir,
|
|
72
|
+
config,
|
|
73
|
+
configFile,
|
|
74
|
+
packageJson,
|
|
75
|
+
source: await detectSource(cwd, config.baseDir, read)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function detectSource(cwd, baseDir, read) {
|
|
79
|
+
const sourceFile = path.join(baseDir, "lib/source.ts");
|
|
80
|
+
const source = await read(sourceFile) ?? "";
|
|
81
|
+
let collections = null;
|
|
82
|
+
let content = source;
|
|
83
|
+
if (source.includes("defineDocs(")) collections = sourceFile;
|
|
84
|
+
else if (await exists(path.join(cwd, "source.config.ts"))) {
|
|
85
|
+
collections = "source.config.ts";
|
|
86
|
+
content = await read(collections) ?? "";
|
|
87
|
+
}
|
|
88
|
+
let baseUrl = /baseUrl:\s*['"]([^'"]*)['"]/.exec(source)?.[1];
|
|
89
|
+
const ref = /baseUrl:\s*(\w+)/.exec(source)?.[1];
|
|
90
|
+
if (!baseUrl && ref) {
|
|
91
|
+
const shared = await read(path.join(baseDir, "lib/shared.ts")) ?? "";
|
|
92
|
+
baseUrl = new RegExp(`${ref}\\s*=\\s*['"]([^'"]*)['"]`).exec(shared)?.[1];
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
loader: /export const source\b/.test(source),
|
|
96
|
+
dynamic: /export (async )?function getSource\b/.test(source),
|
|
97
|
+
collections,
|
|
98
|
+
async: /async:\s*true/.test(content),
|
|
99
|
+
baseUrl: baseUrl ?? "/docs",
|
|
100
|
+
dir: /dir:\s*['"]([^'"]*)['"]/.exec(content)?.[1] ?? "content/docs"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function assertFramework(framework) {
|
|
104
|
+
if (isSupportedFramework(framework)) return framework;
|
|
105
|
+
throw new Error(`Unsupported framework "${framework}", supported: ${Object.keys(frameworks).join(", ")}.`);
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
export { loadProject as i, frameworks as n, isSupportedFramework as r, detectFramework$1 as t };
|
|
109
|
+
|
|
110
|
+
//# sourceMappingURL=project-DwRVTm4p.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-DwRVTm4p.js","names":[],"sources":["../src/project/index.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { detectFramework, detectPackageManager } from 'fuma-cli/detect';\nimport type { LoadedConfig } from '@/config';\nimport { exists } from '@/utils/fs';\n\nexport type PackageManager = NonNullable<Awaited<ReturnType<typeof detectPackageManager>>>['name'];\n\nexport type Framework = 'next' | 'react-router' | 'tanstack-start' | 'waku' | 'astro';\n/** frameworks supported by features that generate routes */\nexport type ReactFramework = Exclude<Framework, 'astro'>;\n\nexport interface FrameworkInfo {\n /** import specifier of `RootProvider` */\n provider: string;\n /** root file that imports the global CSS, relative to base dir */\n rootFile: string;\n /** routes directory, relative to base dir */\n routesDir: string;\n /** config file candidates (relative to cwd) that register bundler plugins */\n configFiles: string[];\n /** how to detect `static` mode from the config file */\n isStatic?: (config: string) => boolean;\n}\n\nexport const frameworks: Record<Framework, FrameworkInfo> = {\n next: {\n provider: 'fumadocs-ui/provider/next',\n rootFile: 'app/layout.tsx',\n routesDir: 'app',\n configFiles: ['next.config.mjs', 'next.config.ts', 'next.config.js'],\n isStatic: (config) => /output:\\s*['\"]export['\"]/.test(config),\n },\n 'react-router': {\n provider: 'fumadocs-ui/provider/react-router',\n rootFile: 'root.tsx',\n routesDir: 'routes',\n configFiles: ['vite.config.ts'],\n },\n 'tanstack-start': {\n provider: 'fumadocs-ui/provider/tanstack',\n rootFile: 'routes/__root.tsx',\n routesDir: 'routes',\n configFiles: ['vite.config.ts'],\n isStatic: (config) => /\\bspa:\\s*\\{/.test(config),\n },\n waku: {\n provider: 'fumadocs-ui/provider/waku',\n rootFile: 'pages/_root.tsx',\n routesDir: 'pages',\n configFiles: ['waku.config.ts'],\n },\n astro: {\n provider: 'fumadocs-ui/provider/astro',\n rootFile: 'layouts/Layout.astro',\n routesDir: 'pages',\n configFiles: ['astro.config.mjs', 'astro.config.ts'],\n },\n};\n\n/** how docs content is loaded, detected from `lib/source.ts` */\nexport interface SourceInfo {\n /** `lib/source.ts` exports a `source` loader */\n loader: boolean;\n /** `lib/source.ts` exports `getSource()`, a runtime content source resolved on demand */\n dynamic: boolean;\n /** file defining the Fumadocs MDX collections, relative to cwd: `lib/source.ts` (macro) or `source.config.ts`; `null` for other content sources */\n collections: string | null;\n /** collections are loaded lazily, `structuredData` is a function */\n async: boolean;\n /** base URL of docs pages */\n baseUrl: string;\n /** content directory */\n dir: string;\n}\n\nexport interface I18nInfo {\n /** the default locale is hidden from URLs (`hideLocale: 'default-locale'`) */\n optionalLocale: boolean;\n}\n\nexport interface Project {\n cwd: string;\n framework: Framework;\n info: FrameworkInfo;\n /** prerendered to static HTML, no server at runtime */\n static: boolean;\n /** `lib/i18n.ts` exists */\n i18n: I18nInfo | null;\n source: SourceInfo;\n packageManager: PackageManager;\n /** directory of app code, relative to `cwd` */\n baseDir: string;\n config: LoadedConfig;\n /** config file that registers bundler plugins, relative to `cwd` */\n configFile?: string;\n packageJson: {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n}\n\nexport function isSupportedFramework(v: string): v is Framework {\n return v in frameworks;\n}\n\nexport async function loadProject(\n config: LoadedConfig,\n cwd = process.cwd(),\n framework: Framework = assertFramework(config.framework),\n): Promise<Project> {\n const info = frameworks[framework];\n const [packageJson, pm] = await Promise.all([\n fs\n .readFile(path.join(cwd, 'package.json'), 'utf-8')\n .then((content) => JSON.parse(content))\n .catch(() => ({})),\n detectPackageManager({ cwd }),\n ]);\n let configFile: string | undefined;\n let configContent: string | undefined;\n for (const file of info.configFiles) {\n if (await exists(path.join(cwd, file))) {\n configFile = file;\n configContent = await fs.readFile(path.join(cwd, file), 'utf-8');\n break;\n }\n }\n\n let isStatic = false;\n if (framework === 'react-router') {\n const content = await fs\n .readFile(path.join(cwd, 'react-router.config.ts'), 'utf-8')\n .catch(() => '');\n isStatic = /ssr:\\s*false/.test(content);\n } else if (configContent && info.isStatic) {\n isStatic = info.isStatic(configContent);\n }\n\n const read = (file: string) => fs.readFile(path.join(cwd, file), 'utf-8').catch(() => null);\n const i18nConfig = await read(path.join(config.baseDir, 'lib/i18n.ts'));\n\n return {\n cwd,\n framework,\n info,\n static: isStatic,\n i18n:\n i18nConfig === null\n ? null\n : { optionalLocale: /hideLocale:\\s*['\"]default-locale['\"]/.test(i18nConfig) },\n packageManager: pm?.name ?? 'npm',\n baseDir: config.baseDir,\n config,\n configFile,\n packageJson,\n source: await detectSource(cwd, config.baseDir, read),\n };\n}\n\nasync function detectSource(\n cwd: string,\n baseDir: string,\n read: (file: string) => Promise<string | null>,\n): Promise<SourceInfo> {\n const sourceFile = path.join(baseDir, 'lib/source.ts');\n const source = (await read(sourceFile)) ?? '';\n let collections: string | null = null;\n let content = source;\n if (source.includes('defineDocs(')) {\n collections = sourceFile;\n } else if (await exists(path.join(cwd, 'source.config.ts'))) {\n collections = 'source.config.ts';\n content = (await read(collections)) ?? '';\n }\n\n let baseUrl = /baseUrl:\\s*['\"]([^'\"]*)['\"]/.exec(source)?.[1];\n // e.g. `baseUrl: docsRoute` from `lib/shared.ts`\n const ref = /baseUrl:\\s*(\\w+)/.exec(source)?.[1];\n if (!baseUrl && ref) {\n const shared = (await read(path.join(baseDir, 'lib/shared.ts'))) ?? '';\n baseUrl = new RegExp(`${ref}\\\\s*=\\\\s*['\"]([^'\"]*)['\"]`).exec(shared)?.[1];\n }\n\n return {\n loader: /export const source\\b/.test(source),\n dynamic: /export (async )?function getSource\\b/.test(source),\n collections,\n async: /async:\\s*true/.test(content),\n baseUrl: baseUrl ?? '/docs',\n dir: /dir:\\s*['\"]([^'\"]*)['\"]/.exec(content)?.[1] ?? 'content/docs',\n };\n}\n\nfunction assertFramework(framework: string): Framework {\n if (isSupportedFramework(framework)) return framework;\n throw new Error(\n `Unsupported framework \"${framework}\", supported: ${Object.keys(frameworks).join(', ')}.`,\n );\n}\n\nexport { detectFramework };\n"],"mappings":"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"}
|