@btst/stack 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.cts +2 -2
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.d.ts +2 -2
- package/dist/client/index.cjs +6 -2
- package/dist/client/index.d.cts +2 -1
- package/dist/client/index.d.mts +2 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.mjs +6 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packages/better-stack/src/plugins/route-docs/client/components/loading/docs-skeleton.cjs +43 -0
- package/dist/packages/better-stack/src/plugins/route-docs/client/components/loading/docs-skeleton.mjs +41 -0
- package/dist/packages/better-stack/src/plugins/route-docs/client/components/pages/docs-page.cjs +794 -0
- package/dist/packages/better-stack/src/plugins/route-docs/client/components/pages/docs-page.mjs +788 -0
- package/dist/packages/better-stack/src/plugins/route-docs/client/plugin.cjs +111 -0
- package/dist/packages/better-stack/src/plugins/route-docs/client/plugin.mjs +106 -0
- package/dist/packages/better-stack/src/plugins/route-docs/generator.cjs +244 -0
- package/dist/packages/better-stack/src/plugins/route-docs/generator.mjs +227 -0
- package/dist/packages/ui/src/components/sheet.cjs +25 -0
- package/dist/packages/ui/src/components/sheet.mjs +24 -1
- package/dist/plugins/api/index.d.cts +2 -2
- package/dist/plugins/api/index.d.mts +2 -2
- package/dist/plugins/api/index.d.ts +2 -2
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/client/index.d.cts +2 -2
- package/dist/plugins/client/index.d.mts +2 -2
- package/dist/plugins/client/index.d.ts +2 -2
- package/dist/plugins/open-api/api/index.d.cts +1 -1
- package/dist/plugins/open-api/api/index.d.mts +1 -1
- package/dist/plugins/open-api/api/index.d.ts +1 -1
- package/dist/plugins/route-docs/client/index.cjs +10 -0
- package/dist/plugins/route-docs/client/index.d.cts +126 -0
- package/dist/plugins/route-docs/client/index.d.mts +126 -0
- package/dist/plugins/route-docs/client/index.d.ts +126 -0
- package/dist/plugins/route-docs/client/index.mjs +1 -0
- package/dist/plugins/route-docs/client.css +3 -0
- package/dist/plugins/route-docs/style.css +19 -0
- package/dist/shared/{stack.CSce37mX.d.cts → stack.u9iYV6vt.d.cts} +14 -2
- package/dist/shared/{stack.CSce37mX.d.mts → stack.u9iYV6vt.d.mts} +14 -2
- package/dist/shared/{stack.CSce37mX.d.ts → stack.u9iYV6vt.d.ts} +14 -2
- package/package.json +15 -1
- package/src/client/index.ts +11 -4
- package/src/plugins/route-docs/client/components/loading/docs-skeleton.tsx +82 -0
- package/src/plugins/route-docs/client/components/loading/index.tsx +1 -0
- package/src/plugins/route-docs/client/components/pages/docs-page.tsx +1240 -0
- package/src/plugins/route-docs/client/index.ts +7 -0
- package/src/plugins/route-docs/client/plugin.tsx +187 -0
- package/src/plugins/route-docs/client.css +3 -0
- package/src/plugins/route-docs/generator.ts +385 -0
- package/src/plugins/route-docs/index.ts +12 -0
- package/src/plugins/route-docs/style.css +19 -0
- package/src/types.ts +19 -1
- package/dist/shared/{stack.CcI4sYJP.d.mts → stack.DLhzx1-D.d.cts} +1 -1
- package/dist/shared/{stack.CcI4sYJP.d.ts → stack.DLhzx1-D.d.mts} +1 -1
- package/dist/shared/{stack.CcI4sYJP.d.cts → stack.DLhzx1-D.d.ts} +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { lazy } from "react";
|
|
2
|
+
import { defineClientPlugin } from "@btst/stack/plugins/client";
|
|
3
|
+
import { createRoute } from "@btst/yar";
|
|
4
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
5
|
+
import type { ClientStackContext } from "../../../types";
|
|
6
|
+
import {
|
|
7
|
+
generateRouteDocsSchema,
|
|
8
|
+
fetchAllSitemapEntries,
|
|
9
|
+
type RouteDocsSchema,
|
|
10
|
+
} from "../generator";
|
|
11
|
+
import type { DocsPageProps } from "./components/pages/docs-page";
|
|
12
|
+
|
|
13
|
+
// Lazy load page components for code splitting
|
|
14
|
+
const DocsPageComponent = lazy(() =>
|
|
15
|
+
import("./components/pages/docs-page").then((m) => ({
|
|
16
|
+
default: m.DocsPageComponent as React.ComponentType<DocsPageProps>,
|
|
17
|
+
})),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const DocsPageSkeleton = lazy(() =>
|
|
21
|
+
import("./components/loading/docs-skeleton").then((m) => ({
|
|
22
|
+
default: m.DocsPageSkeleton,
|
|
23
|
+
})),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Query key for route docs schema
|
|
28
|
+
*/
|
|
29
|
+
export const ROUTE_DOCS_QUERY_KEY = ["route-docs", "schema"] as const;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Module-level storage for the client stack context
|
|
33
|
+
* This allows the schema to be generated on both server and client
|
|
34
|
+
*/
|
|
35
|
+
let moduleStoredContext: ClientStackContext | null = null;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the stored client stack context
|
|
39
|
+
* Used by the docs page component to generate schema on client-side navigation
|
|
40
|
+
*/
|
|
41
|
+
export function getStoredContext(): ClientStackContext | null {
|
|
42
|
+
return moduleStoredContext;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generate the route docs schema from the stored context
|
|
47
|
+
* This can be called from both server and client
|
|
48
|
+
*/
|
|
49
|
+
export async function generateSchema(): Promise<RouteDocsSchema> {
|
|
50
|
+
if (!moduleStoredContext) {
|
|
51
|
+
return {
|
|
52
|
+
plugins: [],
|
|
53
|
+
generatedAt: new Date().toISOString(),
|
|
54
|
+
allSitemapEntries: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const sitemapEntries = await fetchAllSitemapEntries(moduleStoredContext);
|
|
60
|
+
return generateRouteDocsSchema(moduleStoredContext, sitemapEntries);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn("Failed to generate route docs schema:", error);
|
|
63
|
+
// Return schema without sitemap entries on error
|
|
64
|
+
return generateRouteDocsSchema(moduleStoredContext, []);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Configuration for Route Docs client plugin
|
|
70
|
+
*/
|
|
71
|
+
export interface RouteDocsClientConfig {
|
|
72
|
+
/** React Query client for SSR prefetching */
|
|
73
|
+
queryClient: QueryClient;
|
|
74
|
+
/** Title for the documentation page */
|
|
75
|
+
title?: string;
|
|
76
|
+
/** Description for the documentation page */
|
|
77
|
+
description?: string;
|
|
78
|
+
/** Site base path for constructing URLs (e.g., "/pages") */
|
|
79
|
+
siteBasePath: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create meta generator for the docs page
|
|
84
|
+
*/
|
|
85
|
+
function createDocsMeta(config: RouteDocsClientConfig) {
|
|
86
|
+
return () => {
|
|
87
|
+
const title = config.title || "Route Documentation";
|
|
88
|
+
return [
|
|
89
|
+
{ title },
|
|
90
|
+
{ name: "title", content: title },
|
|
91
|
+
{ name: "robots", content: "noindex" },
|
|
92
|
+
];
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Default error component - no required props, matches other plugins
|
|
98
|
+
*/
|
|
99
|
+
function DocsErrorComponent() {
|
|
100
|
+
return (
|
|
101
|
+
<div className="flex items-center justify-center min-h-screen bg-background">
|
|
102
|
+
<div className="text-center">
|
|
103
|
+
<h1 className="text-2xl font-semibold text-destructive mb-2">
|
|
104
|
+
Error Loading Documentation
|
|
105
|
+
</h1>
|
|
106
|
+
<p className="text-muted-foreground">
|
|
107
|
+
An error occurred while loading the documentation.
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create loader for SSR prefetching of route docs schema
|
|
116
|
+
* This properly awaits all sitemap data before storing in React Query
|
|
117
|
+
*/
|
|
118
|
+
function createRouteDocsLoader(config: RouteDocsClientConfig) {
|
|
119
|
+
return async () => {
|
|
120
|
+
// Only run on server for SSR prefetching
|
|
121
|
+
// Client-side navigation uses the queryFn in the component
|
|
122
|
+
if (typeof window === "undefined" && moduleStoredContext) {
|
|
123
|
+
const { queryClient } = config;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Await all sitemap entries from all plugins
|
|
127
|
+
const sitemapEntries =
|
|
128
|
+
await fetchAllSitemapEntries(moduleStoredContext);
|
|
129
|
+
|
|
130
|
+
// Generate the complete schema with sitemap data
|
|
131
|
+
const schema = generateRouteDocsSchema(
|
|
132
|
+
moduleStoredContext,
|
|
133
|
+
sitemapEntries,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Store in React Query for the component to read
|
|
137
|
+
queryClient.setQueryData<RouteDocsSchema>(ROUTE_DOCS_QUERY_KEY, schema);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.warn("Failed to load route docs schema:", error);
|
|
140
|
+
// Store empty schema on error
|
|
141
|
+
queryClient.setQueryData<RouteDocsSchema>(ROUTE_DOCS_QUERY_KEY, {
|
|
142
|
+
plugins: [],
|
|
143
|
+
generatedAt: new Date().toISOString(),
|
|
144
|
+
allSitemapEntries: [],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Route Docs client plugin
|
|
153
|
+
* Provides a route that displays documentation for all client routes
|
|
154
|
+
*/
|
|
155
|
+
export const routeDocsClientPlugin = (config: RouteDocsClientConfig) => {
|
|
156
|
+
return defineClientPlugin({
|
|
157
|
+
name: "route-docs",
|
|
158
|
+
|
|
159
|
+
routes: (context?: ClientStackContext) => {
|
|
160
|
+
// Store context at module level for client-side schema generation
|
|
161
|
+
moduleStoredContext = context || null;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
docs: createRoute("/route-docs", () => {
|
|
165
|
+
return {
|
|
166
|
+
PageComponent: () => (
|
|
167
|
+
<DocsPageComponent
|
|
168
|
+
title={config.title}
|
|
169
|
+
description={config.description}
|
|
170
|
+
siteBasePath={config.siteBasePath || "/pages"}
|
|
171
|
+
/>
|
|
172
|
+
),
|
|
173
|
+
LoadingComponent: () => <DocsPageSkeleton />,
|
|
174
|
+
ErrorComponent: () => <DocsErrorComponent />,
|
|
175
|
+
loader: createRouteDocsLoader(config),
|
|
176
|
+
meta: createDocsMeta(config),
|
|
177
|
+
};
|
|
178
|
+
}),
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
sitemap: async () => {
|
|
183
|
+
// Route docs page should NOT be in sitemap
|
|
184
|
+
return [];
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
};
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import type { ClientStackContext, SitemapEntry } from "../../types";
|
|
2
|
+
import type { Route } from "@btst/yar";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a documented route parameter
|
|
7
|
+
*/
|
|
8
|
+
export interface RouteParameter {
|
|
9
|
+
name: string;
|
|
10
|
+
type: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
description?: string;
|
|
13
|
+
schema?: Record<string, any>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sitemap entry with plugin source
|
|
18
|
+
*/
|
|
19
|
+
export interface PluginSitemapEntry extends SitemapEntry {
|
|
20
|
+
pluginKey: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Represents a documented route
|
|
25
|
+
*/
|
|
26
|
+
export interface DocumentedRoute {
|
|
27
|
+
/** Route key from the plugin */
|
|
28
|
+
key: string;
|
|
29
|
+
/** The route path pattern (e.g., "/users/:id") */
|
|
30
|
+
path: string;
|
|
31
|
+
/** Path parameters extracted from the path */
|
|
32
|
+
pathParams: RouteParameter[];
|
|
33
|
+
/** Query parameters from the route's query schema */
|
|
34
|
+
queryParams: RouteParameter[];
|
|
35
|
+
/** Route metadata */
|
|
36
|
+
meta?: {
|
|
37
|
+
title?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
tags?: string[];
|
|
40
|
+
[key: string]: any;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Represents a plugin's documented routes
|
|
46
|
+
*/
|
|
47
|
+
export interface DocumentedPlugin {
|
|
48
|
+
/** Plugin key */
|
|
49
|
+
key: string;
|
|
50
|
+
/** Plugin name */
|
|
51
|
+
name: string;
|
|
52
|
+
/** Routes from this plugin */
|
|
53
|
+
routes: DocumentedRoute[];
|
|
54
|
+
/** Sitemap entries from this plugin */
|
|
55
|
+
sitemapEntries: PluginSitemapEntry[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The complete route documentation schema
|
|
60
|
+
*/
|
|
61
|
+
export interface RouteDocsSchema {
|
|
62
|
+
/** All documented plugins */
|
|
63
|
+
plugins: DocumentedPlugin[];
|
|
64
|
+
/** Generation timestamp */
|
|
65
|
+
generatedAt: string;
|
|
66
|
+
/** All sitemap entries aggregated */
|
|
67
|
+
allSitemapEntries: PluginSitemapEntry[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract path parameters from a route path pattern
|
|
72
|
+
* e.g., "/users/:id/posts/:postId" => ["id", "postId"]
|
|
73
|
+
*/
|
|
74
|
+
function extractPathParams(path: string): string[] {
|
|
75
|
+
const params: string[] = [];
|
|
76
|
+
const segments = path.split("/");
|
|
77
|
+
|
|
78
|
+
for (const segment of segments) {
|
|
79
|
+
if (segment.startsWith(":")) {
|
|
80
|
+
params.push(segment.slice(1));
|
|
81
|
+
} else if (segment.startsWith("*:")) {
|
|
82
|
+
// Wildcard with name: /*:splat
|
|
83
|
+
params.push(segment.slice(2));
|
|
84
|
+
} else if (segment === "*") {
|
|
85
|
+
// Anonymous wildcard
|
|
86
|
+
params.push("_");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return params;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the primitive type from a Zod type
|
|
95
|
+
*/
|
|
96
|
+
function getTypeFromZodType(zodType: z.ZodType<any>): string {
|
|
97
|
+
if (zodType instanceof z.ZodString) return "string";
|
|
98
|
+
if (zodType instanceof z.ZodNumber) return "number";
|
|
99
|
+
if (zodType instanceof z.ZodBoolean) return "boolean";
|
|
100
|
+
if (zodType instanceof z.ZodArray) return "array";
|
|
101
|
+
if (zodType instanceof z.ZodObject) return "object";
|
|
102
|
+
if (zodType instanceof z.ZodEnum) return "enum";
|
|
103
|
+
if (zodType instanceof z.ZodLiteral) return "literal";
|
|
104
|
+
if (zodType instanceof z.ZodUnion) return "union";
|
|
105
|
+
|
|
106
|
+
// Fallback based on type property if available
|
|
107
|
+
const type = (zodType as any).type;
|
|
108
|
+
if (type === "string") return "string";
|
|
109
|
+
if (type === "number") return "number";
|
|
110
|
+
if (type === "boolean") return "boolean";
|
|
111
|
+
if (type === "array") return "array";
|
|
112
|
+
if (type === "object") return "object";
|
|
113
|
+
|
|
114
|
+
return "string";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Process a Zod type into a simplified schema
|
|
119
|
+
*/
|
|
120
|
+
function processZodType(zodType: z.ZodType<any>): Record<string, any> {
|
|
121
|
+
// Handle optional - unwrap and process inner type
|
|
122
|
+
if (zodType instanceof z.ZodOptional) {
|
|
123
|
+
const innerType =
|
|
124
|
+
(zodType as any)._def?.innerType || (zodType as any).unwrap?.();
|
|
125
|
+
if (innerType) {
|
|
126
|
+
return processZodType(innerType);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Handle nullable
|
|
131
|
+
if (zodType instanceof z.ZodNullable) {
|
|
132
|
+
const innerType =
|
|
133
|
+
(zodType as any)._def?.innerType || (zodType as any).unwrap?.();
|
|
134
|
+
if (innerType) {
|
|
135
|
+
const innerSchema = processZodType(innerType);
|
|
136
|
+
return {
|
|
137
|
+
...innerSchema,
|
|
138
|
+
nullable: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Handle default - unwrap and process inner type
|
|
144
|
+
if (zodType instanceof z.ZodDefault) {
|
|
145
|
+
const innerType = (zodType as any)._def?.innerType;
|
|
146
|
+
const defaultValue = (zodType as any)._def?.defaultValue?.();
|
|
147
|
+
if (innerType) {
|
|
148
|
+
const innerSchema = processZodType(innerType);
|
|
149
|
+
if (defaultValue !== undefined) {
|
|
150
|
+
return {
|
|
151
|
+
...innerSchema,
|
|
152
|
+
default: defaultValue,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return innerSchema;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Handle object
|
|
160
|
+
if (zodType instanceof z.ZodObject) {
|
|
161
|
+
const shape = (zodType as any).shape || (zodType as any)._def?.shape?.();
|
|
162
|
+
if (shape) {
|
|
163
|
+
const properties: Record<string, any> = {};
|
|
164
|
+
const required: string[] = [];
|
|
165
|
+
|
|
166
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
167
|
+
if (value instanceof z.ZodType) {
|
|
168
|
+
properties[key] = processZodType(value);
|
|
169
|
+
if (!(value instanceof z.ZodOptional)) {
|
|
170
|
+
required.push(key);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties,
|
|
178
|
+
...(required.length > 0 ? { required } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Handle array
|
|
184
|
+
if (zodType instanceof z.ZodArray) {
|
|
185
|
+
const elementType = (zodType as any)._def?.type || (zodType as any).element;
|
|
186
|
+
return {
|
|
187
|
+
type: "array",
|
|
188
|
+
items: elementType ? processZodType(elementType) : { type: "string" },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle enum
|
|
193
|
+
if (zodType instanceof z.ZodEnum) {
|
|
194
|
+
const values = (zodType as any)._def?.values || (zodType as any).options;
|
|
195
|
+
return {
|
|
196
|
+
type: "string",
|
|
197
|
+
enum: values,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Handle literal
|
|
202
|
+
if (zodType instanceof z.ZodLiteral) {
|
|
203
|
+
const value = (zodType as any)._def?.value || (zodType as any).value;
|
|
204
|
+
return {
|
|
205
|
+
type: typeof value,
|
|
206
|
+
const: value,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Handle union
|
|
211
|
+
if (zodType instanceof z.ZodUnion) {
|
|
212
|
+
const options = (zodType as any)._def?.options || (zodType as any).options;
|
|
213
|
+
if (options && Array.isArray(options)) {
|
|
214
|
+
return {
|
|
215
|
+
oneOf: options.map((opt: z.ZodType<any>) => processZodType(opt)),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Handle coerce types
|
|
221
|
+
if ((zodType as any)._def?.coerce) {
|
|
222
|
+
const innerType = (zodType as any)._def?.innerType;
|
|
223
|
+
if (innerType) {
|
|
224
|
+
return processZodType(innerType);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Default to primitive type
|
|
229
|
+
return {
|
|
230
|
+
type: getTypeFromZodType(zodType),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if an object is a StandardSchemaV1 (Zod) schema
|
|
236
|
+
*/
|
|
237
|
+
function isStandardSchema(obj: any): boolean {
|
|
238
|
+
return obj && typeof obj === "object" && "~standard" in obj;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Extract query parameters from a route's query schema
|
|
243
|
+
*/
|
|
244
|
+
function extractQueryParams(querySchema: any): RouteParameter[] {
|
|
245
|
+
const params: RouteParameter[] = [];
|
|
246
|
+
|
|
247
|
+
if (!querySchema) return params;
|
|
248
|
+
|
|
249
|
+
// Handle Zod objects directly
|
|
250
|
+
if (querySchema instanceof z.ZodObject) {
|
|
251
|
+
const shape =
|
|
252
|
+
(querySchema as any).shape || (querySchema as any)._def?.shape?.();
|
|
253
|
+
if (shape) {
|
|
254
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
255
|
+
if (value instanceof z.ZodType) {
|
|
256
|
+
params.push({
|
|
257
|
+
name: key,
|
|
258
|
+
type: getTypeFromZodType(value),
|
|
259
|
+
required: !(value instanceof z.ZodOptional),
|
|
260
|
+
schema: processZodType(value),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Handle StandardSchemaV1 (which Zod implements)
|
|
267
|
+
else if (isStandardSchema(querySchema)) {
|
|
268
|
+
// Try to access the underlying Zod schema
|
|
269
|
+
const zodSchema = querySchema;
|
|
270
|
+
if (zodSchema instanceof z.ZodObject) {
|
|
271
|
+
return extractQueryParams(zodSchema);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return params;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Fetch sitemap data from all plugins
|
|
280
|
+
*/
|
|
281
|
+
export async function fetchAllSitemapEntries(
|
|
282
|
+
context: ClientStackContext,
|
|
283
|
+
): Promise<PluginSitemapEntry[]> {
|
|
284
|
+
const allEntries: PluginSitemapEntry[] = [];
|
|
285
|
+
|
|
286
|
+
for (const [pluginKey, plugin] of Object.entries(context.plugins)) {
|
|
287
|
+
// Skip route-docs plugin
|
|
288
|
+
if (pluginKey === "routeDocs" || plugin.name === "route-docs") {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (plugin.sitemap) {
|
|
293
|
+
try {
|
|
294
|
+
const entries = await plugin.sitemap();
|
|
295
|
+
for (const entry of entries) {
|
|
296
|
+
allEntries.push({
|
|
297
|
+
...entry,
|
|
298
|
+
pluginKey,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.warn(`Failed to fetch sitemap for plugin ${pluginKey}:`, error);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return allEntries;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Generate route documentation schema from client stack context
|
|
312
|
+
*/
|
|
313
|
+
export function generateRouteDocsSchema(
|
|
314
|
+
context: ClientStackContext,
|
|
315
|
+
sitemapEntries: PluginSitemapEntry[] = [],
|
|
316
|
+
): RouteDocsSchema {
|
|
317
|
+
const documentedPlugins: DocumentedPlugin[] = [];
|
|
318
|
+
|
|
319
|
+
// Group sitemap entries by plugin
|
|
320
|
+
const sitemapByPlugin: Record<string, PluginSitemapEntry[]> = {};
|
|
321
|
+
for (const entry of sitemapEntries) {
|
|
322
|
+
if (!sitemapByPlugin[entry.pluginKey]) {
|
|
323
|
+
sitemapByPlugin[entry.pluginKey] = [];
|
|
324
|
+
}
|
|
325
|
+
sitemapByPlugin[entry.pluginKey]!.push(entry);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Iterate over all plugins
|
|
329
|
+
for (const [pluginKey, plugin] of Object.entries(context.plugins)) {
|
|
330
|
+
// Skip the route-docs plugin itself
|
|
331
|
+
if (pluginKey === "routeDocs" || plugin.name === "route-docs") {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Get plugin routes
|
|
336
|
+
const pluginRoutes = plugin.routes(context);
|
|
337
|
+
const documentedRoutes: DocumentedRoute[] = [];
|
|
338
|
+
|
|
339
|
+
// Process each route
|
|
340
|
+
for (const [routeKey, route] of Object.entries(pluginRoutes)) {
|
|
341
|
+
const r = route as Route;
|
|
342
|
+
|
|
343
|
+
// Access route properties
|
|
344
|
+
const path = r.path;
|
|
345
|
+
const routeOptions = r.options || {};
|
|
346
|
+
const routeMeta = r.meta;
|
|
347
|
+
|
|
348
|
+
if (!path) continue;
|
|
349
|
+
|
|
350
|
+
// Extract path parameters from the path pattern
|
|
351
|
+
const pathParamNames = extractPathParams(path);
|
|
352
|
+
const pathParams: RouteParameter[] = pathParamNames.map((name) => ({
|
|
353
|
+
name,
|
|
354
|
+
type: "string", // Path params are always strings
|
|
355
|
+
required: true,
|
|
356
|
+
}));
|
|
357
|
+
|
|
358
|
+
// Extract query parameters from the query schema
|
|
359
|
+
const queryParams = extractQueryParams(routeOptions.query);
|
|
360
|
+
|
|
361
|
+
documentedRoutes.push({
|
|
362
|
+
key: routeKey,
|
|
363
|
+
path,
|
|
364
|
+
pathParams,
|
|
365
|
+
queryParams,
|
|
366
|
+
meta: routeMeta as DocumentedRoute["meta"],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (documentedRoutes.length > 0) {
|
|
371
|
+
documentedPlugins.push({
|
|
372
|
+
key: pluginKey,
|
|
373
|
+
name: plugin.name,
|
|
374
|
+
routes: documentedRoutes,
|
|
375
|
+
sitemapEntries: sitemapByPlugin[pluginKey] || [],
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
plugins: documentedPlugins,
|
|
382
|
+
generatedAt: new Date().toISOString(),
|
|
383
|
+
allSitemapEntries: sitemapEntries,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@import "./client.css";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Route Docs Plugin CSS - Includes Tailwind class scanning
|
|
5
|
+
*
|
|
6
|
+
* When consumed from npm, Tailwind v4 will automatically scan this package's
|
|
7
|
+
* source files for Tailwind classes. Consumers only need:
|
|
8
|
+
* @import "@btst/stack/plugins/route-docs/css";
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* Scan this package's source files for Tailwind classes */
|
|
12
|
+
@source "../../../src/**/*.{ts,tsx}";
|
|
13
|
+
|
|
14
|
+
/* Scan UI package components (when installed as npm package the UI package will be in this dir) */
|
|
15
|
+
@source "../../packages/ui/src";
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* alternatively consumer can use @source "../node_modules/@btst/stack/src/**\/*.{ts,tsx}";
|
|
19
|
+
*/
|
package/src/types.ts
CHANGED
|
@@ -15,6 +15,22 @@ export interface BetterStackContext {
|
|
|
15
15
|
adapter: Adapter;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Context passed to client plugins during route creation
|
|
20
|
+
* Provides access to all registered plugins for introspection (used by routeDocs plugin)
|
|
21
|
+
*/
|
|
22
|
+
export interface ClientStackContext<
|
|
23
|
+
TPlugins extends Record<string, ClientPlugin<any, any>> = Record<
|
|
24
|
+
string,
|
|
25
|
+
ClientPlugin<any, any>
|
|
26
|
+
>,
|
|
27
|
+
> {
|
|
28
|
+
/** All registered client plugins */
|
|
29
|
+
plugins: TPlugins;
|
|
30
|
+
/** The base path for the client (e.g., "/app") */
|
|
31
|
+
basePath?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
/**
|
|
19
35
|
* Backend plugin definition
|
|
20
36
|
* Defines API routes and data access for a feature
|
|
@@ -59,8 +75,10 @@ export interface ClientPlugin<
|
|
|
59
75
|
/**
|
|
60
76
|
* Define routes (pages) for this plugin
|
|
61
77
|
* Returns yar routes that will be composed into the router
|
|
78
|
+
*
|
|
79
|
+
* @param context - Optional context with access to all plugins (for introspection)
|
|
62
80
|
*/
|
|
63
|
-
routes: () => TRoutes;
|
|
81
|
+
routes: (context?: ClientStackContext) => TRoutes;
|
|
64
82
|
|
|
65
83
|
/**
|
|
66
84
|
* Optional sitemap generator for this plugin. Should return absolute URLs.
|
|
@@ -35,10 +35,10 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
+
title: z.ZodString;
|
|
38
39
|
slug: z.ZodOptional<z.ZodString>;
|
|
39
40
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
40
41
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
41
|
-
title: z.ZodString;
|
|
42
42
|
content: z.ZodString;
|
|
43
43
|
excerpt: z.ZodString;
|
|
44
44
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -35,10 +35,10 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
+
title: z.ZodString;
|
|
38
39
|
slug: z.ZodOptional<z.ZodString>;
|
|
39
40
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
40
41
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
41
|
-
title: z.ZodString;
|
|
42
42
|
content: z.ZodString;
|
|
43
43
|
excerpt: z.ZodString;
|
|
44
44
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -35,10 +35,10 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
+
title: z.ZodString;
|
|
38
39
|
slug: z.ZodOptional<z.ZodString>;
|
|
39
40
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
40
41
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
41
|
-
title: z.ZodString;
|
|
42
42
|
content: z.ZodString;
|
|
43
43
|
excerpt: z.ZodString;
|
|
44
44
|
image: z.ZodOptional<z.ZodString>;
|