mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
160 lines
4.1 KiB
TypeScript
160 lines
4.1 KiB
TypeScript
import fs from "fs";
|
|
import path from "path";
|
|
|
|
export type DashboardReferenceJSDocEntry = {
|
|
appId: string,
|
|
slug: string,
|
|
body: string,
|
|
title?: string,
|
|
description?: string,
|
|
sourcePath: string,
|
|
};
|
|
|
|
const DASHBOARD_SRC_ROOT = path.join("apps", "dashboard", "src");
|
|
|
|
function walkSourceFiles(dir: string): string[] {
|
|
const results: string[] = [];
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (entry.name === "node_modules" || entry.name === ".next") {
|
|
continue;
|
|
}
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
results.push(...walkSourceFiles(fullPath));
|
|
continue;
|
|
}
|
|
if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function parseDashboardReferenceBlock(
|
|
rawComment: string,
|
|
sourcePath: string,
|
|
): DashboardReferenceJSDocEntry | null {
|
|
const lines = rawComment.split("\n").map((line) => line.replace(/^\s*\*?\s?/, ""));
|
|
|
|
let appId: string | null = null;
|
|
let slug: string | null = null;
|
|
let title: string | undefined;
|
|
let description: string | undefined;
|
|
const bodyLines: string[] = [];
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (trimmed.length === 0) {
|
|
if (bodyLines.length > 0) {
|
|
bodyLines.push("");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const refMatch = trimmed.match(/^@dashboardReference\s+([a-z0-9-]+)\/([a-z0-9-]+)\s*$/);
|
|
if (refMatch != null) {
|
|
appId = refMatch[1];
|
|
slug = refMatch[2];
|
|
continue;
|
|
}
|
|
|
|
const titleMatch = trimmed.match(/^@dashboardReferenceTitle\s+(.+)$/);
|
|
if (titleMatch != null) {
|
|
title = titleMatch[1].trim();
|
|
continue;
|
|
}
|
|
|
|
const descriptionMatch = trimmed.match(/^@dashboardReferenceDescription\s+(.+)$/);
|
|
if (descriptionMatch != null) {
|
|
description = descriptionMatch[1].trim();
|
|
continue;
|
|
}
|
|
|
|
if (trimmed.startsWith("@")) {
|
|
continue;
|
|
}
|
|
|
|
bodyLines.push(line.trimEnd());
|
|
}
|
|
|
|
if (appId == null || slug == null) {
|
|
return null;
|
|
}
|
|
|
|
const body = bodyLines.join("\n").trim();
|
|
if (body.length === 0) {
|
|
throw new Error(
|
|
`@dashboardReference ${appId}/${slug} in ${sourcePath} has no prose body. `
|
|
+ "Add markdown paragraphs after the tags in the JSDoc block.",
|
|
);
|
|
}
|
|
|
|
return {
|
|
appId,
|
|
slug,
|
|
body: `${body}\n`,
|
|
title,
|
|
description,
|
|
sourcePath,
|
|
};
|
|
}
|
|
|
|
function extractBlocksFromFile(content: string, sourcePath: string): DashboardReferenceJSDocEntry[] {
|
|
const entries: DashboardReferenceJSDocEntry[] = [];
|
|
let searchFrom = 0;
|
|
|
|
while (searchFrom < content.length) {
|
|
const tagIndex = content.indexOf("@dashboardReference", searchFrom);
|
|
if (tagIndex === -1) {
|
|
break;
|
|
}
|
|
|
|
const blockStart = content.lastIndexOf("/**", tagIndex);
|
|
if (blockStart === -1) {
|
|
searchFrom = tagIndex + 1;
|
|
continue;
|
|
}
|
|
|
|
const blockEnd = content.indexOf("*/", tagIndex);
|
|
if (blockEnd === -1) {
|
|
break;
|
|
}
|
|
|
|
const rawComment = content.slice(blockStart + 3, blockEnd);
|
|
const parsed = parseDashboardReferenceBlock(rawComment, sourcePath);
|
|
if (parsed != null) {
|
|
entries.push(parsed);
|
|
}
|
|
|
|
searchFrom = blockEnd + 2;
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
export function extractDashboardReferenceJSDocs(repoRoot: string): Map<string, DashboardReferenceJSDocEntry> {
|
|
const dashboardSrc = path.join(repoRoot, DASHBOARD_SRC_ROOT);
|
|
const byKey = new Map<string, DashboardReferenceJSDocEntry>();
|
|
|
|
for (const filePath of walkSourceFiles(dashboardSrc)) {
|
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
if (!content.includes("@dashboardReference")) {
|
|
continue;
|
|
}
|
|
|
|
const relativePath = path.relative(repoRoot, filePath);
|
|
for (const entry of extractBlocksFromFile(content, relativePath)) {
|
|
const key = `${entry.appId}/${entry.slug}`;
|
|
const existing = byKey.get(key);
|
|
if (existing != null) {
|
|
throw new Error(
|
|
`Duplicate @dashboardReference for ${key}: ${existing.sourcePath} and ${entry.sourcePath}`,
|
|
);
|
|
}
|
|
byKey.set(key, entry);
|
|
}
|
|
}
|
|
|
|
return byKey;
|
|
}
|