Skip to content
Merged
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
49 changes: 49 additions & 0 deletions apps/loopover-ui/src/lib/docs-source-server-isolation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";

// REGRESSION: docs-source.ts (now docs-source.server.ts) imports fumadocs-mdx's generated
// `collections/server` module, which globs every content/docs/*.mdx file eagerly and depends
// on Node's `path` module. Every docs.*.tsx route's loader used to import it directly --
// TanStack Router route loaders run in the browser on client-side navigations, not just on
// the server, so any in-app click into a docs page (not a hard reload) crashed with
// "path.join is not a function" and rendered the router's error boundary. Fixed by moving the
// fumadocs-mdx lookup behind a createServerFn (docs-source.functions.ts) so the client always
// fetches the result over the wire instead of re-executing the server-only module itself.
//
// docs-source.server.ts isn't imported here: like docs.miner-coding-agent.test.tsx and
// docs.ams-observability-callout.test.tsx, actually evaluating fumadocs-mdx's generated
// `collections/server` module needs the fumadocs-mdx Vite plugin (registered for the app build,
// not this standalone vitest.config.ts), so this is a source-level drift guard instead.

describe("docs.*.tsx routes never import docs-source(.server) directly", () => {
const routesDir = join(process.cwd(), "src/routes");
const outOfScope = new Set([
"docs.fumadocs-spike-api-reference.tsx",
"docs.index.tsx",
"docs.tsx",
]);
const docsRouteFiles = readdirSync(routesDir).filter(
(name) => name.startsWith("docs.") && name.endsWith(".tsx") && !name.endsWith(".test.tsx"),
);
const inScope = docsRouteFiles.filter((name) => !outOfScope.has(name));

it("found the expected set of in-scope docs route files", () => {
expect(inScope.length).toBe(48);
});

it.each(inScope)(
"%s: loads doc metadata via getDocPage, not docs-source.server directly",
(file) => {
const source = readFileSync(join(routesDir, file), "utf8");
expect(source, `${file} should import getDocPage from docs-source.functions`).toContain(
'import { getDocPage } from "@/lib/docs-source.functions";',
);
expect(
source,
`${file} must not import docs-source(.server) directly -- it would ship the ` +
"Node-only fumadocs-mdx lookup into the client bundle and crash on SPA navigation",
).not.toMatch(/from ["']@\/lib\/docs-source(\.server)?["']/);
},
);
});
22 changes: 22 additions & 0 deletions apps/loopover-ui/src/lib/docs-source.functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";

// Server function wrapper around docs-source.server.ts's fumadocs-mdx lookup. Every
// docs.*.tsx route loader calls this instead of importing docs-source.server directly --
// see that file's comment for why. Going through createServerFn keeps the lookup
// server-only on both hard loads and client-side navigations: the client calls this over
// the wire instead of re-executing docs-source.server.ts (and its Node-only dependencies)
// in the browser.
//
// The import of docs-source.server is dynamic (inside the handler) rather than a static
// top-level import: this file itself is a static import in every docs.*.tsx route, and a
// static top-level import here would drag docs-source.server.ts's eager content/docs/*.mdx
// glob into any test that merely imports a docs.*.tsx route module without invoking its
// loader (e.g. docs.miner-coding-agent.test.tsx importing named exports for assertions) --
// vitest.config.ts doesn't register the fumadocs-mdx plugin needed to parse those files.
export const getDocPage = createServerFn({ method: "POST" })
.inputValidator(z.object({ slugs: z.array(z.string()) }))
.handler(async ({ data }) => {
const { getDocPageMeta } = await import("./docs-source.server");
return getDocPageMeta(data.slugs);
});
21 changes: 21 additions & 0 deletions apps/loopover-ui/src/lib/docs-source.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { docs } from "collections/server";
import { loader } from "fumadocs-core/source";

// Server-only (the .server.ts suffix keeps this out of the client bundle, see
// config.server.ts). `collections/server` is fumadocs-mdx's generated, filesystem-backed
// runtime -- it globs every content/docs/*.mdx file eagerly and depends on Node's `path`
// module, which doesn't exist in the browser. TanStack Router route loaders run on both
// the server (hard loads) AND in the browser (client-side navigations), so this module
// must never be imported directly from a route loader -- only through getDocPageMeta()
// below, called via the createServerFn in docs-source.functions.ts, so a client-side
// navigation fetches the result over the wire instead of re-executing this module.
export const docsSource = loader({
baseUrl: "/docs",
source: docs.toFumadocsSource(),
});

export function getDocPageMeta(slugs: string[]) {
const page = docsSource.getPage(slugs);
if (!page) return null;
return { path: page.path, title: page.data.title, description: page.data.description };
}
7 changes: 0 additions & 7 deletions apps/loopover-ui/src/lib/docs-source.ts

This file was deleted.

2 changes: 1 addition & 1 deletion apps/loopover-ui/src/routes/docs-mdx-eager-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { docsMdxComponents } from "@/lib/docs-mdx-components";
// unimported provided-component name there is a bare, unresolved identifier: a raw ReferenceError,
// not fumadocs' friendly message.
//
// That alone would only break the one page. It broke every docs page because docs-source.ts (used by
// That alone would only break the one page. It broke every docs page because docs-source.server.ts (used by
// every docs.*.tsx route's server loader) imports fumadocs-mdx's generated collections/server module,
// which globs *all* content/docs/*.mdx files with `eager: true` -- so one file with this defect throws
// during module evaluation and takes down every docs route's SSR loader, not just its own page.
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ai-summaries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ai-summaries.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ai-summaries")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ai-summaries"]);
const page = await getDocPage({ data: { slugs: ["ai-summaries"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ams-config-precedence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-config-precedence.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ams-config-precedence")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-config-precedence"]);
const page = await getDocPage({ data: { slugs: ["ams-config-precedence"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ams-deployment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-deployment.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ams-deployment")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-deployment"]);
const page = await getDocPage({ data: { slugs: ["ams-deployment"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ams-discovery-plane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-discovery-plane.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ams-discovery-plane")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-discovery-plane"]);
const page = await getDocPage({ data: { slugs: ["ams-discovery-plane"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ams-env-reference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-env-reference.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock primitives --
// not fumadocs-ui's bundled components. See docs-source.ts's comment for why the
// not fumadocs-ui's bundled components. See docs-source.server.ts's comment for why the
// loader below resolves only a plain, serializable path string. The .mdx content
// itself imports AMS_ENV_REFERENCE_MARKDOWN from @/lib/ams-env-reference (generated by
// packages/loopover-miner/scripts/generate-env-reference.mjs, drift-checked in CI),
// the same pattern docs.self-hosting-configuration.tsx uses for
// SELFHOST_ENV_REFERENCE_MARKDOWN.
export const Route = createFileRoute("/docs/ams-env-reference")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-env-reference"]);
const page = await getDocPage({ data: { slugs: ["ams-env-reference"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ams-fleet-manifest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-fleet-manifest.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ams-fleet-manifest")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-fleet-manifest"]);
const page = await getDocPage({ data: { slugs: ["ams-fleet-manifest"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ams-goal-spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-goal-spec.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ams-goal-spec")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-goal-spec"]);
const page = await getDocPage({ data: { slugs: ["ams-goal-spec"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-kill-switch-incident.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ams-kill-switch-incident")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-kill-switch-incident"]);
const page = await getDocPage({ data: { slugs: ["ams-kill-switch-incident"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
} from "../components/site/ams-observability-callout";

// Every route that embeds the shared callout, so a new route add/remove can't silently skip one (#5191).
// These routes render from content/docs/*.mdx via the fumadocs client-loader (see docs-source.ts's
// comment), so this is now a content drift-guard -- checking the .mdx source for the JSX tag -- rather
// These routes render from content/docs/*.mdx via the fumadocs client-loader (see
// docs-source.server.ts's comment), so this is now a content drift-guard -- checking the .mdx source
// for the JSX tag -- rather
// than a component render, matching the pattern in docs-selfhost-activation-paths.test.ts.
const ROUTES_WITH_CALLOUT = [
["/docs/self-hosting-operations", "content/docs/self-hosting-operations.mdx"],
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ams-observability.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-observability.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ams-observability")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-observability"]);
const page = await getDocPage({ data: { slugs: ["ams-observability"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ams-operations-runbook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-operations-runbook.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ams-operations-runbook")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-operations-runbook"]);
const page = await getDocPage({ data: { slugs: ["ams-operations-runbook"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
8 changes: 4 additions & 4 deletions apps/loopover-ui/src/routes/docs.ams-sizing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { Suspense } from "react";
import { DocsPage } from "@/components/site/docs-page";
import { LoadingState } from "@/components/site/state-views";
import { docsClientLoader } from "@/lib/docs-client-loader";
import { getDocPage } from "@/lib/docs-source.functions";

// Rendered from content/docs/ams-sizing.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// primitives -- not fumadocs-ui's bundled components. See docs-source.server.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/ams-sizing")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["ams-sizing"]);
const page = await getDocPage({ data: { slugs: ["ams-sizing"] } });
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
return page;
},
head: () => ({
meta: [
Expand Down
Loading
Loading