Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion .agents/skills/btst-build-config/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ The `postbuild.cjs` script auto-discovers and copies them — no manual registra

## Updating all three codegen projects

When adding a new plugin or changing plugin config, update ALL three:
When adding a plugin, changing plugin config, or changing a framework entry
point, update ALL three generated projects:

**Next.js** (`codegen-projects/nextjs/`)
- `lib/stack.ts` — backend plugin registration
Expand All @@ -81,6 +82,38 @@ When adding a new plugin or changing plugin config, update ALL three:
- `src/routes/pages/route.tsx`
- `src/styles.css`

Keep generated framework routes on the v3 entry factories:

| Framework | API route | Page route | Provider router |
|---|---|---|---|
| Next.js | `toNextRouteHandlers` | `createNextPage` | `nextRouter()` |
| React Router | `toReactRouterHandlers` | `createReactRouterPage` | `reactRouter()` |
| TanStack Start | `toTanStackHandlers` | `createTanStackPageOptions` | `tanstackRouter()` |

Do not generate hand-written route resolution, loader/meta ordering,
dehydration, or 404 plumbing. The entry factories own that behavior.

Generated layouts must configure shared services once on `StackProvider`:

```tsx
<StackProvider<PluginOverrides>
basePath="/pages"
router={frameworkRouter()}
api={{ baseURL, basePath: "/api/data" }}
auth={authProvider}
overrides={{
blog: { uploadImage },
}}
>
{children}
</StackProvider>
```

Only plugin-specific values belong in `overrides`. Never generate `Link`,
`Image`, `navigate`, `refresh`, API paths, identity, or login values inside a
built-in plugin override. Client plugin factory fields such as `apiBaseURL`,
`siteBaseURL`, and `queryClient` remain necessary for SSR loaders and metadata.

### Override type registration (in each layout)

```typescript
Expand All @@ -93,6 +126,9 @@ type PluginOverrides = {
}
```

Register only the plugin's public override type. Do not recreate removed v2
framework, API, guard, or identity fields in local intersection types.

## Adding shared UI components (@workspace/ui)

