@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.
Files changed (67) hide show
  1. package/dist/api/index.d.cts +2 -2
  2. package/dist/api/index.d.mts +2 -2
  3. package/dist/api/index.d.ts +2 -2
  4. package/dist/client/index.cjs +6 -2
  5. package/dist/client/index.d.cts +2 -1
  6. package/dist/client/index.d.mts +2 -1
  7. package/dist/client/index.d.ts +2 -1
  8. package/dist/client/index.mjs +6 -2
  9. package/dist/index.d.cts +1 -1
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/packages/better-stack/src/plugins/route-docs/client/components/loading/docs-skeleton.cjs +43 -0
  13. package/dist/packages/better-stack/src/plugins/route-docs/client/components/loading/docs-skeleton.mjs +41 -0
  14. package/dist/packages/better-stack/src/plugins/route-docs/client/components/pages/docs-page.cjs +794 -0
  15. package/dist/packages/better-stack/src/plugins/route-docs/client/components/pages/docs-page.mjs +788 -0
  16. package/dist/packages/better-stack/src/plugins/route-docs/client/plugin.cjs +111 -0
  17. package/dist/packages/better-stack/src/plugins/route-docs/client/plugin.mjs +106 -0
  18. package/dist/packages/better-stack/src/plugins/route-docs/generator.cjs +244 -0
  19. package/dist/packages/better-stack/src/plugins/route-docs/generator.mjs +227 -0
  20. package/dist/packages/ui/src/components/sheet.cjs +25 -0
  21. package/dist/packages/ui/src/components/sheet.mjs +24 -1
  22. package/dist/plugins/api/index.d.cts +2 -2
  23. package/dist/plugins/api/index.d.mts +2 -2
  24. package/dist/plugins/api/index.d.ts +2 -2
  25. package/dist/plugins/blog/api/index.d.cts +1 -1
  26. package/dist/plugins/blog/api/index.d.mts +1 -1
  27. package/dist/plugins/blog/api/index.d.ts +1 -1
  28. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  29. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  30. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  31. package/dist/plugins/blog/client/index.d.cts +1 -1
  32. package/dist/plugins/blog/client/index.d.mts +1 -1
  33. package/dist/plugins/blog/client/index.d.ts +1 -1
  34. package/dist/plugins/blog/query-keys.d.cts +2 -2
  35. package/dist/plugins/blog/query-keys.d.mts +2 -2
  36. package/dist/plugins/blog/query-keys.d.ts +2 -2
  37. package/dist/plugins/client/index.d.cts +2 -2
  38. package/dist/plugins/client/index.d.mts +2 -2
  39. package/dist/plugins/client/index.d.ts +2 -2
  40. package/dist/plugins/open-api/api/index.d.cts +1 -1
  41. package/dist/plugins/open-api/api/index.d.mts +1 -1
  42. package/dist/plugins/open-api/api/index.d.ts +1 -1
  43. package/dist/plugins/route-docs/client/index.cjs +10 -0
  44. package/dist/plugins/route-docs/client/index.d.cts +126 -0
  45. package/dist/plugins/route-docs/client/index.d.mts +126 -0
  46. package/dist/plugins/route-docs/client/index.d.ts +126 -0
  47. package/dist/plugins/route-docs/client/index.mjs +1 -0
  48. package/dist/plugins/route-docs/client.css +3 -0
  49. package/dist/plugins/route-docs/style.css +19 -0
  50. package/dist/shared/{stack.CSce37mX.d.cts → stack.u9iYV6vt.d.cts} +14 -2
  51. package/dist/shared/{stack.CSce37mX.d.mts → stack.u9iYV6vt.d.mts} +14 -2
  52. package/dist/shared/{stack.CSce37mX.d.ts → stack.u9iYV6vt.d.ts} +14 -2
  53. package/package.json +15 -1
  54. package/src/client/index.ts +11 -4
  55. package/src/plugins/route-docs/client/components/loading/docs-skeleton.tsx +82 -0
  56. package/src/plugins/route-docs/client/components/loading/index.tsx +1 -0
  57. package/src/plugins/route-docs/client/components/pages/docs-page.tsx +1240 -0
  58. package/src/plugins/route-docs/client/index.ts +7 -0
  59. package/src/plugins/route-docs/client/plugin.tsx +187 -0
  60. package/src/plugins/route-docs/client.css +3 -0
  61. package/src/plugins/route-docs/generator.ts +385 -0
  62. package/src/plugins/route-docs/index.ts +12 -0
  63. package/src/plugins/route-docs/style.css +19 -0
  64. package/src/types.ts +19 -1
  65. package/dist/shared/{stack.CcI4sYJP.d.mts → stack.DLhzx1-D.d.cts} +1 -1
  66. package/dist/shared/{stack.CcI4sYJP.d.ts → stack.DLhzx1-D.d.mts} +1 -1
  67. package/dist/shared/{stack.CcI4sYJP.d.cts → stack.DLhzx1-D.d.ts} +1 -1
@@ -0,0 +1,7 @@
1
+ export {
2
+ routeDocsClientPlugin,
3
+ type RouteDocsClientConfig,
4
+ ROUTE_DOCS_QUERY_KEY,
5
+ generateSchema,
6
+ getStoredContext,
7
+ } from "./plugin";
@@ -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,3 @@
1
+ /* Route Docs Plugin Client CSS */
2
+ /* No custom styles needed - uses shadcn/ui components and Tailwind */
3
+
@@ -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,12 @@
1
+ export {
2
+ routeDocsClientPlugin,
3
+ type RouteDocsClientConfig,
4
+ } from "./client";
5
+
6
+ export {
7
+ generateRouteDocsSchema,
8
+ type RouteDocsSchema,
9
+ type DocumentedPlugin,
10
+ type DocumentedRoute,
11
+ type RouteParameter,
12
+ } from "./generator";
@@ -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>;