Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// @ts-ignore
import { apiAnchor } from "@tsed/vitepress-theme/markdown/api-anchor/api-anchor.js";
import {apiAnchor} from "@tsed/vitepress-theme/markdown/api-anchor/api-anchor.js";
import llmstxt from "vitepress-plugin-llms";
import { defineConfig } from "vitepress";
import pkg from "../../package.json";
import referenceSidebar from "../public/reference-sidebar.json";
import team from "../team.json";
import {defineConfig} from "vitepress";
import pkg from "../../package.json" with {type: "json"};
import referenceSidebar from "../public/reference-sidebar.json" with {type: "json"};
import team from "../team.json" with {type: "json"};
import {buildLlmContentsPlugin} from "./plugins/buildLlmContents.js";
import {apiLlmLinks} from "./plugins/apiLllmLinks.js";

const sort = (items: {text: string; link: string}[]) => items.sort((a, b) => a.text.localeCompare(b.text));

Expand Down Expand Up @@ -405,9 +407,46 @@ const Releases = [
export default defineConfig({
vite: {
plugins: [
buildLlmContentsPlugin({
sections: [
{
source: "guide",
destination: "public/ai/guides",
label: "Guides"
},
{
source: "introduction",
destination: "public/ai/introduction",
label: "Introduction"
},
{
source: "api",
destination: "public/ai/api",
label: "API references"
}
],
sidebar: {
groups: [
{
text: "Core",
pattern: /core|@tsed\/di|hooks|schema$|\/exceptions$|engines|json-mapper|open-spec/
},
{
text: "Platform",
pattern: /platform/
},
{
text: "ORM",
pattern: /adapters|ioredis|mikro-orm|mongoose|objection|prisma/
}
],
thirdPartyGroup: "Third parties"
}
}),
apiLlmLinks,
llmstxt({
ignoreFilesPerOutput: {
llmsFullTxt: ["api/**"]
llmsTxt: ["api/**"]
}
})
]
Expand Down
13 changes: 13 additions & 0 deletions docs/.vitepress/plugins/apiLllmLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {getApiReferenceLinks} from "./utils/sidebar.js";

export const apiLlmLinks = {
enforce: "pre" as const,
name: "tsed-api-llm-links",
transform(content: string, id: string) {
if (!id.endsWith("/api.md")) {
return null;
}

return content.replace("<!-- API_LLM_LINKS -->", getApiReferenceLinks());
}
};
74 changes: 74 additions & 0 deletions docs/.vitepress/plugins/buildLlmContents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {join} from "node:path";

import {intro, log, outro, spinner} from "@clack/prompts";

import {copyFiles} from "./utils/copy-files.js";
import {buildReferenceSidebar, type ApiSidebarOptions} from "./utils/sidebar.js";

export interface LlmContentSection {
destination: string;
label: string;
source: string;
}

export interface BuildLlmContentsOptions {
docsRoot?: string;
sections?: LlmContentSection[];
sidebar?: ApiSidebarOptions;
}

const DEFAULT_DOCS_ROOT = join(import.meta.dirname, "..", "..");
/**
* Each entry describes a docs directory to copy into /public/ai.
* Markdown is normalized (remark), snippet directives are inlined,
* and @@Symbol@@ tokens are rewritten to /ai/api links.
*/
const DEFAULT_DOC_SECTIONS: LlmContentSection[] = [];
let buildTask: Promise<void> | undefined;

export async function buildLlmContents({
docsRoot = DEFAULT_DOCS_ROOT,
sections = DEFAULT_DOC_SECTIONS,
sidebar
}: BuildLlmContentsOptions = {}) {
intro("Building LLM references");

try {
for (const section of sections) {
const completed = await copyFiles({
cwd: docsRoot,
src: section.source,
dest: section.destination,
label: section.label
});

if (!completed) {
log.warn(`${section.label} copy skipped`);
}
}

const sidebarStep = spinner();
sidebarStep.start("Generating API sidebar");
await buildReferenceSidebar(docsRoot, sidebar);
sidebarStep.stop("Sidebar generated");

outro("LLM references ready");
} catch (error) {
log.error(error instanceof Error ? error.message : String(error));
outro("LLM references build failed");
throw error;
}
}

// Orchestration only; implementation lives in ./llm/*

export function buildLlmContentsPlugin(options: BuildLlmContentsOptions = {}) {
return {
enforce: "pre" as const,
name: "tsed-build-llm-contents",
async configResolved() {
buildTask ??= buildLlmContents(options);
await buildTask;
}
};
}
84 changes: 84 additions & 0 deletions docs/.vitepress/plugins/utils/copy-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {dirname, join} from "node:path";

import {log, progress} from "@clack/prompts";
import fsExtra from "fs-extra";
import {globby} from "globby";

import {transformMarkdown} from "./markdown.js";

const {copy, ensureDir, pathExists, readFile, remove, writeFile} = fsExtra;

export interface CopyFilesOptions {
cwd: string;
dest: string;
label: string;
src: string;
}

interface CopyFileOptions {
cwd: string;
destinationRoot: string;
progressLogger: Pick<ReturnType<typeof progress>, "advance" | "stop">;
relativePath: string;
sourceRoot: string;
}

export async function copyFiles({cwd, src, dest, label}: CopyFilesOptions) {
const sourceDir = join(cwd, src);
const destinationDir = join(cwd, dest);

if (!(await pathExists(sourceDir))) {
log.warn(`[build-llm-contents] Skip ${label.toLowerCase()}: missing ${sourceDir}`);
return false;
}

await remove(destinationDir).catch(() => undefined);
await ensureDir(destinationDir);

const files = await globby(["**/*"], {cwd: sourceDir, dot: true, onlyFiles: true});

if (files.length === 0) {
log.warn(`[build-llm-contents] No files found to sync for ${label.toLowerCase()}`);
return false;
}

const copyProgress = progress({style: "heavy", max: files.length, size: 40});

copyProgress.start(`Syncing ${label}`);

try {
for (const relativePath of files) {
await copyFile({
cwd,
sourceRoot: sourceDir,
destinationRoot: destinationDir,
relativePath,
progressLogger: copyProgress
});
}
} catch (error) {
copyProgress.stop(`${label} failed`);
throw error;
}

copyProgress.stop(`${label} updated`);
return true;
}

async function copyFile({cwd, sourceRoot, destinationRoot, relativePath, progressLogger}: CopyFileOptions) {
const sourcePath = join(sourceRoot, relativePath);
const destinationPath = join(destinationRoot, relativePath);

await ensureDir(dirname(destinationPath));

if (relativePath.endsWith(".md")) {
const fileContent = await readFile(sourcePath, "utf8");
const cleaned = await transformMarkdown(fileContent, {docsRoot: cwd});
await writeFile(destinationPath, cleaned);
progressLogger.advance(1, `Formatted ${relativePath}`);
return;
}

await copy(sourcePath, destinationPath);
progressLogger.advance(1, `Copied ${relativePath}`);
}
Loading
Loading