@raven-js/glean
Version:
Glean documentation gold from your codebase - JSDoc parsing, validation, and beautiful doc generation
175 lines (149 loc) • 5.93 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* Documentation router creation and configuration.
*
* Creates a Wings router instance with all documentation routes,
* static asset serving, and middleware configured. This is the core
* routing logic extracted for reuse in both server and resolver modes.
*/
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Router } from "@raven-js/wings";
import { Assets, Logger } from "@raven-js/wings/server";
import { AssetRegistry, createAssetMiddleware } from "../assets/index.js";
import { discover } from "../discover/index.js";
import { extract } from "../extract/index.js";
import { createEntityPageHandler } from "./routes/entity-page.js";
import { createModuleDirectoryHandler } from "./routes/module-directory.js";
import { createModuleOverviewHandler } from "./routes/module-overview.js";
import { createPackageOverviewHandler } from "./routes/package-overview.js";
import { createSitemapHandler } from "./routes/sitemap.js";
/**
* Creates documentation router instance with routes, static assets, and middleware.
*
* Loads and processes package data once at initialization, then creates Wings router
* with all documentation routes configured. This router can be used directly for
* resolver-based static generation or wrapped in a server for HTTP serving.
*
* @param {string} packagePath - Path to package to document
* @param {Object} options - Configuration options
* @param {string} [options.domain] - Base domain for canonical URLs (optional)
* @param {boolean} [options.enableLogging] - Enable request logging (default: false)
* @returns {Router} Wings router instance ready to handle requests
*
* @example
* // Create documentation router for resolver mode
* const router = createDocumentationRouter('./my-package');
*
* // Use in resolver function
* const resolver = async (path) => {
* const ctx = new Context('GET', new URL(`http://localhost${path}`), new Headers());
* await router.handleRequest(ctx);
* return ctx.toResponse();
* };
*
* @example
* // Create router with custom options
* const router = createDocumentationRouter('./my-package', {
* domain: 'docs.mypackage.com',
* enableLogging: true
* });
*/
export function createDocumentationRouter(packagePath, options = {}) {
const { domain, enableLogging = false } = options;
// Validate required parameters
if (!packagePath || typeof packagePath !== "string") {
throw new Error("packagePath is required and must be a string");
}
// Load package data once at initialization
const packageMetadata = discover(packagePath);
const packageInstance = extract(packageMetadata);
// Validate extracted package data
if (!packageInstance || !packageInstance.name) {
throw new Error(
`Failed to extract valid package data from: ${packagePath}`,
);
}
// Create asset registry and register discovered image assets
const assetRegistry = new AssetRegistry();
// Register package-level image assets (from discovery phase)
if (packageMetadata.imageAssets) {
for (const asset of packageMetadata.imageAssets) {
assetRegistry.register(/** @type {any} */ (asset));
}
}
// Register module-level image assets (from discovery phase)
for (const module of packageMetadata.modules) {
if (module.imageAssets) {
for (const asset of module.imageAssets) {
assetRegistry.register(/** @type {any} */ (asset));
}
}
}
// Create Wings router instance
const router = new Router();
// Add Wings middlewares
// Optional request logging middleware
if (enableLogging) {
router.useEarly(new Logger());
}
// Static assets middleware (serves from glean package static/ directory)
// Calculate absolute path to static directory within glean package
const currentFileUrl = import.meta.url;
const currentFilePath = fileURLToPath(currentFileUrl);
const currentDir = path.dirname(currentFilePath);
// Different paths for development vs production (bundled)
// Development: lib/server/router.js -> ../../static
// Production: bin/glean.min.js -> ../static
const isBundled =
currentFilePath.includes("bin/") && currentFilePath.includes(".min.js");
const staticDir = path.resolve(
currentDir,
isBundled ? "../static" : "../../static",
);
router.use(new Assets({ assetsDir: staticDir }));
// Register asset serving route for local image assets
router.get(
"/assets/:filename",
/** @type {any} */ (createAssetMiddleware(assetRegistry)),
);
// Create route handlers with package data and asset registry
const packageOverviewHandler = createPackageOverviewHandler(
packageInstance,
assetRegistry,
);
const moduleDirectoryHandler = createModuleDirectoryHandler(packageInstance);
const moduleOverviewHandler = createModuleOverviewHandler(
packageInstance,
assetRegistry,
);
const entityPageHandler = createEntityPageHandler(packageInstance);
const sitemapHandler = createSitemapHandler(packageInstance, {
baseUrl: domain ? `https://${domain}` : "https://docs.example.com",
});
// Register all documentation routes
// Package overview route (homepage)
router.get("/", /** @type {any} */ (packageOverviewHandler));
// Module directory route (all modules overview)
router.get("/modules/", /** @type {any} */ (moduleDirectoryHandler));
// Module overview route (specific module documentation)
router.get(
"/modules/:moduleName/",
/** @type {any} */ (moduleOverviewHandler),
);
// Entity documentation route (specific API documentation)
router.get(
"/modules/:moduleName/:entityName/",
/** @type {any} */ (entityPageHandler),
);
// Sitemap route (SEO optimization)
router.get("/sitemap.xml", /** @type {any} */ (sitemapHandler));
// Wings router handles 404s automatically when no routes match
return router;
}