diff --git a/biome.json b/biome.json index 775f1236..2606612a 100644 --- a/biome.json +++ b/biome.json @@ -71,7 +71,9 @@ "!**/examples/**", "!**/packages/ui/**", "!**/docs/**", - "!**/test-results/**" + "!**/test-results/**", + "!**/.btst-stack-src/**", + "!**/.btst-stack-ui/**" ] } } diff --git a/demos/ai-chat/app/page.tsx b/demos/ai-chat/app/page.tsx index 5838f719..52025793 100644 --- a/demos/ai-chat/app/page.tsx +++ b/demos/ai-chat/app/page.tsx @@ -1,3 +1,81 @@ -export default function Home() { - return null; +import Link from "next/link"; +import { getOrCreateQueryClient } from "@/lib/query-client"; +import { getStackClient } from "@/lib/stack-client"; +import { generateSchema } from "@btst/stack/plugins/route-docs/client"; + +type RouteItem = { label: string; path: string }; +type RouteGroup = { heading: string; routes: RouteItem[] }; + +const SITE_BASE_PATH = "/pages"; + +function routeKeyToLabel(key: string): string { + return key + .replace(/([A-Z])/g, " $1") + .replace(/^./, (s) => s.toUpperCase()) + .trim(); +} + +export default async function Home() { + const queryClient = getOrCreateQueryClient(); + getStackClient(queryClient); + const schema = await generateSchema(); + + const aiChatPlugin = schema.plugins.find((p) => p.key === "aiChat"); + const staticChatRoutes: RouteItem[] = + aiChatPlugin?.routes + .filter((r) => r.pathParams.length === 0) + .map((r) => ({ + label: routeKeyToLabel(r.key), + path: `${SITE_BASE_PATH}${r.path}`, + })) ?? []; + + const groups: RouteGroup[] = [ + { heading: "Chat", routes: staticChatRoutes }, + { + heading: "Docs", + routes: [ + { label: "Route Docs", path: `${SITE_BASE_PATH}/route-docs` }, + { label: "API Reference", path: "/api/data/reference" }, + ], + }, + ].filter((g) => g.routes.length > 0); + + return ( +
+
+
+

+ BTST AI Chat Demo +

+

+ Available routes in this demo +

+
+
+ {groups.map((group) => ( +
+

+ {group.heading} +

+
    + {group.routes.map(({ label, path }) => ( +
  • + +
    {label}
    + + {path} + + +
  • + ))} +
+
+ ))} +
+
+
+ ); } diff --git a/demos/ai-chat/copy-stack-src.mjs b/demos/ai-chat/copy-stack-src.mjs index eca1c4dd..ca4bc7c3 100644 --- a/demos/ai-chat/copy-stack-src.mjs +++ b/demos/ai-chat/copy-stack-src.mjs @@ -18,8 +18,6 @@ const uiSrc = "node_modules/@btst/stack/dist/packages/ui"; const uiDest = "app/.btst-stack-ui"; if (!existsSync(src)) { - // Likely running in the monorepo where the workspace symlink does not expose - // src/ — fall back gracefully; @source paths in globals.css cover this case. console.log( "[copy-stack-src] node_modules/@btst/stack/src not found, skipping", ); @@ -29,17 +27,27 @@ if (!existsSync(src)) { await rm(dest, { recursive: true, force: true }); await mkdir(dest, { recursive: true }); await cp(src, dest, { recursive: true }); -console.log("[copy-stack-src] copied @btst/stack/src → .btst-stack-src"); +console.log(`[copy-stack-src] copied ${src} → ${dest}`); if (existsSync(uiSrc)) { await rm(uiDest, { recursive: true, force: true }); await mkdir(uiDest, { recursive: true }); await cp(uiSrc, uiDest, { recursive: true }); - console.log( - "[copy-stack-src] copied @btst/stack/dist/packages/ui → .btst-stack-ui", - ); + console.log(`[copy-stack-src] copied ${uiSrc} → ${uiDest}`); } else { + console.log(`[copy-stack-src] ${uiSrc} not found, skipping`); +} + +// When running inside the monorepo, the workspace-built dist/plugins/ has +// @workspace/ui imports already inlined by postbuild.cjs. Copy those files +// over the npm-installed ones so the demo uses the correct, self-contained CSS. +// Outside the monorepo (StackBlitz/WebContainers), this path won't exist and +// the step is silently skipped — the published npm package handles it instead. +const workspacePluginsDist = "../../packages/stack/dist/plugins"; +const npmPluginsDist = "node_modules/@btst/stack/dist/plugins"; +if (existsSync(workspacePluginsDist)) { + await cp(workspacePluginsDist, npmPluginsDist, { recursive: true }); console.log( - "[copy-stack-src] node_modules/@btst/stack/dist/packages/ui not found, skipping", + `[copy-stack-src] overlaid ${workspacePluginsDist} → ${npmPluginsDist}`, ); } diff --git a/demos/ai-chat/next-env.d.ts b/demos/ai-chat/next-env.d.ts index 9edff1c7..1b3be084 100644 --- a/demos/ai-chat/next-env.d.ts +++ b/demos/ai-chat/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/demos/ai-chat/next.config.ts b/demos/ai-chat/next.config.ts index 6cf23600..a7f92544 100644 --- a/demos/ai-chat/next.config.ts +++ b/demos/ai-chat/next.config.ts @@ -4,9 +4,6 @@ const nextConfig: NextConfig = { env: { NEXT_PUBLIC_HAS_OPENAI_KEY: process.env.OPENAI_API_KEY ? "1" : "", }, - async redirects() { - return [{ source: "/", destination: "/pages/chat", permanent: false }]; - }, }; export default nextConfig; diff --git a/demos/ai-chat/package.json b/demos/ai-chat/package.json index 3386dc5d..8f92babc 100644 --- a/demos/ai-chat/package.json +++ b/demos/ai-chat/package.json @@ -11,7 +11,7 @@ "@ai-sdk/openai": "^3.0.41", "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.2", + "@btst/stack": "^2.5.3", "@btst/yar": "^1.2.0", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-dialog": "^1.1.15", diff --git a/demos/ai-chat/tsconfig.json b/demos/ai-chat/tsconfig.json index 3a13f90a..0352f50e 100644 --- a/demos/ai-chat/tsconfig.json +++ b/demos/ai-chat/tsconfig.json @@ -11,7 +11,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { diff --git a/demos/blog/app/page.tsx b/demos/blog/app/page.tsx index 5838f719..1ce16b65 100644 --- a/demos/blog/app/page.tsx +++ b/demos/blog/app/page.tsx @@ -1,3 +1,100 @@ -export default function Home() { - return null; +import Link from "next/link"; +import { getOrCreateQueryClient } from "@/lib/query-client"; +import { getStackClient } from "@/lib/stack-client"; +import { generateSchema } from "@btst/stack/plugins/route-docs/client"; +import { myStack } from "@/lib/stack"; + +type RouteItem = { label: string; path: string }; +type RouteGroup = { heading: string; routes: RouteItem[] }; + +const SITE_BASE_PATH = "/pages"; + +function routeKeyToLabel(key: string): string { + return key + .replace(/([A-Z])/g, " $1") + .replace(/^./, (s) => s.toUpperCase()) + .trim(); +} + +export default async function Home() { + const queryClient = getOrCreateQueryClient(); + getStackClient(queryClient); + const schema = await generateSchema(); + + const blogPlugin = schema.plugins.find((p) => p.key === "blog"); + const staticBlogRoutes: RouteItem[] = + blogPlugin?.routes + .filter((r) => r.pathParams.length === 0) + .map((r) => ({ + label: routeKeyToLabel(r.key), + path: `${SITE_BASE_PATH}${r.path}`, + })) ?? []; + + const { items: posts } = await myStack.api.blog.getAllPosts({ + published: true, + }); + + const groups: RouteGroup[] = [ + { heading: "Blog", routes: staticBlogRoutes }, + { + heading: "Posts (seeded)", + routes: posts.map((p) => ({ + label: p.title, + path: `${SITE_BASE_PATH}/blog/${p.slug}`, + })), + }, + { + heading: "Edit Posts (seeded)", + routes: posts.map((p) => ({ + label: p.title, + path: `${SITE_BASE_PATH}/blog/${p.slug}/edit`, + })), + }, + { + heading: "Docs", + routes: [ + { label: "Route Docs", path: `${SITE_BASE_PATH}/route-docs` }, + { label: "API Reference", path: "/api/data/reference" }, + ], + }, + ].filter((g) => g.routes.length > 0); + + return ( +
+
+
+

+ BTST Blog Demo +

+

+ Available routes in this demo +

+
+
+ {groups.map((group) => ( +
+

+ {group.heading} +

+
    + {group.routes.map(({ label, path }) => ( +
  • + +
    {label}
    + + {path} + + +
  • + ))} +
+
+ ))} +
+
+
+ ); } diff --git a/demos/blog/copy-stack-src.mjs b/demos/blog/copy-stack-src.mjs index eca1c4dd..ca4bc7c3 100644 --- a/demos/blog/copy-stack-src.mjs +++ b/demos/blog/copy-stack-src.mjs @@ -18,8 +18,6 @@ const uiSrc = "node_modules/@btst/stack/dist/packages/ui"; const uiDest = "app/.btst-stack-ui"; if (!existsSync(src)) { - // Likely running in the monorepo where the workspace symlink does not expose - // src/ — fall back gracefully; @source paths in globals.css cover this case. console.log( "[copy-stack-src] node_modules/@btst/stack/src not found, skipping", ); @@ -29,17 +27,27 @@ if (!existsSync(src)) { await rm(dest, { recursive: true, force: true }); await mkdir(dest, { recursive: true }); await cp(src, dest, { recursive: true }); -console.log("[copy-stack-src] copied @btst/stack/src → .btst-stack-src"); +console.log(`[copy-stack-src] copied ${src} → ${dest}`); if (existsSync(uiSrc)) { await rm(uiDest, { recursive: true, force: true }); await mkdir(uiDest, { recursive: true }); await cp(uiSrc, uiDest, { recursive: true }); - console.log( - "[copy-stack-src] copied @btst/stack/dist/packages/ui → .btst-stack-ui", - ); + console.log(`[copy-stack-src] copied ${uiSrc} → ${uiDest}`); } else { + console.log(`[copy-stack-src] ${uiSrc} not found, skipping`); +} + +// When running inside the monorepo, the workspace-built dist/plugins/ has +// @workspace/ui imports already inlined by postbuild.cjs. Copy those files +// over the npm-installed ones so the demo uses the correct, self-contained CSS. +// Outside the monorepo (StackBlitz/WebContainers), this path won't exist and +// the step is silently skipped — the published npm package handles it instead. +const workspacePluginsDist = "../../packages/stack/dist/plugins"; +const npmPluginsDist = "node_modules/@btst/stack/dist/plugins"; +if (existsSync(workspacePluginsDist)) { + await cp(workspacePluginsDist, npmPluginsDist, { recursive: true }); console.log( - "[copy-stack-src] node_modules/@btst/stack/dist/packages/ui not found, skipping", + `[copy-stack-src] overlaid ${workspacePluginsDist} → ${npmPluginsDist}`, ); } diff --git a/demos/blog/next.config.ts b/demos/blog/next.config.ts index 39272564..cb651cdc 100644 --- a/demos/blog/next.config.ts +++ b/demos/blog/next.config.ts @@ -1,9 +1,5 @@ import type { NextConfig } from "next"; -const nextConfig: NextConfig = { - async redirects() { - return [{ source: "/", destination: "/pages/blog", permanent: false }]; - }, -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/demos/blog/package.json b/demos/blog/package.json index 3ed8d559..724821ac 100644 --- a/demos/blog/package.json +++ b/demos/blog/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.2", + "@btst/stack": "^2.5.3", "@btst/yar": "^1.2.0", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-dialog": "^1.1.15", diff --git a/demos/cms/app/page.tsx b/demos/cms/app/page.tsx index 5838f719..bab7b0f0 100644 --- a/demos/cms/app/page.tsx +++ b/demos/cms/app/page.tsx @@ -1,3 +1,106 @@ -export default function Home() { - return null; +import Link from "next/link"; +import { getOrCreateQueryClient } from "@/lib/query-client"; +import { getStackClient } from "@/lib/stack-client"; +import { generateSchema } from "@btst/stack/plugins/route-docs/client"; +import { myStack } from "@/lib/stack"; + +type RouteItem = { label: string; path: string }; +type RouteGroup = { heading: string; routes: RouteItem[] }; + +const SITE_BASE_PATH = "/pages"; + +function routeKeyToLabel(key: string): string { + return key + .replace(/([A-Z])/g, " $1") + .replace(/^./, (s) => s.toUpperCase()) + .trim(); +} + +export default async function Home() { + const queryClient = getOrCreateQueryClient(); + getStackClient(queryClient); + const schema = await generateSchema(); + + const cmsPlugin = schema.plugins.find((p) => p.key === "cms"); + const staticCmsRoutes: RouteItem[] = + cmsPlugin?.routes + .filter((r) => r.pathParams.length === 0) + .map((r) => ({ + label: routeKeyToLabel(r.key), + path: `${SITE_BASE_PATH}${r.path}`, + })) ?? []; + + const [contentTypes, { items: articles }] = await Promise.all([ + myStack.api.cms.getAllContentTypes(), + myStack.api.cms.getAllContentItems("article"), + ]); + + // Expand parameterized CMS admin routes (/cms/:typeSlug, /cms/:typeSlug/new) with real content type slugs + const cmsTypeRoutes: RouteItem[] = contentTypes.flatMap((t) => [ + { label: t.name, path: `${SITE_BASE_PATH}/cms/${t.slug}` }, + { label: `New ${t.name}`, path: `${SITE_BASE_PATH}/cms/${t.slug}/new` }, + ]); + + const groups: RouteGroup[] = [ + { + heading: "Articles (public)", + routes: [ + { label: "Articles", path: `${SITE_BASE_PATH}/articles` }, + ...articles.map((item) => ({ + label: (item.parsedData as { title?: string }).title ?? item.slug, + path: `${SITE_BASE_PATH}/articles/${item.slug}`, + })), + ], + }, + { + heading: "CMS (admin)", + routes: [...staticCmsRoutes, ...cmsTypeRoutes], + }, + { + heading: "Docs", + routes: [ + { label: "Route Docs", path: `${SITE_BASE_PATH}/route-docs` }, + { label: "API Reference", path: "/api/data/reference" }, + ], + }, + ].filter((g) => g.routes.length > 0); + + return ( +
+
+
+

+ BTST CMS Demo +

+

+ Available routes in this demo +

+
+
+ {groups.map((group) => ( +
+

+ {group.heading} +

+
    + {group.routes.map(({ label, path }) => ( +
  • + +
    {label}
    + + {path} + + +
  • + ))} +
+
+ ))} +
+
+
+ ); } diff --git a/demos/cms/copy-stack-src.mjs b/demos/cms/copy-stack-src.mjs index eca1c4dd..ca4bc7c3 100644 --- a/demos/cms/copy-stack-src.mjs +++ b/demos/cms/copy-stack-src.mjs @@ -18,8 +18,6 @@ const uiSrc = "node_modules/@btst/stack/dist/packages/ui"; const uiDest = "app/.btst-stack-ui"; if (!existsSync(src)) { - // Likely running in the monorepo where the workspace symlink does not expose - // src/ — fall back gracefully; @source paths in globals.css cover this case. console.log( "[copy-stack-src] node_modules/@btst/stack/src not found, skipping", ); @@ -29,17 +27,27 @@ if (!existsSync(src)) { await rm(dest, { recursive: true, force: true }); await mkdir(dest, { recursive: true }); await cp(src, dest, { recursive: true }); -console.log("[copy-stack-src] copied @btst/stack/src → .btst-stack-src"); +console.log(`[copy-stack-src] copied ${src} → ${dest}`); if (existsSync(uiSrc)) { await rm(uiDest, { recursive: true, force: true }); await mkdir(uiDest, { recursive: true }); await cp(uiSrc, uiDest, { recursive: true }); - console.log( - "[copy-stack-src] copied @btst/stack/dist/packages/ui → .btst-stack-ui", - ); + console.log(`[copy-stack-src] copied ${uiSrc} → ${uiDest}`); } else { + console.log(`[copy-stack-src] ${uiSrc} not found, skipping`); +} + +// When running inside the monorepo, the workspace-built dist/plugins/ has +// @workspace/ui imports already inlined by postbuild.cjs. Copy those files +// over the npm-installed ones so the demo uses the correct, self-contained CSS. +// Outside the monorepo (StackBlitz/WebContainers), this path won't exist and +// the step is silently skipped — the published npm package handles it instead. +const workspacePluginsDist = "../../packages/stack/dist/plugins"; +const npmPluginsDist = "node_modules/@btst/stack/dist/plugins"; +if (existsSync(workspacePluginsDist)) { + await cp(workspacePluginsDist, npmPluginsDist, { recursive: true }); console.log( - "[copy-stack-src] node_modules/@btst/stack/dist/packages/ui not found, skipping", + `[copy-stack-src] overlaid ${workspacePluginsDist} → ${npmPluginsDist}`, ); } diff --git a/demos/cms/next-env.d.ts b/demos/cms/next-env.d.ts index 9edff1c7..1b3be084 100644 --- a/demos/cms/next-env.d.ts +++ b/demos/cms/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/demos/cms/next.config.ts b/demos/cms/next.config.ts index b7155578..cb651cdc 100644 --- a/demos/cms/next.config.ts +++ b/demos/cms/next.config.ts @@ -1,9 +1,5 @@ import type { NextConfig } from "next"; -const nextConfig: NextConfig = { - async redirects() { - return [{ source: "/", destination: "/pages/cms", permanent: false }]; - }, -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/demos/cms/package.json b/demos/cms/package.json index 0491f309..8b658223 100644 --- a/demos/cms/package.json +++ b/demos/cms/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.2", + "@btst/stack": "^2.5.3", "@btst/yar": "^1.2.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/demos/cms/tsconfig.json b/demos/cms/tsconfig.json index 3a13f90a..0352f50e 100644 --- a/demos/cms/tsconfig.json +++ b/demos/cms/tsconfig.json @@ -11,7 +11,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { diff --git a/demos/form-builder/app/page.tsx b/demos/form-builder/app/page.tsx index 5838f719..fac5c354 100644 --- a/demos/form-builder/app/page.tsx +++ b/demos/form-builder/app/page.tsx @@ -1,3 +1,91 @@ -export default function Home() { - return null; +import Link from "next/link"; +import { getOrCreateQueryClient } from "@/lib/query-client"; +import { getStackClient } from "@/lib/stack-client"; +import { generateSchema } from "@btst/stack/plugins/route-docs/client"; +import { myStack } from "@/lib/stack"; + +type RouteItem = { label: string; path: string }; +type RouteGroup = { heading: string; routes: RouteItem[] }; + +const SITE_BASE_PATH = "/pages"; + +function routeKeyToLabel(key: string): string { + return key + .replace(/([A-Z])/g, " $1") + .replace(/^./, (s) => s.toUpperCase()) + .trim(); +} + +export default async function Home() { + const queryClient = getOrCreateQueryClient(); + getStackClient(queryClient); + const schema = await generateSchema(); + + const formBuilderPlugin = schema.plugins.find((p) => p.key === "formBuilder"); + const staticFormRoutes: RouteItem[] = + formBuilderPlugin?.routes + .filter((r) => r.pathParams.length === 0) + .map((r) => ({ + label: routeKeyToLabel(r.key), + path: `${SITE_BASE_PATH}${r.path}`, + })) ?? []; + + const { items: forms } = await myStack.api.formBuilder.getAllForms(); + + const groups: RouteGroup[] = [ + { heading: "Forms (admin)", routes: staticFormRoutes }, + { + heading: "Public Forms (seeded)", + routes: forms.map((f) => ({ + label: f.name, + path: `/submit/${f.slug}`, + })), + }, + { + heading: "Docs", + routes: [ + { label: "Route Docs", path: `${SITE_BASE_PATH}/route-docs` }, + { label: "API Reference", path: "/api/data/reference" }, + ], + }, + ].filter((g) => g.routes.length > 0); + + return ( +
+
+
+

+ BTST Form Builder Demo +

+

+ Available routes in this demo +

+
+
+ {groups.map((group) => ( +
+

+ {group.heading} +

+
    + {group.routes.map(({ label, path }) => ( +
  • + +
    {label}
    + + {path} + + +
  • + ))} +
+
+ ))} +
+
+
+ ); } diff --git a/demos/form-builder/copy-stack-src.mjs b/demos/form-builder/copy-stack-src.mjs index eca1c4dd..ca4bc7c3 100644 --- a/demos/form-builder/copy-stack-src.mjs +++ b/demos/form-builder/copy-stack-src.mjs @@ -18,8 +18,6 @@ const uiSrc = "node_modules/@btst/stack/dist/packages/ui"; const uiDest = "app/.btst-stack-ui"; if (!existsSync(src)) { - // Likely running in the monorepo where the workspace symlink does not expose - // src/ — fall back gracefully; @source paths in globals.css cover this case. console.log( "[copy-stack-src] node_modules/@btst/stack/src not found, skipping", ); @@ -29,17 +27,27 @@ if (!existsSync(src)) { await rm(dest, { recursive: true, force: true }); await mkdir(dest, { recursive: true }); await cp(src, dest, { recursive: true }); -console.log("[copy-stack-src] copied @btst/stack/src → .btst-stack-src"); +console.log(`[copy-stack-src] copied ${src} → ${dest}`); if (existsSync(uiSrc)) { await rm(uiDest, { recursive: true, force: true }); await mkdir(uiDest, { recursive: true }); await cp(uiSrc, uiDest, { recursive: true }); - console.log( - "[copy-stack-src] copied @btst/stack/dist/packages/ui → .btst-stack-ui", - ); + console.log(`[copy-stack-src] copied ${uiSrc} → ${uiDest}`); } else { + console.log(`[copy-stack-src] ${uiSrc} not found, skipping`); +} + +// When running inside the monorepo, the workspace-built dist/plugins/ has +// @workspace/ui imports already inlined by postbuild.cjs. Copy those files +// over the npm-installed ones so the demo uses the correct, self-contained CSS. +// Outside the monorepo (StackBlitz/WebContainers), this path won't exist and +// the step is silently skipped — the published npm package handles it instead. +const workspacePluginsDist = "../../packages/stack/dist/plugins"; +const npmPluginsDist = "node_modules/@btst/stack/dist/plugins"; +if (existsSync(workspacePluginsDist)) { + await cp(workspacePluginsDist, npmPluginsDist, { recursive: true }); console.log( - "[copy-stack-src] node_modules/@btst/stack/dist/packages/ui not found, skipping", + `[copy-stack-src] overlaid ${workspacePluginsDist} → ${npmPluginsDist}`, ); } diff --git a/demos/form-builder/next-env.d.ts b/demos/form-builder/next-env.d.ts index 9edff1c7..1b3be084 100644 --- a/demos/form-builder/next-env.d.ts +++ b/demos/form-builder/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/demos/form-builder/next.config.ts b/demos/form-builder/next.config.ts index 46abc098..cb651cdc 100644 --- a/demos/form-builder/next.config.ts +++ b/demos/form-builder/next.config.ts @@ -1,9 +1,5 @@ import type { NextConfig } from "next"; -const nextConfig: NextConfig = { - async redirects() { - return [{ source: "/", destination: "/pages/forms", permanent: false }]; - }, -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/demos/form-builder/package.json b/demos/form-builder/package.json index dddcda1d..90b616a1 100644 --- a/demos/form-builder/package.json +++ b/demos/form-builder/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.2", + "@btst/stack": "^2.5.3", "@btst/yar": "^1.2.0", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-checkbox": "^1.3.3", diff --git a/demos/form-builder/tsconfig.json b/demos/form-builder/tsconfig.json index 3a13f90a..0352f50e 100644 --- a/demos/form-builder/tsconfig.json +++ b/demos/form-builder/tsconfig.json @@ -11,7 +11,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { diff --git a/demos/kanban/app/page.tsx b/demos/kanban/app/page.tsx index 5838f719..aa9166a4 100644 --- a/demos/kanban/app/page.tsx +++ b/demos/kanban/app/page.tsx @@ -1,3 +1,91 @@ -export default function Home() { - return null; +import Link from "next/link"; +import { getOrCreateQueryClient } from "@/lib/query-client"; +import { getStackClient } from "@/lib/stack-client"; +import { generateSchema } from "@btst/stack/plugins/route-docs/client"; +import { myStack } from "@/lib/stack"; + +type RouteItem = { label: string; path: string }; +type RouteGroup = { heading: string; routes: RouteItem[] }; + +const SITE_BASE_PATH = "/pages"; + +function routeKeyToLabel(key: string): string { + return key + .replace(/([A-Z])/g, " $1") + .replace(/^./, (s) => s.toUpperCase()) + .trim(); +} + +export default async function Home() { + const queryClient = getOrCreateQueryClient(); + getStackClient(queryClient); + const schema = await generateSchema(); + + const kanbanPlugin = schema.plugins.find((p) => p.key === "kanban"); + const staticKanbanRoutes: RouteItem[] = + kanbanPlugin?.routes + .filter((r) => r.pathParams.length === 0) + .map((r) => ({ + label: routeKeyToLabel(r.key), + path: `${SITE_BASE_PATH}${r.path}`, + })) ?? []; + + const { items: boards } = await myStack.api.kanban.getAllBoards(); + + const groups: RouteGroup[] = [ + { heading: "Kanban", routes: staticKanbanRoutes }, + { + heading: "Boards (seeded)", + routes: boards.map((b) => ({ + label: b.name, + path: `${SITE_BASE_PATH}/kanban/${b.id}`, + })), + }, + { + heading: "Docs", + routes: [ + { label: "Route Docs", path: `${SITE_BASE_PATH}/route-docs` }, + { label: "API Reference", path: "/api/data/reference" }, + ], + }, + ].filter((g) => g.routes.length > 0); + + return ( +
+
+
+

+ BTST Kanban Demo +

+

+ Available routes in this demo +

+
+
+ {groups.map((group) => ( +
+

+ {group.heading} +

+
    + {group.routes.map(({ label, path }) => ( +
  • + +
    {label}
    + + {path} + + +
  • + ))} +
+
+ ))} +
+
+
+ ); } diff --git a/demos/kanban/copy-stack-src.mjs b/demos/kanban/copy-stack-src.mjs index eca1c4dd..ca4bc7c3 100644 --- a/demos/kanban/copy-stack-src.mjs +++ b/demos/kanban/copy-stack-src.mjs @@ -18,8 +18,6 @@ const uiSrc = "node_modules/@btst/stack/dist/packages/ui"; const uiDest = "app/.btst-stack-ui"; if (!existsSync(src)) { - // Likely running in the monorepo where the workspace symlink does not expose - // src/ — fall back gracefully; @source paths in globals.css cover this case. console.log( "[copy-stack-src] node_modules/@btst/stack/src not found, skipping", ); @@ -29,17 +27,27 @@ if (!existsSync(src)) { await rm(dest, { recursive: true, force: true }); await mkdir(dest, { recursive: true }); await cp(src, dest, { recursive: true }); -console.log("[copy-stack-src] copied @btst/stack/src → .btst-stack-src"); +console.log(`[copy-stack-src] copied ${src} → ${dest}`); if (existsSync(uiSrc)) { await rm(uiDest, { recursive: true, force: true }); await mkdir(uiDest, { recursive: true }); await cp(uiSrc, uiDest, { recursive: true }); - console.log( - "[copy-stack-src] copied @btst/stack/dist/packages/ui → .btst-stack-ui", - ); + console.log(`[copy-stack-src] copied ${uiSrc} → ${uiDest}`); } else { + console.log(`[copy-stack-src] ${uiSrc} not found, skipping`); +} + +// When running inside the monorepo, the workspace-built dist/plugins/ has +// @workspace/ui imports already inlined by postbuild.cjs. Copy those files +// over the npm-installed ones so the demo uses the correct, self-contained CSS. +// Outside the monorepo (StackBlitz/WebContainers), this path won't exist and +// the step is silently skipped — the published npm package handles it instead. +const workspacePluginsDist = "../../packages/stack/dist/plugins"; +const npmPluginsDist = "node_modules/@btst/stack/dist/plugins"; +if (existsSync(workspacePluginsDist)) { + await cp(workspacePluginsDist, npmPluginsDist, { recursive: true }); console.log( - "[copy-stack-src] node_modules/@btst/stack/dist/packages/ui not found, skipping", + `[copy-stack-src] overlaid ${workspacePluginsDist} → ${npmPluginsDist}`, ); } diff --git a/demos/kanban/next-env.d.ts b/demos/kanban/next-env.d.ts index 9edff1c7..1b3be084 100644 --- a/demos/kanban/next-env.d.ts +++ b/demos/kanban/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/demos/kanban/next.config.ts b/demos/kanban/next.config.ts index 5b25b592..cb651cdc 100644 --- a/demos/kanban/next.config.ts +++ b/demos/kanban/next.config.ts @@ -1,9 +1,5 @@ import type { NextConfig } from "next"; -const nextConfig: NextConfig = { - async redirects() { - return [{ source: "/", destination: "/pages/kanban", permanent: false }]; - }, -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/demos/kanban/package.json b/demos/kanban/package.json index b2cac79c..7fcb31f8 100644 --- a/demos/kanban/package.json +++ b/demos/kanban/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.2", + "@btst/stack": "^2.5.3", "@btst/yar": "^1.2.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/demos/kanban/tsconfig.json b/demos/kanban/tsconfig.json index 3a13f90a..0352f50e 100644 --- a/demos/kanban/tsconfig.json +++ b/demos/kanban/tsconfig.json @@ -11,7 +11,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { diff --git a/demos/ui-builder/app/page.tsx b/demos/ui-builder/app/page.tsx index 5838f719..17651d89 100644 --- a/demos/ui-builder/app/page.tsx +++ b/demos/ui-builder/app/page.tsx @@ -1,3 +1,102 @@ -export default function Home() { - return null; +import Link from "next/link"; +import { getOrCreateQueryClient } from "@/lib/query-client"; +import { getStackClient } from "@/lib/stack-client"; +import { generateSchema } from "@btst/stack/plugins/route-docs/client"; +import { myStack } from "@/lib/stack"; + +type RouteItem = { label: string; path: string }; +type RouteGroup = { heading: string; routes: RouteItem[] }; + +const SITE_BASE_PATH = "/pages"; + +function routeKeyToLabel(key: string): string { + return key + .replace(/([A-Z])/g, " $1") + .replace(/^./, (s) => s.toUpperCase()) + .trim(); +} + +export default async function Home() { + const queryClient = getOrCreateQueryClient(); + getStackClient(queryClient); + const schema = await generateSchema(); + + const uiBuilderPlugin = schema.plugins.find((p) => p.key === "uiBuilder"); + const staticUiBuilderRoutes: RouteItem[] = + uiBuilderPlugin?.routes + .filter((r) => r.pathParams.length === 0) + .map((r) => ({ + label: routeKeyToLabel(r.key), + path: `${SITE_BASE_PATH}${r.path}`, + })) ?? []; + + const cmsPlugin = schema.plugins.find((p) => p.key === "cms"); + const staticCmsRoutes: RouteItem[] = + cmsPlugin?.routes + .filter((r) => r.pathParams.length === 0) + .map((r) => ({ + label: routeKeyToLabel(r.key), + path: `${SITE_BASE_PATH}${r.path}`, + })) ?? []; + + // Expand parameterized CMS admin routes with real content type slugs + const contentTypes = await myStack.api.cms.getAllContentTypes(); + const cmsTypeRoutes: RouteItem[] = contentTypes.flatMap((t) => [ + { label: t.name, path: `${SITE_BASE_PATH}/cms/${t.slug}` }, + { label: `New ${t.name}`, path: `${SITE_BASE_PATH}/cms/${t.slug}/new` }, + ]); + + const groups: RouteGroup[] = [ + { heading: "UI Builder", routes: staticUiBuilderRoutes }, + { + heading: "CMS (admin)", + routes: [...staticCmsRoutes, ...cmsTypeRoutes], + }, + { + heading: "Docs", + routes: [ + { label: "Route Docs", path: `${SITE_BASE_PATH}/route-docs` }, + { label: "API Reference", path: "/api/data/reference" }, + ], + }, + ].filter((g) => g.routes.length > 0); + + return ( +
+
+
+

+ BTST UI Builder Demo +

+

+ Available routes in this demo +

+
+
+ {groups.map((group) => ( +
+

+ {group.heading} +

+
    + {group.routes.map(({ label, path }) => ( +
  • + +
    {label}
    + + {path} + + +
  • + ))} +
+
+ ))} +
+
+
+ ); } diff --git a/demos/ui-builder/copy-stack-src.mjs b/demos/ui-builder/copy-stack-src.mjs index eca1c4dd..ca4bc7c3 100644 --- a/demos/ui-builder/copy-stack-src.mjs +++ b/demos/ui-builder/copy-stack-src.mjs @@ -18,8 +18,6 @@ const uiSrc = "node_modules/@btst/stack/dist/packages/ui"; const uiDest = "app/.btst-stack-ui"; if (!existsSync(src)) { - // Likely running in the monorepo where the workspace symlink does not expose - // src/ — fall back gracefully; @source paths in globals.css cover this case. console.log( "[copy-stack-src] node_modules/@btst/stack/src not found, skipping", ); @@ -29,17 +27,27 @@ if (!existsSync(src)) { await rm(dest, { recursive: true, force: true }); await mkdir(dest, { recursive: true }); await cp(src, dest, { recursive: true }); -console.log("[copy-stack-src] copied @btst/stack/src → .btst-stack-src"); +console.log(`[copy-stack-src] copied ${src} → ${dest}`); if (existsSync(uiSrc)) { await rm(uiDest, { recursive: true, force: true }); await mkdir(uiDest, { recursive: true }); await cp(uiSrc, uiDest, { recursive: true }); - console.log( - "[copy-stack-src] copied @btst/stack/dist/packages/ui → .btst-stack-ui", - ); + console.log(`[copy-stack-src] copied ${uiSrc} → ${uiDest}`); } else { + console.log(`[copy-stack-src] ${uiSrc} not found, skipping`); +} + +// When running inside the monorepo, the workspace-built dist/plugins/ has +// @workspace/ui imports already inlined by postbuild.cjs. Copy those files +// over the npm-installed ones so the demo uses the correct, self-contained CSS. +// Outside the monorepo (StackBlitz/WebContainers), this path won't exist and +// the step is silently skipped — the published npm package handles it instead. +const workspacePluginsDist = "../../packages/stack/dist/plugins"; +const npmPluginsDist = "node_modules/@btst/stack/dist/plugins"; +if (existsSync(workspacePluginsDist)) { + await cp(workspacePluginsDist, npmPluginsDist, { recursive: true }); console.log( - "[copy-stack-src] node_modules/@btst/stack/dist/packages/ui not found, skipping", + `[copy-stack-src] overlaid ${workspacePluginsDist} → ${npmPluginsDist}`, ); } diff --git a/demos/ui-builder/next-env.d.ts b/demos/ui-builder/next-env.d.ts index 9edff1c7..1b3be084 100644 --- a/demos/ui-builder/next-env.d.ts +++ b/demos/ui-builder/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/demos/ui-builder/next.config.ts b/demos/ui-builder/next.config.ts index d31197a4..cb651cdc 100644 --- a/demos/ui-builder/next.config.ts +++ b/demos/ui-builder/next.config.ts @@ -1,11 +1,5 @@ import type { NextConfig } from "next"; -const nextConfig: NextConfig = { - async redirects() { - return [ - { source: "/", destination: "/pages/ui-builder", permanent: false }, - ]; - }, -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/demos/ui-builder/package.json b/demos/ui-builder/package.json index ca8d856b..a45e7ec8 100644 --- a/demos/ui-builder/package.json +++ b/demos/ui-builder/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.2", + "@btst/stack": "^2.5.3", "@btst/yar": "^1.2.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/demos/ui-builder/tsconfig.json b/demos/ui-builder/tsconfig.json index 3a13f90a..0352f50e 100644 --- a/demos/ui-builder/tsconfig.json +++ b/demos/ui-builder/tsconfig.json @@ -11,7 +11,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { diff --git a/packages/stack/package.json b/packages/stack/package.json index 749c22ca..de6181b9 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -1,6 +1,6 @@ { "name": "@btst/stack", - "version": "2.5.2", + "version": "2.5.3", "description": "A composable, plugin-based library for building full-stack applications.", "repository": { "type": "git", diff --git a/packages/stack/scripts/postbuild.cjs b/packages/stack/scripts/postbuild.cjs index e9e054fc..740b0b65 100644 --- a/packages/stack/scripts/postbuild.cjs +++ b/packages/stack/scripts/postbuild.cjs @@ -2,9 +2,9 @@ /* Post-build step for BTST package. - Copies all .css files from src/plugins/** to dist/plugins/** preserving structure - - Resolves @workspace/ui/... CSS imports in dist files by inlining the referenced - content into dist/plugins/shared/ and rewriting the import path — so consumers - outside the monorepo (e.g. npm installs, StackBlitz) never see workspace imports + - Resolves @workspace/ui/... CSS imports by inlining the referenced content directly + into each dist CSS file — producing fully self-contained files with no workspace + references, so npm consumers and StackBlitz never see unresolvable imports - Executes optional per-plugin postbuild scripts if present at: src/plugins//postbuild.(js|cjs|mjs) */ @@ -81,14 +81,15 @@ function inlineCssImports(cssContent, baseDir, seen = new Set()) { /** * After all plugin CSS files are copied to dist, scan them for - * @import "@workspace/ui/..." declarations. For each unique specifier found: - * 1. Resolve the actual file via packages/ui/package.json exports map - * 2. Inline all relative sub-imports recursively into one blob - * 3. Write the blob to dist/plugins/shared/.css - * 4. Rewrite the @workspace/ui/... import in the dist CSS to the relative path + * @import "@workspace/ui/..." declarations and inline the referenced CSS + * content directly — replacing the import with the full CSS text. * - * This keeps source files using proper workspace imports while ensuring the - * published package is self-contained. + * This makes every dist CSS file fully self-contained: no @workspace/ui + * references survive into the published npm package, so consumers outside + * the monorepo (npm installs, StackBlitz) never see unresolvable imports. + * + * Strategy: inline rather than redirect to a shared/ file, so the package + * works correctly with no extra files and no relative-path assumptions. */ function resolveWorkspaceCssImports() { const UI_PKG_DIR = path.resolve(ROOT, "..", "ui"); @@ -112,10 +113,10 @@ function resolveWorkspaceCssImports() { } const WORKSPACE_IMPORT_RE = /@import\s+"(@workspace\/ui[^"]+)";?[ \t]*/g; - const sharedDir = path.join(DIST_PLUGINS_DIR, "shared"); - // specifier → filename written in dist/plugins/shared/ - const generated = new Map(); + // Cache resolved+inlined content per specifier so each unique import is + // only read and processed once even if referenced by multiple dist files. + const cache = new Map(); if (!fs.existsSync(DIST_PLUGINS_DIR)) return; @@ -129,35 +130,27 @@ function resolveWorkspaceCssImports() { let modified = false; content = content.replace(WORKSPACE_IMPORT_RE, (match, specifier) => { - if (!generated.has(specifier)) { + if (!cache.has(specifier)) { const resolvedPath = resolveUiSpecifier(specifier); if (!resolvedPath || !fs.existsSync(resolvedPath)) { console.warn( `@btst/stack: could not resolve workspace import: ${specifier}`, ); + cache.set(specifier, null); return match; } const raw = fs.readFileSync(resolvedPath, "utf8"); + // Recursively inline any relative sub-imports within the resolved file const inlined = inlineCssImports(raw, path.dirname(resolvedPath)); - // Derive a stable filename from the specifier - const slug = specifier - .replace("@workspace/ui/", "") - .replace(/\//g, "-"); - ensureDir(sharedDir); - const destPath = path.join(sharedDir, slug); - fs.writeFileSync(destPath, inlined); - generated.set(specifier, slug); + cache.set(specifier, inlined); console.log( - `@btst/stack: resolved workspace import "${specifier}" → shared/${slug}`, + `@btst/stack: inlined workspace import "${specifier}" into ${path.relative(DIST_PLUGINS_DIR, filePath)}`, ); } - const slug = generated.get(specifier); - const rel = path - .relative(path.dirname(filePath), path.join(sharedDir, slug)) - .replace(/\\/g, "/"); - const relNormalized = rel.startsWith(".") ? rel : `./${rel}`; + const inlined = cache.get(specifier); + if (inlined === null) return match; // could not resolve — keep original modified = true; - return `@import "${relNormalized}";`; + return inlined; }); if (modified) { diff --git a/packages/stack/src/plugins/route-docs/generator.ts b/packages/stack/src/plugins/route-docs/generator.ts index 3e02f260..50ad9bfd 100644 --- a/packages/stack/src/plugins/route-docs/generator.ts +++ b/packages/stack/src/plugins/route-docs/generator.ts @@ -143,7 +143,9 @@ function processZodType(zodType: z.ZodType): Record { // Handle default - unwrap and process inner type if (zodType instanceof z.ZodDefault) { const innerType = (zodType as any)._def?.innerType; - const defaultValue = (zodType as any)._def?.defaultValue?.(); + const rawDefault = (zodType as any)._def?.defaultValue; + const defaultValue = + typeof rawDefault === "function" ? rawDefault() : rawDefault; if (innerType) { const innerSchema = processZodType(innerType); if (defaultValue !== undefined) {