Components live in `packages/ui/src/components/`. Add via shadcn CLI:
Expand Down Expand Up @@ -122,3 +158,6 @@ pnpm turbo clean && pnpm build
- **Build cache** — run `pnpm turbo clean` if changes aren't reflected in codegen projects after `pnpm build`.
- **CSS not loading** — ensure `"./plugins/{name}/css"` entry exists in `package.json` exports; `postbuild.cjs` handles the rest automatically.
- **`@workspace/ui` sub-path components** — if a new component imports from a directory (not a single file), add it to `EXTERNAL_REGISTRY_COMPONENTS` in `build-registry.ts`.
- **Stale v2 templates** — generated routes must use framework entry factories,
and generated layouts must keep shared `router`, `api`, and `auth` services at
the top level of `StackProvider`.
2 changes: 1 addition & 1 deletion .agents/skills/btst-client-plugin-dev/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function MyPage({ id }: { id: string }) {
## Client hooks (lifecycle) example

```typescript
// In defineClientPlugin config:
// In the config passed to myClientPlugin(config):
hooks: {
beforeLoadDetail: async (id, ctx) => {
const session = await getSession(ctx.headers)
Expand Down
108 changes: 64 additions & 44 deletions .agents/skills/btst-client-plugin-dev/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,26 +91,34 @@ function createMyMeta(id: string, config: MyClientConfig) {

---

## Query Keys Factory (query-keys.ts)
## Resource declaration and query keys (query-keys.ts)

```typescript
import { mergeQueryKeys, createQueryKeys } from "@lukemorales/query-key-factory"
import { createApiClient } from "@btst/stack/client"
import type { MyApiRouter } from "./api/plugin"

export function createMyQueryKeys(client: ReturnType<typeof createApiClient<MyApiRouter>>, headers?: HeadersInit) {
return mergeQueryKeys(
createQueryKeys("myPlugin", {
list: () => ({
queryKey: ["list"],
queryFn: async () => client.items.list({ headers }),
}),
detail: (id: string) => ({
queryKey: [id],
queryFn: async () => client.items.get(id, { headers }),
}),
})
)
import {
createResourceQueryKeys,
type ResourceClient,
type ResourcesDeclaration,
} from "@btst/stack/plugins/client"

export const myResources = {
items: {
queries: {
list: {
path: "/items",
select: (data: any) => data?.items ?? [],
},
detail: {
path: "/items",
query: (id: string) => ({ id }),
key: (id: string) => [id],
select: (data: any) => data?.item ?? null,
},
},
},
} satisfies ResourcesDeclaration

export function createMyQueryKeys(client: ResourceClient, headers?: HeadersInit) {
return createResourceQueryKeys(client, myResources, headers)
}
```

Expand All @@ -119,39 +127,51 @@ export function createMyQueryKeys(client: ReturnType<typeof createApiClient<MyAp
## defineClientPlugin shape (client/plugin.tsx)

```typescript
import { defineClientPlugin, createRoute } from "@btst/stack/plugins"
import {
defineClientPlugin,
defineRoute,
defineRoutes,
} from "@btst/stack/plugins/client"
import type { QueryClient } from "@tanstack/react-query"
import { lazy } from "react"

export interface MyClientConfig {
queryClient: QueryClient
apiBaseURL: string
apiBasePath: string
siteBaseURL: string
siteBasePath: string
headers?: HeadersInit
hooks?: MyClientHooks
seo?: MySeoConfig
}

const ListPage = lazy(() =>
import("./components/pages/list-page").then(m => ({ default: m.ListPageComponent }))
)
const DetailPage = lazy(() =>
import("./components/pages/detail-page").then(m => ({ default: m.DetailPageComponent }))
)

export const myClientPlugin = defineClientPlugin({
name: "my-plugin",
config: (overrides) => ({
queryClient: overrides.queryClient,
apiBaseURL: overrides.apiBaseURL,
apiBasePath: overrides.apiBasePath,
siteBaseURL: overrides.siteBaseURL,
siteBasePath: overrides.siteBasePath,
hooks: overrides.hooks,
headers: overrides.headers,
seo: overrides.seo,
}),
routes: (config) => ({
list: createRoute("/my-plugin", () => ({
PageComponent: () => <ListPage />,
loader: createListLoader(config),
meta: createListMeta(config),
})),
detail: createRoute("/my-plugin/:id", ({ params }) => ({
PageComponent: () => <DetailPage id={params.id} />,
loader: createDetailLoader(params.id, config),
meta: createDetailMeta(params.id, config),
})),
}),
})
export const myClientPlugin = (config: MyClientConfig) =>
defineClientPlugin({
name: "my-plugin",
routes: () =>
defineRoutes({
list: defineRoute("/my-plugin", {
page: ListPage,
loader: createListLoader(config),
meta: createListMeta(config),
}),
detail: defineRoute("/my-plugin/:id", {
page: ({ params }) => <DetailPage id={params.id} />,
loader: ({ params }) => createDetailLoader(params.id, config)(),
meta: ({ params }) => createDetailMeta(params.id, config)(),
}),
}),
})
```

`MyClientConfig` is constructed in `getStackClient(queryClient)`. Do not
derive it from `StackProvider` overrides: the plugin factory runs during SSR,
while provider overrides are browser-runtime, plugin-specific customization.
24 changes: 17 additions & 7 deletions .agents/skills/btst-client-plugin-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,16 @@ Rules:

## Route anatomy

Each route returns exactly three things:
Use `defineRoute` / `defineRoutes` for new routes. The `page`, `loader`, and
`meta` handlers on a parameterized route each receive the route context:

```typescript
routes: (config) => ({
myRoute: createRoute("/path/:id", ({ params }) => ({
PageComponent: () => <MyPageComponent id={params.id} />,
loader: createMyLoader(params.id, config), // SSR only
meta: createMyMeta(params.id, config), // SEO tags
})),
routes: () => defineRoutes({
myRoute: defineRoute("/path/:id", {
page: ({ params }) => <MyPageComponent id={params.id} />,
loader: ({ params }) => createMyLoader(params.id, config)(), // SSR only
meta: ({ params }) => createMyMeta(params.id, config)(), // SEO tags
}),
})
```

Expand Down Expand Up @@ -197,9 +198,18 @@ type PluginOverrides = {
}
```

The client plugin factory still receives the QueryClient, absolute site/API
URLs, optional SSR headers, SEO, and loader hooks because loaders and metadata
run outside React Context. Keep that factory config independent from
`StackProvider` overrides; never add a `config(overrides)` adapter that copies
provider fields back into the plugin.

## Gotchas

- **Framework config in plugin overrides** — `Link`, `Image`, navigation, refresh, and client API paths come from the top-level `StackProvider`.
- **Building plugin config from overrides** — plugin factory config is created
in `getStackClient(queryClient)`; provider overrides are browser-runtime
customization only.
- **`staleTime: Infinity`** — use for data that should not auto-refetch.
- **Next.js Link href undefined** — use `href={href || "#"}` pattern.
- **Suspense errors not caught** — add `if (error && !isFetching) throw error` in every suspense hook.
Expand Down
Loading
Loading