@vexcms/next 0.1.0-alpha.22 → 0.1.0-alpha.24

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/README.md CHANGED
@@ -11,7 +11,8 @@ pnpm add @vexcms/next@alpha
11
11
  ## Quick Setup
12
12
 
13
13
  ```tsx
14
- // app/admin/clientProviders.tsx
14
+ // components/providers/client.tsx — mounted at the APP root, not just the admin
15
+ // route: the public site reads the same config for live preview.
15
16
  "use client"
16
17
  import { VexConfigProvider } from "@vexcms/react"
17
18
  import config from "@/vex.config"
@@ -26,15 +27,10 @@ export function ClientProviders({ children }: { children: React.ReactNode }) {
26
27
  import { NextAdminLayout } from "@vexcms/next/client"
27
28
  import { getCurrentUser } from "@/auth/serverUtils"
28
29
 
29
- import { ClientProviders } from "./clientProviders"
30
-
31
30
  export default async function AdminRootLayout({ children }: { children: React.ReactNode }) {
32
31
  const user = await getCurrentUser()
33
- return (
34
- <ClientProviders>
35
- <NextAdminLayout user={user ?? undefined}>{children}</NextAdminLayout>
36
- </ClientProviders>
37
- )
32
+ // No provider here: the root layout's `ClientProviders` already mounted it.
33
+ return <NextAdminLayout user={user ?? undefined}>{children}</NextAdminLayout>
38
34
  }
39
35
  ```
40
36
 
