mirror of
https://github.com/jakejarvis/stanza.git
synced 2026-07-16 18:05:58 -04:00
126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
import { create, insert, search } from "@orama/orama";
|
|
import { createFileRoute } from "@tanstack/react-router";
|
|
import type { Module, ModuleMetadata, RegistryIndex } from "@withstanza/schema";
|
|
import { cache } from "react";
|
|
|
|
import { loadRegistryFile } from "@/server/registry-base.server";
|
|
|
|
const moduleSchema = {
|
|
category: "string",
|
|
moduleId: "string",
|
|
label: "string",
|
|
description: "string",
|
|
// Joined text for full-text matching. These fields never travel back to the
|
|
// client — only `metadata[hit.id]` does, which is `ModuleMetadata`-shaped.
|
|
deps: "string",
|
|
envNames: "string",
|
|
envDescriptions: "string",
|
|
} as const;
|
|
|
|
type ModuleIndex = {
|
|
db: ReturnType<typeof create<typeof moduleSchema>>;
|
|
metadata: Map<string, ModuleMetadata>;
|
|
};
|
|
|
|
// React `cache()` dedupes the (async) build across any concurrent callers
|
|
// within the same server render. The returned promise is reused on warm
|
|
// calls so the registry-load + Orama-insert work only runs once.
|
|
const getModuleIndex = cache((): Promise<ModuleIndex> => buildIndex());
|
|
|
|
async function buildIndex(): Promise<ModuleIndex> {
|
|
const index = await loadRegistryFile<RegistryIndex>("index.json");
|
|
const db = create({ schema: moduleSchema });
|
|
const metadata = new Map<string, ModuleMetadata>();
|
|
|
|
// Per-module file missing (shouldn't happen for first-party modules, but
|
|
// be tolerant): fall back to metadata-only indexing via `mod: null`.
|
|
const loaded = await Promise.all(
|
|
index.modules.map(async (meta) => {
|
|
try {
|
|
const mod = await loadRegistryFile<Module>(meta.path);
|
|
return { meta, mod };
|
|
} catch {
|
|
return { meta, mod: null as Module | null };
|
|
}
|
|
}),
|
|
);
|
|
|
|
const inserted = await Promise.all(
|
|
loaded.map(async ({ meta, mod }) => {
|
|
const deps = new Set<string>();
|
|
const envNames = new Set<string>();
|
|
const envDescriptions: string[] = [];
|
|
if (mod) {
|
|
for (const k of Object.keys(mod.dependencies ?? {})) deps.add(k);
|
|
for (const k of Object.keys(mod.devDependencies ?? {})) deps.add(k);
|
|
for (const e of mod.env ?? []) {
|
|
envNames.add(e.name);
|
|
if (e.description) envDescriptions.push(e.description);
|
|
}
|
|
for (const adapter of mod.adapters) {
|
|
for (const k of Object.keys(adapter.dependencies ?? {})) deps.add(k);
|
|
for (const k of Object.keys(adapter.devDependencies ?? {})) deps.add(k);
|
|
for (const e of adapter.env ?? []) {
|
|
envNames.add(e.name);
|
|
if (e.description) envDescriptions.push(e.description);
|
|
}
|
|
}
|
|
}
|
|
|
|
const id = await insert(db, {
|
|
category: meta.category,
|
|
moduleId: meta.id,
|
|
label: meta.label,
|
|
description: meta.description,
|
|
deps: [...deps].join(" "),
|
|
envNames: [...envNames].join(" "),
|
|
envDescriptions: envDescriptions.join(" "),
|
|
});
|
|
return { id, meta };
|
|
}),
|
|
);
|
|
for (const { id, meta } of inserted) metadata.set(id, meta);
|
|
|
|
return { db, metadata };
|
|
}
|
|
|
|
/**
|
|
* `GET /api/search/modules?q=…` — server-side module search. Builds an Orama
|
|
* index from the full module data (deps, env vars, descriptions) so we can
|
|
* match on richer fields than the client ever sees. Only `ModuleMetadata`-
|
|
* shaped hits travel back, which means we can index README bodies later
|
|
* without bloating any client payload.
|
|
*
|
|
* Empty query returns an empty array — the client renders all modules from
|
|
* the registry metadata it already has from the root loader.
|
|
*/
|
|
export const Route = createFileRoute("/api/search/modules")({
|
|
server: {
|
|
handlers: {
|
|
GET: async ({ request }) => {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const q = url.searchParams.get("q") ?? "";
|
|
if (!q) return Response.json({ results: [] });
|
|
|
|
const { db, metadata } = await getModuleIndex();
|
|
const hits = await search(db, { term: q, limit: 50 });
|
|
|
|
const results: ModuleMetadata[] = [];
|
|
for (const hit of hits.hits) {
|
|
const meta = metadata.get(hit.id);
|
|
if (meta) results.push(meta);
|
|
}
|
|
return Response.json({ results });
|
|
} catch (error) {
|
|
console.error("[server-error]", error, { source: "/api/search/modules" });
|
|
return Response.json(
|
|
{ error: "Search failed", code: "MODULES_SEARCH_FAILED" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
},
|
|
},
|
|
},
|
|
});
|