@@ -70,11 +66,11 @@ export default function AdminPage({
70
66
 
71
67
  ### Roadmap note
72
68
 
73
- Versioning, drafts, and live preview are not implemented yet — both are in progress on the [roadmap](https://docs.vexcms.dev). A global's `versions.drafts` option parses and is stored on the resolved config, but it is not enforced: every read still returns the live document. See [docs.vexcms.dev](https://docs.vexcms.dev) for current status.
69
+ Versioning and drafts are not implemented yet — in progress on the [roadmap](https://docs.vexcms.dev). Live preview ships: `NextAdminPage` resolves each document's preview URL (including a `{ server }` resolver that reads the database) and the edit views render the preview panel. A global's `versions.drafts` option parses and is stored on the resolved config, but it is not enforced: every read still returns the live document. See [docs.vexcms.dev](https://docs.vexcms.dev) for current status.
74
70
 
75
71
  ### Access Control
76
72
 
77
- - **Config provider** — `VexConfigProvider` (`@vexcms/react`) supplies the resolved client config — including `access` and `storage.clientUploads` — to the admin panel via React context, mounted once by your app around the admin route
73
+ - **Config provider** — `VexConfigProvider` (`@vexcms/react`) supplies the resolved client config — including `access` and `storage.clientUploads` — to the admin panel via React context, mounted once by your app at the ROOT layout so the admin panel and the public site share one provenance (live preview reads its collections, globals, and `allowedOrigins` from the same context)
78
74
  - **Collection-level permissions** — `usePermission` gates create, read, update, and delete per collection and per global
79
75
  - **Field-level permissions** — a per-field access rule hides a read-denied input entirely and renders an update-denied one read-only; edit forms submit only the fields the user actually changed
80
76
  - **UI enforcement** — Buttons and actions are disabled or hidden when the current user lacks permission
@@ -8,8 +8,9 @@ import type { AdminUser } from "@vexcms/react";
8
8
  * client flash) and passes it, plus `user`/`organization`/`children`, to the
9
9
  * client leaf `NextAdminLayoutClient`. Config no longer crosses this
10
10
  * boundary at all — the admin panel's client tree gets its config from the
11
- * app's own `VexConfigProvider` mount (`clientProviders.tsx`), not from a
12
- * prop threaded through this layout.
11
+ * app's own root `VexConfigProvider` mount
12
+ * (`components/providers/client.tsx`), not from a prop threaded through this
13
+ * layout.
13
14
  *
14
15
  * Render it from your admin `layout.tsx`. Because this is a server component,
15
16
  * your `layout.tsx` may itself be an `async` server component (e.g. to run auth
@@ -1 +1 @@
1
- {"version":3,"file":"NextAdminLayout.d.ts","sourceRoot":"","sources":["../src/NextAdminLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE;IAC3C,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC,wCAaA"}
1
+ {"version":3,"file":"NextAdminLayout.d.ts","sourceRoot":"","sources":["../src/NextAdminLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE;IAC3C,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC,wCAaA"}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NextAdminLayout
3
- } from "./chunk-K4AAS5B5.js";
3
+ } from "./chunk-UMJTAD5A.js";
4
4
  export {
5
5
  NextAdminLayout
6
6
  };
@@ -1 +1 @@
1
- {"version":3,"file":"NextAdminPage.d.ts","sourceRoot":"","sources":["../src/NextAdminPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoC,SAAS,EAAoB,MAAM,cAAc,CAAC;AAqBlG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE;IACzC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,wCAwIA"}
1
+ {"version":3,"file":"NextAdminPage.d.ts","sourceRoot":"","sources":["../src/NextAdminPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoC,SAAS,EAAoB,MAAM,cAAc,CAAC;AAuDlG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE;IACzC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,wCAmLA"}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NextAdminPage
3
- } from "./chunk-L6N3D6L5.js";
3
+ } from "./chunk-DDH2TJTP.js";
4
4
  export {
5
5
  NextAdminPage
6
6
  };
@@ -81,8 +81,8 @@ export interface CreateVexRevalidateRouteProps {
81
81
  * ```
82
82
  */
83
83
  export declare function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {
84
- POST: (request: NextRequest) => Promise<NextResponse<{
84
+ POST: (request: NextRequest) => Promise<NextResponse<VexRevalidateResponse | {
85
85
  error: string;
86
- } | VexRevalidateResponse>>;
86
+ }>>;
87
87
  };
88
88
  //# sourceMappingURL=createVexRevalidateRoute.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createVexRevalidateRoute.d.ts","sourceRoot":"","sources":["../../src/cache/createVexRevalidateRoute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EAEd,SAAS,EACT,WAAW,EAIX,qBAAqB,EACtB,MAAM,cAAc,CAAC;AAQtB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,kDAAkD;AAClD,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,MAAM,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACvC,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IACnD;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;CAClF;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG;IAC9E,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,qBAAqB,CAAC,CAAC,CAAC;CAClG,CAiHA"}
1
+ {"version":3,"file":"createVexRevalidateRoute.d.ts","sourceRoot":"","sources":["../../src/cache/createVexRevalidateRoute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EAEd,SAAS,EACT,WAAW,EAIX,qBAAqB,EACtB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAS/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,kDAAkD;AAClD,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,MAAM,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACvC,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IACnD;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;CAClF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG;IAC9E,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,qBAAqB,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAClG,CAiHA"}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createVexRevalidateRoute,
3
3
  createVexServerClient
4
- } from "../chunk-3J7LCKFC.js";
4
+ } from "../chunk-IW5G5UJ6.js";
5
5
  export {
6
6
  createVexRevalidateRoute,
7
7
  createVexServerClient
@@ -1,9 +1,11 @@
1
1
  // src/NextAdminPage.tsx
2
2
  import {
3
+ appendLivePreviewParams,
3
4
  livePreviewLayoutCookieName,
4
5
  livePreviewPanelCookieName,
5
6
  readLivePreviewLayoutCookie,
6
7
  readLivePreviewPanelCookie,
8
+ resolveLivePreviewSettings,
7
9
  vexConvexApi
8
10
  } from "@vexcms/core";
9
11
  import {
@@ -18,6 +20,15 @@ import {
18
20
  import { fetchQuery } from "convex/nextjs";
19
21
  import { cookies } from "next/headers";
20
22
  import { jsx, jsxs } from "react/jsx-runtime";
23
+ async function resolveInitialPreviewUrl(props) {
24
+ const url = await fetchQuery(
25
+ vexConvexApi.livePreviewUrl,
26
+ props.args,
27
+ props.token ? { token: props.token } : void 0
28
+ );
29
+ if (!url) return void 0;
30
+ return appendLivePreviewParams({ url, collectionSlug: props.slug });
31
+ }
21
32
  async function NextAdminPage(props) {
22
33
  const { path = [] } = await props.params;
23
34
  const [collectionSlug, documentId] = path;
@@ -42,20 +53,33 @@ async function NextAdminPage(props) {
42
53
  }
43
54
  const global = await fetchQuery(
44
55
  vexConvexApi.globals.get,
45
- { slug: globalConfig.slug },
56
+ // Same args as `GlobalEditView`'s client query (`getGlobal` falls back to
57
+ // the published row for a caller without `readDrafts`).
58
+ { slug: globalConfig.slug, drafts: globalConfig.versions.drafts },
46
59
  props.token ? { token: props.token } : void 0
47
60
  );
61
+ const globalLivePreview = resolveLivePreviewSettings({
62
+ config: props.config.admin.livePreview,
63
+ kind: "global",
64
+ slug: globalConfig.slug,
65
+ admin: globalConfig.admin.livePreview
66
+ });
48
67
  return /* @__PURE__ */ jsx(
49
68
  GlobalEditView,
50
69
  {
51
70
  global: globalConfig.slug,
52
71
  initialData: global,
53
- initialPreviewPanelOpen: readLivePreviewPanelCookie({
72
+ initialPreviewPanelOpen: globalLivePreview !== void 0 && readLivePreviewPanelCookie({
54
73
  cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: globalConfig.slug }))?.value,
55
- defaultOpen: globalConfig.admin.livePreview?.defaultOpen ?? false
74
+ defaultOpen: globalLivePreview.defaultOpen
56
75
  }),
57
76
  initialPreviewPanelSize: readLivePreviewLayoutCookie({
58
77
  cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: globalConfig.slug }))?.value
78
+ }),
79
+ initialPreviewUrl: globalLivePreview === void 0 ? void 0 : await resolveInitialPreviewUrl({
80
+ args: { collection: globalConfig.slug, kind: "global", values: {} },
81
+ slug: globalConfig.slug,
82
+ token: props.token
59
83
  })
60
84
  }
61
85
  );
@@ -111,21 +135,34 @@ async function NextAdminPage(props) {
111
135
  if (collection && documentId) {
112
136
  const initialData2 = await fetchQuery(
113
137
  vexConvexApi.get,
114
- { id: documentId, collection: collection.slug },
138
+ // Same args as `CollectionEditView`'s client query (`get` falls back to
139
+ // published-only for a caller without `readDrafts`).
140
+ { id: documentId, collection: collection.slug, drafts: collection.versions.drafts },
115
141
  props.token ? { token: props.token } : void 0
116
142
  );
143
+ const collectionLivePreview = resolveLivePreviewSettings({
144
+ config: props.config.admin.livePreview,
145
+ kind: "collection",
146
+ slug: collection.slug,
147
+ admin: collection.admin.livePreview
148
+ });
117
149
  return /* @__PURE__ */ jsx(
118
150
  CollectionEditView,
119
151
  {
120
152
  collection: collection.slug,
121
153
  documentId,
122
154
  initialData: initialData2,
123
- initialPreviewPanelOpen: readLivePreviewPanelCookie({
155
+ initialPreviewPanelOpen: collectionLivePreview !== void 0 && readLivePreviewPanelCookie({
124
156
  cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: collection.slug }))?.value,
125
- defaultOpen: collection.admin.livePreview?.defaultOpen ?? false
157
+ defaultOpen: collectionLivePreview.defaultOpen
126
158
  }),
127
159
  initialPreviewPanelSize: readLivePreviewLayoutCookie({
128
160
  cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: collection.slug }))?.value
161
+ }),
162
+ initialPreviewUrl: collectionLivePreview === void 0 ? void 0 : await resolveInitialPreviewUrl({
163
+ args: { collection: collection.slug, kind: "collection", documentId, values: {} },
164
+ slug: collection.slug,
165
+ token: props.token
129
166
  })
130
167
  }
131
168
  );
@@ -141,6 +178,9 @@ async function NextAdminPage(props) {
141
178
  vexConvexApi.findPaginated,
142
179
  {
143
180
  collection: collectionSlug,
181
+ // Same args as `CollectionListView`'s client query, so the hydrated
182
+ // initial data belongs to the query the client subscribes to.
183
+ drafts: collection.versions.drafts ? true : void 0,
144
184
  depth: 1,
145
185
  paginationOpts: { cursor: null, numItems, totalDocs: true }
146
186
  },
@@ -152,4 +192,4 @@ async function NextAdminPage(props) {
152
192
  export {
153
193
  NextAdminPage
154
194
  };
155
- //# sourceMappingURL=chunk-L6N3D6L5.js.map
195
+ //# sourceMappingURL=chunk-DDH2TJTP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/NextAdminPage.tsx"],"sourcesContent":["import type { CollectionSlug, PaginationResult, VexConfig, VexMediaDocument } from \"@vexcms/core\";\n\nimport {\n appendLivePreviewParams,\n livePreviewLayoutCookieName,\n livePreviewPanelCookieName,\n readLivePreviewLayoutCookie,\n readLivePreviewPanelCookie,\n resolveLivePreviewSettings,\n vexConvexApi,\n} from \"@vexcms/core\";\nimport {\n CollectionEditView,\n CollectionListView,\n DashboardView,\n GlobalEditView,\n GlobalsListView,\n MediaCollectionEditView,\n MediaCollectionListView,\n} from \"@vexcms/react\";\nimport { fetchQuery } from \"convex/nextjs\";\nimport { cookies } from \"next/headers\";\n\n/**\n * Resolves a `{ server }` preview URL for first paint.\n *\n * Two things the caller must not have to remember. First, the query only\n * answers for the `{ server }` form — the string and client forms resolve\n * synchronously in the browser and return `null` here, so the iframe falls back\n * to its own resolution. Second, the resolved URL is a plain public URL: it\n * needs `vexLivePreview=1` appended or the preview loads an ordinary page that\n * listens to nothing, which looks like \"live preview is broken\" with no error\n * anywhere. Appending it here keeps the server path identical to the client one.\n *\n * @param props.args - Query arguments identifying the document or global.\n * @param props.slug - Collection or global slug, for the temp-id params.\n * @param props.token - Auth token forwarded to Convex.\n * @returns The preview-ready URL, or `undefined` when the config resolves in\n * the browser instead.\n */\nasync function resolveInitialPreviewUrl(props: {\n args: Parameters<typeof fetchQuery<typeof vexConvexApi.livePreviewUrl>>[1];\n slug: string;\n token?: string;\n}): Promise<string | undefined> {\n const url = await fetchQuery(\n vexConvexApi.livePreviewUrl,\n props.args,\n props.token ? { token: props.token } : undefined,\n );\n if (!url) return undefined;\n\n return appendLivePreviewParams({ url, collectionSlug: props.slug });\n}\n\n/**\n * VexCMS admin page server component for Next.js.\n *\n * An `async` server component that routes by the `[[...slug]]` catch-all\n * params, prefetches Convex data via `fetchQuery`, and renders the correct\n * view component. Does **not** include a layout wrapper — `NextAdminLayout`\n * in `app/admin/layout.tsx` owns the persistent shell.\n *\n * **Route mapping:**\n * | `path` array | View |\n * |---|---|\n * | `[]` or undefined | `DashboardView` |\n * | `[collectionSlug]` | `CollectionListView` with preloaded docs |\n * | `[collectionSlug, \"new\"]` | `CollectionEditView` (empty form) |\n * | `[collectionSlug, documentId]` | `CollectionEditView` with preloaded doc |\n *\n * @param props - Component props\n * @param props.config - The resolved server `VexConfig`, typically the default export of\n * `vex.config.server.ts` — not the client-safe `vex.config.ts`.\n * @param props.params - Next.js 15 async params `{ path?: string[] }`\n * @param props.token - Optional session token forwarded to `fetchQuery` for authenticated reads.\n * @returns The appropriate admin view for the current URL path.\n *\n * @example\n * ```tsx\n * // app/admin/[[...slug]]/page.tsx\n * import { NextAdminPage } from \"@vexcms/next/server\";\n * import config from \"../../../../vex.config.server\";\n *\n * export default function AdminPage({\n * params,\n * }: {\n * params: Promise<{ path?: string[] }>;\n * }) {\n * return <NextAdminPage config={config} params={params} />;\n * }\n * ```\n */\nexport async function NextAdminPage(props: {\n config: VexConfig;\n params: Promise<{ path?: string[] }>;\n token?: string;\n}) {\n const { path = [] } = await props.params;\n const [collectionSlug, documentId] = path;\n const cookieStore = await cookies();\n\n if (!collectionSlug) {\n return <DashboardView />;\n }\n\n if (collectionSlug === \"globals\") {\n if (!documentId) {\n return <GlobalsListView />;\n }\n const globalConfig = props.config.globals.find((g) => g.slug === documentId);\n if (!globalConfig) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">Global &quot;{documentId}&quot; not found.</p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n const global = await fetchQuery(\n vexConvexApi.globals.get,\n // Same args as `GlobalEditView`'s client query (`getGlobal` falls back to\n // the published row for a caller without `readDrafts`).\n { slug: globalConfig.slug, drafts: globalConfig.versions.drafts },\n props.token ? { token: props.token } : undefined,\n );\n const globalLivePreview = resolveLivePreviewSettings({\n config: props.config.admin.livePreview,\n kind: \"global\",\n slug: globalConfig.slug,\n admin: globalConfig.admin.livePreview,\n });\n return (\n <GlobalEditView\n global={globalConfig.slug}\n initialData={global}\n initialPreviewPanelOpen={\n globalLivePreview !== undefined &&\n readLivePreviewPanelCookie({\n cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: globalConfig.slug }))\n ?.value,\n defaultOpen: globalLivePreview.defaultOpen,\n })\n }\n initialPreviewPanelSize={readLivePreviewLayoutCookie({\n cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: globalConfig.slug }))\n ?.value,\n })}\n initialPreviewUrl={\n globalLivePreview === undefined\n ? undefined\n : await resolveInitialPreviewUrl({\n args: { collection: globalConfig.slug, kind: \"global\", values: {} },\n slug: globalConfig.slug,\n token: props.token,\n })\n }\n />\n );\n }\n\n const collection = props.config.collections.find((c) => c.slug === collectionSlug);\n const mediaCollection = props.config.mediaCollections.find((mc) => mc.slug === collectionSlug);\n\n if (!collection && !mediaCollection) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">\n Collection &quot;{collectionSlug}&quot; not found.\n </p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n\n if (mediaCollection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n { id: documentId, collection: mediaCollection.slug },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionEditView\n collection={mediaCollection.slug}\n documentId={documentId}\n initialData={initialData as null | VexMediaDocument}\n />\n );\n }\n\n if (mediaCollection) {\n const numItems = Math.max(\n mediaCollection.admin.table.serverPageSize,\n mediaCollection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.find,\n {\n collection: collectionSlug as CollectionSlug,\n paginationOpts: { numItems, totalDocs: true, cursor: null },\n },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionListView\n collection={mediaCollection.slug}\n initialData={initialData as PaginationResult<VexMediaDocument>}\n />\n );\n }\n\n if (collection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n // Same args as `CollectionEditView`'s client query (`get` falls back to\n // published-only for a caller without `readDrafts`).\n { id: documentId, collection: collection.slug, drafts: collection.versions.drafts },\n props.token ? { token: props.token } : undefined,\n );\n const collectionLivePreview = resolveLivePreviewSettings({\n config: props.config.admin.livePreview,\n kind: \"collection\",\n slug: collection.slug,\n admin: collection.admin.livePreview,\n });\n return (\n <CollectionEditView\n collection={collection.slug}\n documentId={documentId}\n initialData={initialData}\n initialPreviewPanelOpen={\n collectionLivePreview !== undefined &&\n readLivePreviewPanelCookie({\n cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: collection.slug }))\n ?.value,\n defaultOpen: collectionLivePreview.defaultOpen,\n })\n }\n initialPreviewPanelSize={readLivePreviewLayoutCookie({\n cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: collection.slug }))\n ?.value,\n })}\n initialPreviewUrl={\n collectionLivePreview === undefined\n ? undefined\n : await resolveInitialPreviewUrl({\n args: { collection: collection.slug, kind: \"collection\", documentId, values: {} },\n slug: collection.slug,\n token: props.token,\n })\n }\n />\n );\n }\n\n if (!collection) {\n throw new Error(\"invalid collection slug\");\n }\n\n const numItems = Math.max(\n collection.admin.table.serverPageSize,\n collection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.findPaginated,\n {\n collection: collectionSlug as CollectionSlug,\n // Same args as `CollectionListView`'s client query, so the hydrated\n // initial data belongs to the query the client subscribes to.\n drafts: collection.versions.drafts ? true : undefined,\n depth: 1,\n paginationOpts: { cursor: null, numItems, totalDocs: true },\n },\n props.token ? { token: props.token } : undefined,\n );\n return <CollectionListView collection={collection.slug} initialData={initialData} />;\n}\n"],"mappings":"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","names":["initialData","numItems"]}
@@ -7,11 +7,6 @@ import {
7
7
  } from "@vexcms/core";
8
8
  import { revalidatePath } from "next/cache";
9
9
  import { NextResponse } from "next/server";
10
- function toCrudAction(operation) {
11
- if (operation === "remove") return "delete";
12
- if (operation === "upsert") return "update";
13
- return operation;
14
- }
15
10
  function createVexRevalidateRoute(props) {
16
11
  return {
17
12
  async POST(request) {
@@ -100,6 +95,15 @@ function createVexRevalidateRoute(props) {
100
95
  }
101
96
  };
102
97
  }
98
+ function toCrudAction(operation) {
99
+ if (operation === "remove") {
100
+ return "delete";
101
+ }
102
+ if (operation === "upsert" || operation === "publish" || operation === "unpublish") {
103
+ return "update";
104
+ }
105
+ return operation;
106
+ }
103
107
 
104
108
  // src/cache/createVexServerClient.ts
105
109
  import { ConvexHttpClient } from "convex/browser";
@@ -126,4 +130,4 @@ export {
126
130
  createVexRevalidateRoute,
127
131
  createVexServerClient
128
132
  };
129
- //# sourceMappingURL=chunk-3J7LCKFC.js.map
133
+ //# sourceMappingURL=chunk-IW5G5UJ6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cache/createVexRevalidateRoute.ts","../src/cache/createVexServerClient.ts"],"sourcesContent":["import type {\n CollectionSlug,\n CrudWriteAction,\n VexConfig,\n VexDocument,\n VexMutationOperation,\n VexRevalidateChange,\n VexRevalidateRequest,\n VexRevalidateResponse,\n} from \"@vexcms/core\";\nimport type { NextRequest } from \"next/server\";\n\nimport {\n hasPermission,\n PERMISSION_SCOPES,\n resolveTargets,\n VEX_REVALIDATE_BATCH_SIZE,\n} from \"@vexcms/core\";\nimport { revalidatePath } from \"next/cache\";\nimport { NextResponse } from \"next/server\";\n\n/** Props for {@link createVexRevalidateRoute}. */\nexport interface CreateVexRevalidateRouteProps {\n /**\n * The resolved VexCMS config. Supplies `access` for the write-permission\n * check and `routes.map` for target resolution.\n */\n config: VexConfig;\n /**\n * Resolves the authenticated user and active organization for the current\n * request. Wire to `() => fetchAuthQuery(api.auth.api.getUserOrg, {})` — the\n * same call the admin panel route already makes.\n */\n getAuth: () => Promise<{\n organization?: Record<string, unknown>;\n user: null | Record<string, unknown>;\n }>;\n /**\n * Reads the caller's session token for the current request. Wire to the host\n * app's `getToken` export (`convexBetterAuthNextJs`'s output, re-exported\n * from e.g. `~/auth/server`). A falsy return means \"no session\".\n */\n getToken: () => Promise<null | string | undefined>;\n /**\n * Lists every document in a collection, for the collection-wide purge\n * (`{ collection, all: true }`). Wire to a read through the app's own\n * `publishedSlugs`-style query — the factory cannot know an app's generated\n * Convex `api`, which is why this is injected like `getToken`/`getAuth`.\n *\n * Omit to leave the collection-wide branch unwired: it then purges nothing\n * and reports that in `errors`, rather than silently succeeding.\n */\n listCollection?: (collection: CollectionSlug) => Promise<Partial<VexDocument>[]>;\n}\n\n/**\n * Creates a Next.js route handler that purges the cached paths affected by a\n * document write. The admin panel calls this via `useVexMutation` after every\n * successful create/update/delete, same-origin, as a relative fetch — no URL\n * config, no shared secret.\n *\n * **Session-authorized, not secret-authorized.** The caller is a signed-in\n * admin, so this reuses `getToken`/`getAuth` exactly like the admin panel route\n * does, rather than introducing an env var or a shared secret. There is no\n * API-key fallback: a caller with no browser session cannot reach this route as\n * designed.\n *\n * **Path-based, not tag-based.** `revalidatePath` is the only purge mechanism.\n * `cacheComponents: true` — required for `cacheTag`/`revalidateTag` — is\n * incompatible with the `dynamic`/`runtime` segment configs the auth and admin\n * routes require (measured: 5 files fail to build with it enabled), so\n * `revalidateTag` is not an option in this codebase. Do not \"upgrade\" this to\n * tag-based revalidation without first removing that segment-config dependency\n * everywhere.\n *\n * **Never fails the caller past authorization.** A missing session returns 401,\n * a session without write permission 403, and an oversized `changes` batch 413\n * — all three reject before any map work runs. Past that point the handler\n * never throws or returns 5xx: an unconfigured map, a throwing map, or an\n * individual `revalidatePath` failure all resolve to 200 with the failures\n * reported in the body. A failed purge is a stale page; failing the endpoint\n * would only teach the fire-and-forget caller to retry pointlessly.\n *\n * @param props - Config and the host app's auth accessors.\n * @returns `{ POST }` — mount directly as the route module's named export.\n *\n * @example\n * ```ts\n * // app/api/vex/revalidate/route.ts\n * import { api } from \"@convex/_generated/api\";\n * import { createVexRevalidateRoute } from \"@vexcms/next/cache\";\n * import { fetchAuthQuery, getToken } from \"~/auth/server\";\n * import config from \"~/vex.config\";\n *\n * export const { POST } = createVexRevalidateRoute({\n * config,\n * getToken,\n * getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),\n * });\n * ```\n */\nexport function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {\n POST: (request: NextRequest) => Promise<NextResponse<VexRevalidateResponse | { error: string }>>;\n} {\n return {\n async POST(\n request: NextRequest,\n ): Promise<NextResponse<VexRevalidateResponse | { error: string }>> {\n const token = await props.getToken();\n if (token === null || token === undefined || token === \"\") {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n let body: VexRevalidateRequest;\n try {\n body = (await request.json()) as VexRevalidateRequest;\n } catch {\n return NextResponse.json({ error: \"Bad Request\" }, { status: 400 });\n }\n\n const { organization, user } = await props.getAuth();\n\n // One mapping, used for BOTH the permission check and target resolution.\n const operation: VexMutationOperation = \"all\" in body ? \"update\" : body.operation;\n const action = toCrudAction(operation);\n\n const allowed = hasPermission({\n access: props.config.access,\n action,\n organization,\n resource: body.collection,\n user,\n // A purge writes no document fields — this asks whether the caller\n // may act on the collection at all, not against a specific payload.\n scope: PERMISSION_SCOPES.any,\n });\n if (!allowed) {\n return NextResponse.json({ error: \"Forbidden\" }, { status: 403 });\n }\n\n const errors: unknown[] = [];\n let changes: VexRevalidateChange[];\n\n if (\"all\" in body) {\n // Server-derived, so no client-supplied batch to bound. Each document\n // is mapped as its current state.\n if (props.listCollection === undefined) {\n errors.push(\n new Error(\n \"createVexRevalidateRoute: a collection-wide purge was requested but `listCollection` is not wired\",\n ),\n );\n changes = [];\n } else {\n try {\n const docs = await props.listCollection(body.collection);\n changes = docs.map((doc) => ({ after: doc }));\n } catch (error) {\n // An unreachable deployment must not 500 a purge request.\n errors.push(error);\n changes = [];\n }\n }\n } else {\n changes = body.changes;\n if (changes.length > VEX_REVALIDATE_BATCH_SIZE) {\n return NextResponse.json(\n {\n error: `Cannot revalidate more than ${VEX_REVALIDATE_BATCH_SIZE} changes in a single request`,\n },\n { status: 413 },\n );\n }\n }\n\n const routesConfig = props.config.routes;\n if (routesConfig === undefined) {\n // A project with no route map still gets a working, harmless\n // endpoint rather than a 500. Any error already accumulated (e.g. an\n // unwired collection-wide purge) is still reported.\n return NextResponse.json({ errors, revalidated: [] }, { status: 200 });\n }\n\n const pool: string[] = [];\n\n for (const change of changes) {\n // A throwing map is contained inside `resolveTargets` — its failure\n // lands in that call's `errors` and never propagates here.\n const target = resolveTargets({\n after: change.after,\n before: change.before,\n collection: body.collection,\n map: routesConfig.map,\n operation: action,\n });\n pool.push(...target.paths);\n errors.push(...target.errors);\n }\n\n // Dedupe across the whole batch, preserving first-seen order — the same\n // contract `resolveTargets` applies to one document's before/after.\n const revalidated: string[] = [];\n for (const path of new Set(pool)) {\n try {\n revalidatePath(path);\n revalidated.push(path);\n } catch (error) {\n // `revalidatePath` is a Next API called here, so it is not covered by\n // `resolveTargets`' containment.\n errors.push(error);\n }\n }\n\n return NextResponse.json({ errors, revalidated }, { status: 200 });\n },\n };\n}\n\n/**\n * Maps the wire verb to the CRUD action `hasPermission` and `resolveTargets`\n * accept.\n *\n * `VexMutationOperation` is named for the `vexConvexApi` function that produced\n * the write (`vexConvexApi.remove`), while `hasPermission` only accepts\n * `CRUD_ACTIONS`. Mapping happens exactly once per request so the same value\n * drives both the permission check and target resolution.\n *\n * @param operation - The wire operation from the request body.\n * @returns The equivalent CRUD action.\n */\nfunction toCrudAction(operation: VexMutationOperation): CrudWriteAction {\n if (operation === \"remove\") {return \"delete\";}\n if (operation === \"upsert\" || operation === \"publish\" || operation === \"unpublish\") {return \"update\";}\n return operation;\n}\n","import type { FunctionReference, OptionalRestArgs } from \"convex/server\";\n\nimport { ConvexHttpClient } from \"convex/browser\";\nimport { cache } from \"react\";\n\nimport type { VexServerClientOptions, VexServerClient } from \"./types\";\n\n/**\n * Creates a server-only Convex read client safe to use in prerenderable\n * Next.js routes.\n *\n * Wraps `ConvexHttpClient` from `convex/browser` instead of `convex/nextjs`'s\n * `fetchQuery`, which hard-codes `client.setFetchOptions({ cache: \"no-store\" })`\n * — the reason every route calling it builds dynamic (`ƒ`). This client never\n * sets that option, so a route calling only `query()` can be prerendered and\n * governed by the route's own `export const revalidate`.\n *\n * Every `query()` call is deduped via `React.cache` for the lifetime of one\n * request: two identical `(query, args)` calls — e.g. `generateMetadata` and\n * its page both reading the same document — resolve from a single Convex round\n * trip instead of two.\n *\n * `React.cache` is instantiated fresh inside every `createVexServerClient()`\n * call, so that dedupe only holds across calls sharing the SAME client\n * instance — two clients built in two different modules cannot share a round\n * trip even when they read identical `(query, args)`. Each app therefore\n * creates exactly one instance, exported from its own `src/lib/vex.ts`, and\n * every route or component imports that shared `vex` rather than calling\n * `createVexServerClient()` itself.\n *\n * @param props - Client configuration.\n * @returns A {@link VexServerClient} whose `query` method is deduped per request.\n * @throws {Error} When no `url` is given and `process.env.NEXT_PUBLIC_CONVEX_URL`\n * is unset.\n */\nexport function createVexServerClient(props: VexServerClientOptions = {}): VexServerClient {\n const url = props.url ?? process.env.NEXT_PUBLIC_CONVEX_URL;\n if (url === undefined || url === \"\") {\n throw new Error(\n \"createVexServerClient: no Convex deployment URL. Pass `url`, or set NEXT_PUBLIC_CONVEX_URL.\",\n );\n }\n\n const client = new ConvexHttpClient(url, {\n skipConvexDeploymentUrlCheck: props.skipConvexDeploymentUrlCheck,\n });\n\n // Deliberately NOT `client.setFetchOptions({ cache: \"no-store\" })` — that is\n // exactly what `convex/nextjs`'s `fetchQuery` does and what forces every\n // consuming route dynamic.\n\n // `React.cache` keys on the argument list, so the entry is (query reference,\n // args) — never the reference alone. Args are normalised to a single value so\n // `query(fn)` and `query(fn, {})` share one entry.\n const cachedQuery = cache(\n async (query: FunctionReference<\"query\">, args: unknown): Promise<unknown> =>\n client.query(query, args as OptionalRestArgs<FunctionReference<\"query\">>[0]),\n );\n\n return {\n query: async (query, ...args) => cachedQuery(query, args[0] ?? {}),\n };\n}\n"],"mappings":"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","names":[]}
@@ -19,4 +19,4 @@ async function NextAdminLayout(props) {
19
19
  export {
20
20
  NextAdminLayout
21
21
  };
22
- //# sourceMappingURL=chunk-K4AAS5B5.js.map
22
+ //# sourceMappingURL=chunk-UMJTAD5A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/NextAdminLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport type { AdminUser } from \"@vexcms/react\";\nimport { NextAdminLayoutClient } from \"./NextAdminLayoutClient\";\nimport { cookies } from \"next/headers\";\n\n/**\n * Next.js admin layout shell for VexCMS.\n *\n * A **server component** that reads the `sidebar_state` cookie server-side\n * (so the sidebar's open/closed state is correct on first paint, with no\n * client flash) and passes it, plus `user`/`organization`/`children`, to the\n * client leaf `NextAdminLayoutClient`. Config no longer crosses this\n * boundary at all — the admin panel's client tree gets its config from the\n * app's own root `VexConfigProvider` mount\n * (`components/providers/client.tsx`), not from a prop threaded through this\n * layout.\n *\n * Render it from your admin `layout.tsx`. Because this is a server component,\n * your `layout.tsx` may itself be an `async` server component (e.g. to run auth\n * checks).\n *\n * @param props - Layout props.\n * @param props.children - The page content from `[[...slug]]/page.tsx`.\n * @param props.user - Current user for the admin shell.\n * @returns The rendered admin shell, wrapping `children` in the client boundary\n * ({@link NextAdminLayoutClient}) with the server-read sidebar cookie state.\n * @example\n * ```tsx\n * // app/admin/layout.tsx\n * import { NextAdminLayout } from \"@vexcms/next/client\";\n * import { getCurrentUser } from \"~/auth/serverUtils\";\n *\n * export default async function AdminLayout({ children }: { children: ReactNode }) {\n * const user = await getCurrentUser();\n * return (\n * <NextAdminLayout user={user ?? undefined}>\n * {children}\n * </NextAdminLayout>\n * );\n * }\n * ```\n */\nexport async function NextAdminLayout(props: {\n children: ReactNode;\n user?: AdminUser;\n organization?: Record<string, unknown>;\n}) {\n const cookieStore = await cookies();\n const sidebarOpen = String(cookieStore.get(\"sidebar_state\")?.value) === \"true\";\n\n return (\n <NextAdminLayoutClient\n user={props.user}\n organization={props.organization}\n sidebarOpen={sidebarOpen}\n >\n {props.children}\n </NextAdminLayoutClient>\n );\n}\n"],"mappings":";AAEA,SAAS,6BAA6B;AACtC,SAAS,eAAe;AAgDpB;AATJ,eAAsB,gBAAgB,OAInC;AACD,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,cAAc,OAAO,YAAY,IAAI,eAAe,GAAG,KAAK,MAAM;AAExE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,MAAM;AAAA,MACZ,cAAc,MAAM;AAAA,MACpB;AAAA,MAEC,gBAAM;AAAA;AAAA,EACT;AAEJ;","names":[]}
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  NextAdminPage
3
- } from "./chunk-L6N3D6L5.js";
3
+ } from "./chunk-DDH2TJTP.js";
4
4
  import {
5
5
  NextAdminLayout
6
- } from "./chunk-K4AAS5B5.js";
6
+ } from "./chunk-UMJTAD5A.js";
7
7
  import {
8
8
  createVexRevalidateRoute,
9
9
  createVexServerClient
10
- } from "./chunk-3J7LCKFC.js";
10
+ } from "./chunk-IW5G5UJ6.js";
11
11
  import {
12
12
  createVexRobots,
13
13
  createVexSitemap,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vexcms/next",
3
- "version": "0.1.0-alpha.22",
3
+ "version": "0.1.0-alpha.24",
4
4
  "description": "Next.js adapter and admin routes for VexCMS",
5
5
  "keywords": [
6
6
  "cms",
@@ -66,8 +66,8 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "lucide-react": "0.577.0",
69
- "@vexcms/core": "0.1.0-alpha.22",
70
- "@vexcms/react": "0.1.0-alpha.22"
69
+ "@vexcms/core": "0.1.0-alpha.24",
70
+ "@vexcms/react": "0.1.0-alpha.24"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@convex-dev/react-query": "0.1.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cache/createVexRevalidateRoute.ts","../src/cache/createVexServerClient.ts"],"sourcesContent":["import type {\n CollectionSlug,\n CrudWriteAction,\n VexConfig,\n VexDocument,\n VexMutationOperation,\n VexRevalidateChange,\n VexRevalidateRequest,\n VexRevalidateResponse,\n} from \"@vexcms/core\";\nimport {\n hasPermission,\n PERMISSION_SCOPES,\n resolveTargets,\n VEX_REVALIDATE_BATCH_SIZE,\n} from \"@vexcms/core\";\nimport { revalidatePath } from \"next/cache\";\nimport type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\n\n/** Props for {@link createVexRevalidateRoute}. */\nexport interface CreateVexRevalidateRouteProps {\n /**\n * The resolved VexCMS config. Supplies `access` for the write-permission\n * check and `routes.map` for target resolution.\n */\n config: VexConfig;\n /**\n * Resolves the authenticated user and active organization for the current\n * request. Wire to `() => fetchAuthQuery(api.auth.api.getUserOrg, {})` — the\n * same call the admin panel route already makes.\n */\n getAuth: () => Promise<{\n organization?: Record<string, unknown>;\n user: null | Record<string, unknown>;\n }>;\n /**\n * Reads the caller's session token for the current request. Wire to the host\n * app's `getToken` export (`convexBetterAuthNextJs`'s output, re-exported\n * from e.g. `~/auth/server`). A falsy return means \"no session\".\n */\n getToken: () => Promise<null | string | undefined>;\n /**\n * Lists every document in a collection, for the collection-wide purge\n * (`{ collection, all: true }`). Wire to a read through the app's own\n * `publishedSlugs`-style query — the factory cannot know an app's generated\n * Convex `api`, which is why this is injected like `getToken`/`getAuth`.\n *\n * Omit to leave the collection-wide branch unwired: it then purges nothing\n * and reports that in `errors`, rather than silently succeeding.\n */\n listCollection?: (collection: CollectionSlug) => Promise<Partial<VexDocument>[]>;\n}\n\n/**\n * Maps the wire verb to the CRUD action `hasPermission` and `resolveTargets`\n * accept.\n *\n * `VexMutationOperation` is named for the `vexConvexApi` function that produced\n * the write (`vexConvexApi.remove`), while `hasPermission` only accepts\n * `CRUD_ACTIONS`. Mapping happens exactly once per request so the same value\n * drives both the permission check and target resolution.\n *\n * @param operation - The wire operation from the request body.\n * @returns The equivalent CRUD action.\n */\nfunction toCrudAction(operation: VexMutationOperation): CrudWriteAction {\n if (operation === \"remove\") return \"delete\";\n if (operation === \"upsert\") return \"update\";\n return operation;\n}\n\n/**\n * Creates a Next.js route handler that purges the cached paths affected by a\n * document write. The admin panel calls this via `useVexMutation` after every\n * successful create/update/delete, same-origin, as a relative fetch — no URL\n * config, no shared secret.\n *\n * **Session-authorized, not secret-authorized.** The caller is a signed-in\n * admin, so this reuses `getToken`/`getAuth` exactly like the admin panel route\n * does, rather than introducing an env var or a shared secret. There is no\n * API-key fallback: a caller with no browser session cannot reach this route as\n * designed.\n *\n * **Path-based, not tag-based.** `revalidatePath` is the only purge mechanism.\n * `cacheComponents: true` — required for `cacheTag`/`revalidateTag` — is\n * incompatible with the `dynamic`/`runtime` segment configs the auth and admin\n * routes require (measured: 5 files fail to build with it enabled), so\n * `revalidateTag` is not an option in this codebase. Do not \"upgrade\" this to\n * tag-based revalidation without first removing that segment-config dependency\n * everywhere.\n *\n * **Never fails the caller past authorization.** A missing session returns 401,\n * a session without write permission 403, and an oversized `changes` batch 413\n * — all three reject before any map work runs. Past that point the handler\n * never throws or returns 5xx: an unconfigured map, a throwing map, or an\n * individual `revalidatePath` failure all resolve to 200 with the failures\n * reported in the body. A failed purge is a stale page; failing the endpoint\n * would only teach the fire-and-forget caller to retry pointlessly.\n *\n * @param props - Config and the host app's auth accessors.\n * @returns `{ POST }` — mount directly as the route module's named export.\n *\n * @example\n * ```ts\n * // app/api/vex/revalidate/route.ts\n * import { api } from \"@convex/_generated/api\";\n * import { createVexRevalidateRoute } from \"@vexcms/next/cache\";\n * import { fetchAuthQuery, getToken } from \"~/auth/server\";\n * import config from \"~/vex.config\";\n *\n * export const { POST } = createVexRevalidateRoute({\n * config,\n * getToken,\n * getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),\n * });\n * ```\n */\nexport function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {\n POST: (request: NextRequest) => Promise<NextResponse<{ error: string } | VexRevalidateResponse>>;\n} {\n return {\n async POST(\n request: NextRequest,\n ): Promise<NextResponse<{ error: string } | VexRevalidateResponse>> {\n const token = await props.getToken();\n if (token === null || token === undefined || token === \"\") {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n let body: VexRevalidateRequest;\n try {\n body = (await request.json()) as VexRevalidateRequest;\n } catch {\n return NextResponse.json({ error: \"Bad Request\" }, { status: 400 });\n }\n\n const { organization, user } = await props.getAuth();\n\n // One mapping, used for BOTH the permission check and target resolution.\n const operation: VexMutationOperation = \"all\" in body ? \"update\" : body.operation;\n const action = toCrudAction(operation);\n\n const allowed = hasPermission({\n access: props.config.access,\n action,\n organization,\n resource: body.collection,\n user,\n // A purge writes no document fields — this asks whether the caller\n // may act on the collection at all, not against a specific payload.\n scope: PERMISSION_SCOPES.any,\n });\n if (!allowed) {\n return NextResponse.json({ error: \"Forbidden\" }, { status: 403 });\n }\n\n const errors: unknown[] = [];\n let changes: VexRevalidateChange[];\n\n if (\"all\" in body) {\n // Server-derived, so no client-supplied batch to bound. Each document\n // is mapped as its current state.\n if (props.listCollection === undefined) {\n errors.push(\n new Error(\n \"createVexRevalidateRoute: a collection-wide purge was requested but `listCollection` is not wired\",\n ),\n );\n changes = [];\n } else {\n try {\n const docs = await props.listCollection(body.collection);\n changes = docs.map((doc) => ({ after: doc }));\n } catch (error) {\n // An unreachable deployment must not 500 a purge request.\n errors.push(error);\n changes = [];\n }\n }\n } else {\n changes = body.changes;\n if (changes.length > VEX_REVALIDATE_BATCH_SIZE) {\n return NextResponse.json(\n {\n error: `Cannot revalidate more than ${VEX_REVALIDATE_BATCH_SIZE} changes in a single request`,\n },\n { status: 413 },\n );\n }\n }\n\n const routesConfig = props.config.routes;\n if (routesConfig === undefined) {\n // A project with no route map still gets a working, harmless\n // endpoint rather than a 500. Any error already accumulated (e.g. an\n // unwired collection-wide purge) is still reported.\n return NextResponse.json({ errors, revalidated: [] }, { status: 200 });\n }\n\n const pool: string[] = [];\n\n for (const change of changes) {\n // A throwing map is contained inside `resolveTargets` — its failure\n // lands in that call's `errors` and never propagates here.\n const target = resolveTargets({\n after: change.after,\n before: change.before,\n collection: body.collection,\n map: routesConfig.map,\n operation: action,\n });\n pool.push(...target.paths);\n errors.push(...target.errors);\n }\n\n // Dedupe across the whole batch, preserving first-seen order — the same\n // contract `resolveTargets` applies to one document's before/after.\n const revalidated: string[] = [];\n for (const path of new Set(pool)) {\n try {\n revalidatePath(path);\n revalidated.push(path);\n } catch (error) {\n // `revalidatePath` is a Next API called here, so it is not covered by\n // `resolveTargets`' containment.\n errors.push(error);\n }\n }\n\n return NextResponse.json({ errors, revalidated }, { status: 200 });\n },\n };\n}\n","import type { FunctionReference, OptionalRestArgs } from \"convex/server\";\n\nimport { ConvexHttpClient } from \"convex/browser\";\nimport { cache } from \"react\";\n\nimport type { VexServerClientOptions, VexServerClient } from \"./types\";\n\n/**\n * Creates a server-only Convex read client safe to use in prerenderable\n * Next.js routes.\n *\n * Wraps `ConvexHttpClient` from `convex/browser` instead of `convex/nextjs`'s\n * `fetchQuery`, which hard-codes `client.setFetchOptions({ cache: \"no-store\" })`\n * — the reason every route calling it builds dynamic (`ƒ`). This client never\n * sets that option, so a route calling only `query()` can be prerendered and\n * governed by the route's own `export const revalidate`.\n *\n * Every `query()` call is deduped via `React.cache` for the lifetime of one\n * request: two identical `(query, args)` calls — e.g. `generateMetadata` and\n * its page both reading the same document — resolve from a single Convex round\n * trip instead of two.\n *\n * `React.cache` is instantiated fresh inside every `createVexServerClient()`\n * call, so that dedupe only holds across calls sharing the SAME client\n * instance — two clients built in two different modules cannot share a round\n * trip even when they read identical `(query, args)`. Each app therefore\n * creates exactly one instance, exported from its own `src/lib/vex.ts`, and\n * every route or component imports that shared `vex` rather than calling\n * `createVexServerClient()` itself.\n *\n * @param props - Client configuration.\n * @returns A {@link VexServerClient} whose `query` method is deduped per request.\n * @throws {Error} When no `url` is given and `process.env.NEXT_PUBLIC_CONVEX_URL`\n * is unset.\n */\nexport function createVexServerClient(props: VexServerClientOptions = {}): VexServerClient {\n const url = props.url ?? process.env.NEXT_PUBLIC_CONVEX_URL;\n if (url === undefined || url === \"\") {\n throw new Error(\n \"createVexServerClient: no Convex deployment URL. Pass `url`, or set NEXT_PUBLIC_CONVEX_URL.\",\n );\n }\n\n const client = new ConvexHttpClient(url, {\n skipConvexDeploymentUrlCheck: props.skipConvexDeploymentUrlCheck,\n });\n\n // Deliberately NOT `client.setFetchOptions({ cache: \"no-store\" })` — that is\n // exactly what `convex/nextjs`'s `fetchQuery` does and what forces every\n // consuming route dynamic.\n\n // `React.cache` keys on the argument list, so the entry is (query reference,\n // args) — never the reference alone. Args are normalised to a single value so\n // `query(fn)` and `query(fn, {})` share one entry.\n const cachedQuery = cache(\n async (query: FunctionReference<\"query\">, args: unknown): Promise<unknown> =>\n client.query(query, args as OptionalRestArgs<FunctionReference<\"query\">>[0]),\n );\n\n return {\n query: async (query, ...args) => cachedQuery(query, args[0] ?? {}),\n };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/NextAdminLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport type { AdminUser } from \"@vexcms/react\";\nimport { NextAdminLayoutClient } from \"./NextAdminLayoutClient\";\nimport { cookies } from \"next/headers\";\n\n/**\n * Next.js admin layout shell for VexCMS.\n *\n * A **server component** that reads the `sidebar_state` cookie server-side\n * (so the sidebar's open/closed state is correct on first paint, with no\n * client flash) and passes it, plus `user`/`organization`/`children`, to the\n * client leaf `NextAdminLayoutClient`. Config no longer crosses this\n * boundary at all — the admin panel's client tree gets its config from the\n * app's own `VexConfigProvider` mount (`clientProviders.tsx`), not from a\n * prop threaded through this layout.\n *\n * Render it from your admin `layout.tsx`. Because this is a server component,\n * your `layout.tsx` may itself be an `async` server component (e.g. to run auth\n * checks).\n *\n * @param props - Layout props.\n * @param props.children - The page content from `[[...slug]]/page.tsx`.\n * @param props.user - Current user for the admin shell.\n * @returns The rendered admin shell, wrapping `children` in the client boundary\n * ({@link NextAdminLayoutClient}) with the server-read sidebar cookie state.\n * @example\n * ```tsx\n * // app/admin/layout.tsx\n * import { NextAdminLayout } from \"@vexcms/next/client\";\n * import { getCurrentUser } from \"~/auth/serverUtils\";\n *\n * export default async function AdminLayout({ children }: { children: ReactNode }) {\n * const user = await getCurrentUser();\n * return (\n * <NextAdminLayout user={user ?? undefined}>\n * {children}\n * </NextAdminLayout>\n * );\n * }\n * ```\n */\nexport async function NextAdminLayout(props: {\n children: ReactNode;\n user?: AdminUser;\n organization?: Record<string, unknown>;\n}) {\n const cookieStore = await cookies();\n const sidebarOpen = String(cookieStore.get(\"sidebar_state\")?.value) === \"true\";\n\n return (\n <NextAdminLayoutClient\n user={props.user}\n organization={props.organization}\n sidebarOpen={sidebarOpen}\n >\n {props.children}\n </NextAdminLayoutClient>\n );\n}\n"],"mappings":";AAEA,SAAS,6BAA6B;AACtC,SAAS,eAAe;AA+CpB;AATJ,eAAsB,gBAAgB,OAInC;AACD,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,cAAc,OAAO,YAAY,IAAI,eAAe,GAAG,KAAK,MAAM;AAExE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,MAAM;AAAA,MACZ,cAAc,MAAM;AAAA,MACpB;AAAA,MAEC,gBAAM;AAAA;AAAA,EACT;AAEJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/NextAdminPage.tsx"],"sourcesContent":["import type { CollectionSlug, PaginationResult, VexConfig, VexMediaDocument } from \"@vexcms/core\";\n\nimport {\n livePreviewLayoutCookieName,\n livePreviewPanelCookieName,\n readLivePreviewLayoutCookie,\n readLivePreviewPanelCookie,\n vexConvexApi,\n} from \"@vexcms/core\";\nimport {\n CollectionEditView,\n CollectionListView,\n DashboardView,\n GlobalEditView,\n GlobalsListView,\n MediaCollectionEditView,\n MediaCollectionListView,\n} from \"@vexcms/react\";\nimport { fetchQuery } from \"convex/nextjs\";\nimport { cookies } from \"next/headers\";\n\n/**\n * VexCMS admin page server component for Next.js.\n *\n * An `async` server component that routes by the `[[...slug]]` catch-all\n * params, prefetches Convex data via `fetchQuery`, and renders the correct\n * view component. Does **not** include a layout wrapper — `NextAdminLayout`\n * in `app/admin/layout.tsx` owns the persistent shell.\n *\n * **Route mapping:**\n * | `path` array | View |\n * |---|---|\n * | `[]` or undefined | `DashboardView` |\n * | `[collectionSlug]` | `CollectionListView` with preloaded docs |\n * | `[collectionSlug, \"new\"]` | `CollectionEditView` (empty form) |\n * | `[collectionSlug, documentId]` | `CollectionEditView` with preloaded doc |\n *\n * @param props - Component props\n * @param props.config - The resolved server `VexConfig`, typically the default export of\n * `vex.config.server.ts` — not the client-safe `vex.config.ts`.\n * @param props.params - Next.js 15 async params `{ path?: string[] }`\n * @param props.token - Optional session token forwarded to `fetchQuery` for authenticated reads.\n * @returns The appropriate admin view for the current URL path.\n *\n * @example\n * ```tsx\n * // app/admin/[[...slug]]/page.tsx\n * import { NextAdminPage } from \"@vexcms/next/server\";\n * import config from \"../../../../vex.config.server\";\n *\n * export default function AdminPage({\n * params,\n * }: {\n * params: Promise<{ path?: string[] }>;\n * }) {\n * return <NextAdminPage config={config} params={params} />;\n * }\n * ```\n */\nexport async function NextAdminPage(props: {\n config: VexConfig;\n params: Promise<{ path?: string[] }>;\n token?: string;\n}) {\n const { path = [] } = await props.params;\n const [collectionSlug, documentId] = path;\n const cookieStore = await cookies();\n\n if (!collectionSlug) {\n return <DashboardView />;\n }\n\n if (collectionSlug === \"globals\") {\n if (!documentId) {\n return <GlobalsListView />;\n }\n const globalConfig = props.config.globals.find((g) => g.slug === documentId);\n if (!globalConfig) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">Global &quot;{documentId}&quot; not found.</p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n const global = await fetchQuery(\n vexConvexApi.globals.get,\n { slug: globalConfig.slug },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <GlobalEditView\n global={globalConfig.slug}\n initialData={global}\n initialPreviewPanelOpen={readLivePreviewPanelCookie({\n cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: globalConfig.slug }))\n ?.value,\n defaultOpen: globalConfig.admin.livePreview?.defaultOpen ?? false,\n })}\n initialPreviewPanelSize={readLivePreviewLayoutCookie({\n cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: globalConfig.slug }))\n ?.value,\n })}\n />\n );\n }\n\n const collection = props.config.collections.find((c) => c.slug === collectionSlug);\n const mediaCollection = props.config.mediaCollections.find((mc) => mc.slug === collectionSlug);\n\n if (!collection && !mediaCollection) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">\n Collection &quot;{collectionSlug}&quot; not found.\n </p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n\n if (mediaCollection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n { id: documentId, collection: mediaCollection.slug },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionEditView\n collection={mediaCollection.slug}\n documentId={documentId}\n initialData={initialData as null | VexMediaDocument}\n />\n );\n }\n\n if (mediaCollection) {\n const numItems = Math.max(\n mediaCollection.admin.table.serverPageSize,\n mediaCollection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.find,\n {\n collection: collectionSlug as CollectionSlug,\n paginationOpts: { numItems, totalDocs: true, cursor: null },\n },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionListView\n collection={mediaCollection.slug}\n initialData={initialData as PaginationResult<VexMediaDocument>}\n />\n );\n }\n\n if (collection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n { id: documentId, collection: collection.slug },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <CollectionEditView\n collection={collection.slug}\n documentId={documentId}\n initialData={initialData}\n initialPreviewPanelOpen={readLivePreviewPanelCookie({\n cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: collection.slug }))\n ?.value,\n defaultOpen: collection.admin.livePreview?.defaultOpen ?? false,\n })}\n initialPreviewPanelSize={readLivePreviewLayoutCookie({\n cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: collection.slug }))\n ?.value,\n })}\n />\n );\n }\n\n if (!collection) {\n throw new Error(\"invalid collection slug\");\n }\n\n const numItems = Math.max(\n collection.admin.table.serverPageSize,\n collection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.findPaginated,\n {\n collection: collectionSlug as CollectionSlug,\n depth: 1,\n paginationOpts: { cursor: null, numItems, totalDocs: true },\n },\n props.token ? { token: props.token } : undefined,\n );\n return <CollectionListView collection={collection.slug} initialData={initialData} />;\n}\n"],"mappings":"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","names":["initialData","numItems"]}