@vexcms/next 0.1.0-alpha.2 → 0.1.0-alpha.21
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 +59 -48
- package/dist/NextAdminLayout.d.ts +14 -23
- package/dist/NextAdminLayout.d.ts.map +1 -1
- package/dist/NextAdminLayout.js +3 -20
- package/dist/NextAdminLayout.js.map +1 -1
- package/dist/NextAdminLayoutClient.d.ts +10 -11
- package/dist/NextAdminLayoutClient.d.ts.map +1 -1
- package/dist/NextAdminLayoutClient.js +6 -7
- package/dist/NextAdminLayoutClient.js.map +1 -1
- package/dist/NextAdminPage.d.ts +7 -5
- package/dist/NextAdminPage.d.ts.map +1 -1
- package/dist/NextAdminPage.js +2 -137
- package/dist/NextAdminPage.js.map +1 -1
- package/dist/cache/createVexRevalidateRoute.d.ts +88 -0
- package/dist/cache/createVexRevalidateRoute.d.ts.map +1 -0
- package/dist/cache/createVexServerClient.d.ts +31 -0
- package/dist/cache/createVexServerClient.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +9 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/types.d.ts +36 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/chunk-3J7LCKFC.js +129 -0
- package/dist/chunk-3J7LCKFC.js.map +1 -0
- package/dist/chunk-K4AAS5B5.js +22 -0
- package/dist/chunk-K4AAS5B5.js.map +1 -0
- package/dist/chunk-KIRSFMPI.js +78 -0
- package/dist/chunk-KIRSFMPI.js.map +1 -0
- package/dist/chunk-L6N3D6L5.js +155 -0
- package/dist/chunk-L6N3D6L5.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/seo/createVexRobots.d.ts +22 -0
- package/dist/seo/createVexRobots.d.ts.map +1 -0
- package/dist/seo/createVexSitemap.d.ts +41 -0
- package/dist/seo/createVexSitemap.d.ts.map +1 -0
- package/dist/seo/index.d.ts +5 -0
- package/dist/seo/index.d.ts.map +1 -0
- package/dist/seo/index.js +13 -0
- package/dist/seo/index.js.map +1 -0
- package/dist/seo/vexMetadata.d.ts +30 -0
- package/dist/seo/vexMetadata.d.ts.map +1 -0
- package/dist/seo/vexStaticParams.d.ts +25 -0
- package/dist/seo/vexStaticParams.d.ts.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +25 -5
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { CollectionSlug, VexConfig, VexDocument, VexRevalidateResponse } from "@vexcms/core";
|
|
2
|
+
import type { NextRequest } from "next/server";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
/** Props for {@link createVexRevalidateRoute}. */
|
|
5
|
+
export interface CreateVexRevalidateRouteProps {
|
|
6
|
+
/**
|
|
7
|
+
* The resolved VexCMS config. Supplies `access` for the write-permission
|
|
8
|
+
* check and `routes.map` for target resolution.
|
|
9
|
+
*/
|
|
10
|
+
config: VexConfig;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the authenticated user and active organization for the current
|
|
13
|
+
* request. Wire to `() => fetchAuthQuery(api.auth.api.getUserOrg, {})` — the
|
|
14
|
+
* same call the admin panel route already makes.
|
|
15
|
+
*/
|
|
16
|
+
getAuth: () => Promise<{
|
|
17
|
+
organization?: Record<string, unknown>;
|
|
18
|
+
user: null | Record<string, unknown>;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Reads the caller's session token for the current request. Wire to the host
|
|
22
|
+
* app's `getToken` export (`convexBetterAuthNextJs`'s output, re-exported
|
|
23
|
+
* from e.g. `~/auth/server`). A falsy return means "no session".
|
|
24
|
+
*/
|
|
25
|
+
getToken: () => Promise<null | string | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* Lists every document in a collection, for the collection-wide purge
|
|
28
|
+
* (`{ collection, all: true }`). Wire to a read through the app's own
|
|
29
|
+
* `publishedSlugs`-style query — the factory cannot know an app's generated
|
|
30
|
+
* Convex `api`, which is why this is injected like `getToken`/`getAuth`.
|
|
31
|
+
*
|
|
32
|
+
* Omit to leave the collection-wide branch unwired: it then purges nothing
|
|
33
|
+
* and reports that in `errors`, rather than silently succeeding.
|
|
34
|
+
*/
|
|
35
|
+
listCollection?: (collection: CollectionSlug) => Promise<Partial<VexDocument>[]>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a Next.js route handler that purges the cached paths affected by a
|
|
39
|
+
* document write. The admin panel calls this via `useVexMutation` after every
|
|
40
|
+
* successful create/update/delete, same-origin, as a relative fetch — no URL
|
|
41
|
+
* config, no shared secret.
|
|
42
|
+
*
|
|
43
|
+
* **Session-authorized, not secret-authorized.** The caller is a signed-in
|
|
44
|
+
* admin, so this reuses `getToken`/`getAuth` exactly like the admin panel route
|
|
45
|
+
* does, rather than introducing an env var or a shared secret. There is no
|
|
46
|
+
* API-key fallback: a caller with no browser session cannot reach this route as
|
|
47
|
+
* designed.
|
|
48
|
+
*
|
|
49
|
+
* **Path-based, not tag-based.** `revalidatePath` is the only purge mechanism.
|
|
50
|
+
* `cacheComponents: true` — required for `cacheTag`/`revalidateTag` — is
|
|
51
|
+
* incompatible with the `dynamic`/`runtime` segment configs the auth and admin
|
|
52
|
+
* routes require (measured: 5 files fail to build with it enabled), so
|
|
53
|
+
* `revalidateTag` is not an option in this codebase. Do not "upgrade" this to
|
|
54
|
+
* tag-based revalidation without first removing that segment-config dependency
|
|
55
|
+
* everywhere.
|
|
56
|
+
*
|
|
57
|
+
* **Never fails the caller past authorization.** A missing session returns 401,
|
|
58
|
+
* a session without write permission 403, and an oversized `changes` batch 413
|
|
59
|
+
* — all three reject before any map work runs. Past that point the handler
|
|
60
|
+
* never throws or returns 5xx: an unconfigured map, a throwing map, or an
|
|
61
|
+
* individual `revalidatePath` failure all resolve to 200 with the failures
|
|
62
|
+
* reported in the body. A failed purge is a stale page; failing the endpoint
|
|
63
|
+
* would only teach the fire-and-forget caller to retry pointlessly.
|
|
64
|
+
*
|
|
65
|
+
* @param props - Config and the host app's auth accessors.
|
|
66
|
+
* @returns `{ POST }` — mount directly as the route module's named export.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* // app/api/vex/revalidate/route.ts
|
|
71
|
+
* import { api } from "@convex/_generated/api";
|
|
72
|
+
* import { createVexRevalidateRoute } from "@vexcms/next/cache";
|
|
73
|
+
* import { fetchAuthQuery, getToken } from "~/auth/server";
|
|
74
|
+
* import config from "~/vex.config";
|
|
75
|
+
*
|
|
76
|
+
* export const { POST } = createVexRevalidateRoute({
|
|
77
|
+
* config,
|
|
78
|
+
* getToken,
|
|
79
|
+
* getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {
|
|
84
|
+
POST: (request: NextRequest) => Promise<NextResponse<{
|
|
85
|
+
error: string;
|
|
86
|
+
} | VexRevalidateResponse>>;
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=createVexRevalidateRoute.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { VexServerClientOptions, VexServerClient } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a server-only Convex read client safe to use in prerenderable
|
|
4
|
+
* Next.js routes.
|
|
5
|
+
*
|
|
6
|
+
* Wraps `ConvexHttpClient` from `convex/browser` instead of `convex/nextjs`'s
|
|
7
|
+
* `fetchQuery`, which hard-codes `client.setFetchOptions({ cache: "no-store" })`
|
|
8
|
+
* — the reason every route calling it builds dynamic (`ƒ`). This client never
|
|
9
|
+
* sets that option, so a route calling only `query()` can be prerendered and
|
|
10
|
+
* governed by the route's own `export const revalidate`.
|
|
11
|
+
*
|
|
12
|
+
* Every `query()` call is deduped via `React.cache` for the lifetime of one
|
|
13
|
+
* request: two identical `(query, args)` calls — e.g. `generateMetadata` and
|
|
14
|
+
* its page both reading the same document — resolve from a single Convex round
|
|
15
|
+
* trip instead of two.
|
|
16
|
+
*
|
|
17
|
+
* `React.cache` is instantiated fresh inside every `createVexServerClient()`
|
|
18
|
+
* call, so that dedupe only holds across calls sharing the SAME client
|
|
19
|
+
* instance — two clients built in two different modules cannot share a round
|
|
20
|
+
* trip even when they read identical `(query, args)`. Each app therefore
|
|
21
|
+
* creates exactly one instance, exported from its own `src/lib/vex.ts`, and
|
|
22
|
+
* every route or component imports that shared `vex` rather than calling
|
|
23
|
+
* `createVexServerClient()` itself.
|
|
24
|
+
*
|
|
25
|
+
* @param props - Client configuration.
|
|
26
|
+
* @returns A {@link VexServerClient} whose `query` method is deduped per request.
|
|
27
|
+
* @throws {Error} When no `url` is given and `process.env.NEXT_PUBLIC_CONVEX_URL`
|
|
28
|
+
* is unset.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createVexServerClient(props?: VexServerClientOptions): VexServerClient;
|
|
31
|
+
//# sourceMappingURL=createVexServerClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createVexServerClient.d.ts","sourceRoot":"","sources":["../../src/cache/createVexServerClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,GAAE,sBAA2B,GAAG,eAAe,CA2BzF"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createVexRevalidateRoute } from "./createVexRevalidateRoute";
|
|
2
|
+
export type { CreateVexRevalidateRouteProps } from "./createVexRevalidateRoute";
|
|
3
|
+
export { createVexServerClient } from "./createVexServerClient";
|
|
4
|
+
export type { VexServerClientOptions, VexServerClient, } from "./types";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAOhE,YAAY,EACV,sBAAsB,EACtB,eAAe,GAChB,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FunctionReference, FunctionReturnType, OptionalRestArgs } from "convex/server";
|
|
2
|
+
/**
|
|
3
|
+
* Construction options for {@link createVexServerClient}.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here configures caching — the client's only cache behaviour is that
|
|
6
|
+
* it never sets `cache: "no-store"` and dedupes per request via `React.cache`.
|
|
7
|
+
* Cache *policy* lives where Next requires it: `export const revalidate` in
|
|
8
|
+
* each route.
|
|
9
|
+
*/
|
|
10
|
+
export interface VexServerClientOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Skip Convex's deployment URL format check — for self-hosted backends whose
|
|
13
|
+
* URL does not match `https://<name>.convex.cloud`.
|
|
14
|
+
*/
|
|
15
|
+
skipConvexDeploymentUrlCheck?: boolean;
|
|
16
|
+
/** Convex deployment URL. Defaults to `process.env.NEXT_PUBLIC_CONVEX_URL`. */
|
|
17
|
+
url?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Server-only Convex read client returned by {@link createVexServerClient}.
|
|
21
|
+
*
|
|
22
|
+
* Exposes only `query` — this client is for prerenderable read paths, and it
|
|
23
|
+
* never sets `cache: "no-store"`, unlike `convex/nextjs`'s `fetchQuery`.
|
|
24
|
+
*/
|
|
25
|
+
export interface VexServerClient {
|
|
26
|
+
/**
|
|
27
|
+
* Runs a Convex query, deduped via `React.cache` for the lifetime of one
|
|
28
|
+
* request when called again with the same function reference and args.
|
|
29
|
+
*
|
|
30
|
+
* @param query - The Convex query function reference to call.
|
|
31
|
+
* @param args - Arguments for `query`, per Convex's `OptionalRestArgs`.
|
|
32
|
+
* @returns The query's resolved return value.
|
|
33
|
+
*/
|
|
34
|
+
query<Query extends FunctionReference<"query">>(query: Query, ...args: OptionalRestArgs<Query>): Promise<FunctionReturnType<Query>>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cache/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE7F;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,KAAK,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC5C,KAAK,EAAE,KAAK,EACZ,GAAG,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAC/B,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;CACvC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// src/cache/createVexRevalidateRoute.ts
|
|
2
|
+
import {
|
|
3
|
+
hasPermission,
|
|
4
|
+
PERMISSION_SCOPES,
|
|
5
|
+
resolveTargets,
|
|
6
|
+
VEX_REVALIDATE_BATCH_SIZE
|
|
7
|
+
} from "@vexcms/core";
|
|
8
|
+
import { revalidatePath } from "next/cache";
|
|
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
|
+
function createVexRevalidateRoute(props) {
|
|
16
|
+
return {
|
|
17
|
+
async POST(request) {
|
|
18
|
+
const token = await props.getToken();
|
|
19
|
+
if (token === null || token === void 0 || token === "") {
|
|
20
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
21
|
+
}
|
|
22
|
+
let body;
|
|
23
|
+
try {
|
|
24
|
+
body = await request.json();
|
|
25
|
+
} catch {
|
|
26
|
+
return NextResponse.json({ error: "Bad Request" }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
const { organization, user } = await props.getAuth();
|
|
29
|
+
const operation = "all" in body ? "update" : body.operation;
|
|
30
|
+
const action = toCrudAction(operation);
|
|
31
|
+
const allowed = hasPermission({
|
|
32
|
+
access: props.config.access,
|
|
33
|
+
action,
|
|
34
|
+
organization,
|
|
35
|
+
resource: body.collection,
|
|
36
|
+
user,
|
|
37
|
+
// A purge writes no document fields — this asks whether the caller
|
|
38
|
+
// may act on the collection at all, not against a specific payload.
|
|
39
|
+
scope: PERMISSION_SCOPES.any
|
|
40
|
+
});
|
|
41
|
+
if (!allowed) {
|
|
42
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
43
|
+
}
|
|
44
|
+
const errors = [];
|
|
45
|
+
let changes;
|
|
46
|
+
if ("all" in body) {
|
|
47
|
+
if (props.listCollection === void 0) {
|
|
48
|
+
errors.push(
|
|
49
|
+
new Error(
|
|
50
|
+
"createVexRevalidateRoute: a collection-wide purge was requested but `listCollection` is not wired"
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
changes = [];
|
|
54
|
+
} else {
|
|
55
|
+
try {
|
|
56
|
+
const docs = await props.listCollection(body.collection);
|
|
57
|
+
changes = docs.map((doc) => ({ after: doc }));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
errors.push(error);
|
|
60
|
+
changes = [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
changes = body.changes;
|
|
65
|
+
if (changes.length > VEX_REVALIDATE_BATCH_SIZE) {
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{
|
|
68
|
+
error: `Cannot revalidate more than ${VEX_REVALIDATE_BATCH_SIZE} changes in a single request`
|
|
69
|
+
},
|
|
70
|
+
{ status: 413 }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const routesConfig = props.config.routes;
|
|
75
|
+
if (routesConfig === void 0) {
|
|
76
|
+
return NextResponse.json({ errors, revalidated: [] }, { status: 200 });
|
|
77
|
+
}
|
|
78
|
+
const pool = [];
|
|
79
|
+
for (const change of changes) {
|
|
80
|
+
const target = resolveTargets({
|
|
81
|
+
after: change.after,
|
|
82
|
+
before: change.before,
|
|
83
|
+
collection: body.collection,
|
|
84
|
+
map: routesConfig.map,
|
|
85
|
+
operation: action
|
|
86
|
+
});
|
|
87
|
+
pool.push(...target.paths);
|
|
88
|
+
errors.push(...target.errors);
|
|
89
|
+
}
|
|
90
|
+
const revalidated = [];
|
|
91
|
+
for (const path of new Set(pool)) {
|
|
92
|
+
try {
|
|
93
|
+
revalidatePath(path);
|
|
94
|
+
revalidated.push(path);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
errors.push(error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return NextResponse.json({ errors, revalidated }, { status: 200 });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/cache/createVexServerClient.ts
|
|
105
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
106
|
+
import { cache } from "react";
|
|
107
|
+
function createVexServerClient(props = {}) {
|
|
108
|
+
const url = props.url ?? process.env.NEXT_PUBLIC_CONVEX_URL;
|
|
109
|
+
if (url === void 0 || url === "") {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"createVexServerClient: no Convex deployment URL. Pass `url`, or set NEXT_PUBLIC_CONVEX_URL."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const client = new ConvexHttpClient(url, {
|
|
115
|
+
skipConvexDeploymentUrlCheck: props.skipConvexDeploymentUrlCheck
|
|
116
|
+
});
|
|
117
|
+
const cachedQuery = cache(
|
|
118
|
+
async (query, args) => client.query(query, args)
|
|
119
|
+
);
|
|
120
|
+
return {
|
|
121
|
+
query: async (query, ...args) => cachedQuery(query, args[0] ?? {})
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
createVexRevalidateRoute,
|
|
127
|
+
createVexServerClient
|
|
128
|
+
};
|
|
129
|
+
//# sourceMappingURL=chunk-3J7LCKFC.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 {\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":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/NextAdminLayout.tsx
|
|
2
|
+
import { NextAdminLayoutClient } from "./NextAdminLayoutClient";
|
|
3
|
+
import { cookies } from "next/headers";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
async function NextAdminLayout(props) {
|
|
6
|
+
const cookieStore = await cookies();
|
|
7
|
+
const sidebarOpen = String(cookieStore.get("sidebar_state")?.value) === "true";
|
|
8
|
+
return /* @__PURE__ */ jsx(
|
|
9
|
+
NextAdminLayoutClient,
|
|
10
|
+
{
|
|
11
|
+
user: props.user,
|
|
12
|
+
organization: props.organization,
|
|
13
|
+
sidebarOpen,
|
|
14
|
+
children: props.children
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
NextAdminLayout
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=chunk-K4AAS5B5.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 `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":[]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/seo/createVexRobots.ts
|
|
2
|
+
function createVexRobots(props) {
|
|
3
|
+
return function robots() {
|
|
4
|
+
const disallow = props.disallow ?? [];
|
|
5
|
+
return {
|
|
6
|
+
rules: {
|
|
7
|
+
allow: "/",
|
|
8
|
+
userAgent: "*",
|
|
9
|
+
...disallow.length === 0 ? {} : { disallow }
|
|
10
|
+
},
|
|
11
|
+
sitemap: `${props.siteUrl}/sitemap.xml`
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/seo/createVexSitemap.ts
|
|
17
|
+
function createVexSitemap(props) {
|
|
18
|
+
return async function sitemap() {
|
|
19
|
+
try {
|
|
20
|
+
const items = await props.client.query(props.query, props.args ?? {});
|
|
21
|
+
if (!Array.isArray(items)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const { getUpdatedAt } = props;
|
|
25
|
+
return items.map((item) => {
|
|
26
|
+
const updatedAt = getUpdatedAt?.(item);
|
|
27
|
+
return {
|
|
28
|
+
url: props.toUrl(props.getSlug(item)),
|
|
29
|
+
...typeof updatedAt === "number" && Number.isFinite(updatedAt) ? { lastModified: new Date(updatedAt) } : {}
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/seo/vexMetadata.ts
|
|
39
|
+
function vexMetadata(props) {
|
|
40
|
+
return {
|
|
41
|
+
alternates: { canonical: props.path },
|
|
42
|
+
description: props.description,
|
|
43
|
+
metadataBase: new URL(props.siteUrl),
|
|
44
|
+
// `openGraph` is ALWAYS emitted. Gating the whole block on an image is the
|
|
45
|
+
// defect Step 1 fixed in `apps/www/src/lib/metadata.ts`: a page with no
|
|
46
|
+
// uploaded image published no `og:title` and no `og:description` at all.
|
|
47
|
+
// Only `images` is conditional.
|
|
48
|
+
openGraph: {
|
|
49
|
+
description: props.description,
|
|
50
|
+
title: props.title,
|
|
51
|
+
...props.imageUrl === void 0 || props.imageUrl === "" ? {} : { images: [{ url: props.imageUrl }] }
|
|
52
|
+
},
|
|
53
|
+
title: props.title
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/seo/vexStaticParams.ts
|
|
58
|
+
async function vexStaticParams(props) {
|
|
59
|
+
try {
|
|
60
|
+
const items = await props.client.query(props.query, props.args ?? {});
|
|
61
|
+
if (!Array.isArray(items)) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
return items.map((item) => ({
|
|
65
|
+
[props.paramName]: props.getSlug(item)
|
|
66
|
+
}));
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
createVexRobots,
|
|
74
|
+
createVexSitemap,
|
|
75
|
+
vexMetadata,
|
|
76
|
+
vexStaticParams
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=chunk-KIRSFMPI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/seo/createVexRobots.ts","../src/seo/createVexSitemap.ts","../src/seo/vexMetadata.ts","../src/seo/vexStaticParams.ts"],"sourcesContent":["import type { MetadataRoute } from \"next\";\n\n/**\n * Creates a Next.js `robots.ts` default export that allows crawlers across the\n * site and points them at the generated sitemap.\n *\n * @param props - Robots configuration.\n * @returns A `robots()` function suitable as `app/robots.ts`'s default export.\n */\nexport function createVexRobots(props: {\n /**\n * Paths to disallow. Every vexcms template passes `[\"/admin\", \"/api\"]` — the\n * admin panel is authenticated and neither belongs in an index. Omitted\n * entirely when absent or empty, rather than emitted as an empty array.\n */\n disallow?: string[];\n /**\n * The site's absolute origin, no trailing slash (e.g. `https://example.com`).\n * The sitemap URL is derived as `${siteUrl}/sitemap.xml`.\n */\n siteUrl: string;\n}): () => MetadataRoute.Robots {\n return function robots(): MetadataRoute.Robots {\n const disallow = props.disallow ?? [];\n return {\n rules: {\n allow: \"/\",\n userAgent: \"*\",\n ...(disallow.length === 0 ? {} : { disallow }),\n },\n sitemap: `${props.siteUrl}/sitemap.xml`,\n };\n };\n}\n","import type { FunctionReference, FunctionReturnType, OptionalRestArgs } from \"convex/server\";\nimport type { MetadataRoute } from \"next\";\n\nimport type { VexServerClient } from \"../cache/types\";\n\ntype ArrayItem<T> = T extends (infer Item)[] ? Item : never;\n\n/**\n * Creates a Next.js `sitemap.ts` default export from a Convex published-slugs\n * query (`packages/core/src/api/publishedSlugs/server.ts`).\n *\n * Tolerates an unreachable Convex deployment (P-020): a read failure returns\n * `[]` — an empty sitemap — rather than failing the build.\n *\n * @param props - Sitemap configuration.\n * @returns A `sitemap()` function suitable as `app/sitemap.ts`'s default export.\n */\nexport function createVexSitemap<Query extends FunctionReference<\"query\">>(props: {\n args?: OptionalRestArgs<Query>[0];\n client: VexServerClient;\n getSlug: (item: ArrayItem<FunctionReturnType<Query>>) => string;\n /**\n * Optional last-modified extractor. Omit it when the collection carries no\n * modification timestamp — Convex's `_creationTime` is set at insert and\n * never moves, so reporting it as `lastModified` would be a lie.\n * `lastModified` is optional in the sitemap protocol; an absent value is\n * correct, a wrong one costs crawl budget.\n *\n * May return `undefined` for an individual item, and that is the common\n * case: `defineCollection` injects `updatedAt` on every content collection,\n * but only a write through `create`/`update` populates it — a seeded or\n * imported row has none. Those entries are emitted with no `lastModified`\n * rather than an `Invalid Date`.\n */\n getUpdatedAt?: (item: ArrayItem<FunctionReturnType<Query>>) => number | undefined;\n /**\n * Maps one item's slug to its absolute public URL — the route mapper, so URL\n * shape (e.g. `/blog/[slug]` vs `/[slug]`) is the caller's choice.\n */\n toUrl: (slug: string) => string;\n query: Query;\n}): () => Promise<MetadataRoute.Sitemap> {\n return async function sitemap(): Promise<MetadataRoute.Sitemap> {\n try {\n const items = await props.client.query(props.query, props.args ?? {});\n if (!Array.isArray(items)) {\n return [];\n }\n const { getUpdatedAt } = props;\n // `Array.isArray` widens a generic return to `any[]`, so the element\n // type is restated here rather than asserted on `items`.\n return items.map((item: ArrayItem<FunctionReturnType<Query>>) => {\n // Resolved per item, not per call: a collection can carry `updatedAt`\n // while an individual seeded or imported row has never been written\n // through the API. `new Date(undefined)` is an `Invalid Date`, which\n // serializes to a `<lastmod>` no crawler can parse, so the tag is\n // dropped for that entry instead.\n const updatedAt = getUpdatedAt?.(item);\n return {\n url: props.toUrl(props.getSlug(item)),\n ...(typeof updatedAt === \"number\" && Number.isFinite(updatedAt)\n ? { lastModified: new Date(updatedAt) }\n : {}),\n };\n });\n } catch {\n // Unreachable deployment, or any other read failure.\n return [];\n }\n };\n}\n","import type { Metadata } from \"next\";\n\n/**\n * Builds a Next.js `Metadata` object with unconditional title/description, a\n * conditional OG image, `metadataBase`, and a canonical link.\n *\n * Generalizes what Step 1 fixed by hand in `apps/www/src/lib/metadata.ts`:\n * title and description are set regardless of whether an OG image resolved —\n * only the `openGraph.images` entry is conditional on `props.imageUrl`. The\n * original gated the whole `openGraph` block on the image, so a page without\n * one published no OG title or description at all.\n *\n * This FORMATS metadata; it does not read Convex. Resolving which title and\n * description win is the caller's job, because only the caller knows the\n * page/site precedence rules.\n *\n * @param props - Metadata inputs.\n * @returns A `Metadata` object for a page's `generateMetadata` or static\n * `metadata` export.\n */\nexport function vexMetadata(props: {\n description?: string;\n /** Absolute OG image URL. Omit to emit `openGraph` without an image. */\n imageUrl?: string;\n /** The page's path (e.g. `/about`), used for the canonical link. */\n path: string;\n /** The site's absolute origin, used for `metadataBase`. */\n siteUrl: string;\n title: string;\n}): Metadata {\n return {\n alternates: { canonical: props.path },\n description: props.description,\n metadataBase: new URL(props.siteUrl),\n // `openGraph` is ALWAYS emitted. Gating the whole block on an image is the\n // defect Step 1 fixed in `apps/www/src/lib/metadata.ts`: a page with no\n // uploaded image published no `og:title` and no `og:description` at all.\n // Only `images` is conditional.\n openGraph: {\n description: props.description,\n title: props.title,\n ...(props.imageUrl === undefined || props.imageUrl === \"\"\n ? {}\n : { images: [{ url: props.imageUrl }] }),\n },\n title: props.title,\n };\n}\n","import type { FunctionReference, FunctionReturnType, OptionalRestArgs } from \"convex/server\";\n\nimport type { VexServerClient } from \"../cache/types\";\n\ntype ArrayItem<T> = T extends (infer Item)[] ? Item : never;\n\n/**\n * Builds Next.js `generateStaticParams` output for a slug-driven route from a\n * Convex query.\n *\n * Returns `[]` when the read fails — including an unreachable Convex\n * deployment, which every CI build hits because it supplies placeholder env\n * rather than skipping validation (P-020). A route with no static params still\n * renders correctly on demand; it is simply not prerendered until the next\n * build.\n *\n * @param props - Static params configuration.\n * @returns `{ [paramName]: string }[]`, or `[]` if the read fails.\n */\nexport async function vexStaticParams<Query extends FunctionReference<\"query\">>(props: {\n args?: OptionalRestArgs<Query>[0];\n client: VexServerClient;\n getSlug: (item: ArrayItem<FunctionReturnType<Query>>) => string;\n paramName: string;\n query: Query;\n}): Promise<Record<string, string>[]> {\n try {\n const items = await props.client.query(props.query, props.args ?? {});\n if (!Array.isArray(items)) {\n return [];\n }\n // `Array.isArray` widens a generic return to `any[]`, so the element type\n // is restated here rather than asserted on `items`.\n return items.map((item: ArrayItem<FunctionReturnType<Query>>) => ({\n [props.paramName]: props.getSlug(item),\n }));\n } catch {\n // Unreachable deployment, or any other read failure. An empty list means\n // \"prerender nothing\", which is correct — the route still renders on\n // demand — and never fails the build.\n return [];\n }\n}\n"],"mappings":";AASO,SAAS,gBAAgB,OAYD;AAC7B,SAAO,SAAS,SAA+B;AAC7C,UAAM,WAAW,MAAM,YAAY,CAAC;AACpC,WAAO;AAAA,MACL,OAAO;AAAA,QACL,OAAO;AAAA,QACP,WAAW;AAAA,QACX,GAAI,SAAS,WAAW,IAAI,CAAC,IAAI,EAAE,SAAS;AAAA,MAC9C;AAAA,MACA,SAAS,GAAG,MAAM,OAAO;AAAA,IAC3B;AAAA,EACF;AACF;;;AChBO,SAAS,iBAA2D,OAwBlC;AACvC,SAAO,eAAe,UAA0C;AAC9D,QAAI;AACF,YAAM,QAAQ,MAAM,MAAM,OAAO,MAAM,MAAM,OAAO,MAAM,QAAQ,CAAC,CAAC;AACpE,UAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,eAAO,CAAC;AAAA,MACV;AACA,YAAM,EAAE,aAAa,IAAI;AAGzB,aAAO,MAAM,IAAI,CAAC,SAA+C;AAM/D,cAAM,YAAY,eAAe,IAAI;AACrC,eAAO;AAAA,UACL,KAAK,MAAM,MAAM,MAAM,QAAQ,IAAI,CAAC;AAAA,UACpC,GAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,IAC1D,EAAE,cAAc,IAAI,KAAK,SAAS,EAAE,IACpC,CAAC;AAAA,QACP;AAAA,MACF,CAAC;AAAA,IACH,QAAQ;AAEN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;;;AClDO,SAAS,YAAY,OASf;AACX,SAAO;AAAA,IACL,YAAY,EAAE,WAAW,MAAM,KAAK;AAAA,IACpC,aAAa,MAAM;AAAA,IACnB,cAAc,IAAI,IAAI,MAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKnC,WAAW;AAAA,MACT,aAAa,MAAM;AAAA,MACnB,OAAO,MAAM;AAAA,MACb,GAAI,MAAM,aAAa,UAAa,MAAM,aAAa,KACnD,CAAC,IACD,EAAE,QAAQ,CAAC,EAAE,KAAK,MAAM,SAAS,CAAC,EAAE;AAAA,IAC1C;AAAA,IACA,OAAO,MAAM;AAAA,EACf;AACF;;;AC5BA,eAAsB,gBAA0D,OAM1C;AACpC,MAAI;AACF,UAAM,QAAQ,MAAM,MAAM,OAAO,MAAM,MAAM,OAAO,MAAM,QAAQ,CAAC,CAAC;AACpE,QAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,aAAO,CAAC;AAAA,IACV;AAGA,WAAO,MAAM,IAAI,CAAC,UAAgD;AAAA,MAChE,CAAC,MAAM,SAAS,GAAG,MAAM,QAAQ,IAAI;AAAA,IACvC,EAAE;AAAA,EACJ,QAAQ;AAIN,WAAO,CAAC;AAAA,EACV;AACF;","names":[]}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/NextAdminPage.tsx
|
|
2
|
+
import {
|
|
3
|
+
livePreviewLayoutCookieName,
|
|
4
|
+
livePreviewPanelCookieName,
|
|
5
|
+
readLivePreviewLayoutCookie,
|
|
6
|
+
readLivePreviewPanelCookie,
|
|
7
|
+
vexConvexApi
|
|
8
|
+
} from "@vexcms/core";
|
|
9
|
+
import {
|
|
10
|
+
CollectionEditView,
|
|
11
|
+
CollectionListView,
|
|
12
|
+
DashboardView,
|
|
13
|
+
GlobalEditView,
|
|
14
|
+
GlobalsListView,
|
|
15
|
+
MediaCollectionEditView,
|
|
16
|
+
MediaCollectionListView
|
|
17
|
+
} from "@vexcms/react";
|
|
18
|
+
import { fetchQuery } from "convex/nextjs";
|
|
19
|
+
import { cookies } from "next/headers";
|
|
20
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
21
|
+
async function NextAdminPage(props) {
|
|
22
|
+
const { path = [] } = await props.params;
|
|
23
|
+
const [collectionSlug, documentId] = path;
|
|
24
|
+
const cookieStore = await cookies();
|
|
25
|
+
if (!collectionSlug) {
|
|
26
|
+
return /* @__PURE__ */ jsx(DashboardView, {});
|
|
27
|
+
}
|
|
28
|
+
if (collectionSlug === "globals") {
|
|
29
|
+
if (!documentId) {
|
|
30
|
+
return /* @__PURE__ */ jsx(GlobalsListView, {});
|
|
31
|
+
}
|
|
32
|
+
const globalConfig = props.config.globals.find((g) => g.slug === documentId);
|
|
33
|
+
if (!globalConfig) {
|
|
34
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
35
|
+
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground p-6", children: [
|
|
36
|
+
'Global "',
|
|
37
|
+
documentId,
|
|
38
|
+
'" not found.'
|
|
39
|
+
] }),
|
|
40
|
+
/* @__PURE__ */ jsx("p", { children: "TODO: add not found view" })
|
|
41
|
+
] });
|
|
42
|
+
}
|
|
43
|
+
const global = await fetchQuery(
|
|
44
|
+
vexConvexApi.globals.get,
|
|
45
|
+
{ slug: globalConfig.slug },
|
|
46
|
+
props.token ? { token: props.token } : void 0
|
|
47
|
+
);
|
|
48
|
+
return /* @__PURE__ */ jsx(
|
|
49
|
+
GlobalEditView,
|
|
50
|
+
{
|
|
51
|
+
global: globalConfig.slug,
|
|
52
|
+
initialData: global,
|
|
53
|
+
initialPreviewPanelOpen: readLivePreviewPanelCookie({
|
|
54
|
+
cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: globalConfig.slug }))?.value,
|
|
55
|
+
defaultOpen: globalConfig.admin.livePreview?.defaultOpen ?? false
|
|
56
|
+
}),
|
|
57
|
+
initialPreviewPanelSize: readLivePreviewLayoutCookie({
|
|
58
|
+
cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: globalConfig.slug }))?.value
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const collection = props.config.collections.find((c) => c.slug === collectionSlug);
|
|
64
|
+
const mediaCollection = props.config.mediaCollections.find((mc) => mc.slug === collectionSlug);
|
|
65
|
+
if (!collection && !mediaCollection) {
|
|
66
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
67
|
+
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground p-6", children: [
|
|
68
|
+
'Collection "',
|
|
69
|
+
collectionSlug,
|
|
70
|
+
'" not found.'
|
|
71
|
+
] }),
|
|
72
|
+
/* @__PURE__ */ jsx("p", { children: "TODO: add not found view" })
|
|
73
|
+
] });
|
|
74
|
+
}
|
|
75
|
+
if (mediaCollection && documentId) {
|
|
76
|
+
const initialData2 = await fetchQuery(
|
|
77
|
+
vexConvexApi.get,
|
|
78
|
+
{ id: documentId, collection: mediaCollection.slug },
|
|
79
|
+
props.token ? { token: props.token } : void 0
|
|
80
|
+
);
|
|
81
|
+
return /* @__PURE__ */ jsx(
|
|
82
|
+
MediaCollectionEditView,
|
|
83
|
+
{
|
|
84
|
+
collection: mediaCollection.slug,
|
|
85
|
+
documentId,
|
|
86
|
+
initialData: initialData2
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (mediaCollection) {
|
|
91
|
+
const numItems2 = Math.max(
|
|
92
|
+
mediaCollection.admin.table.serverPageSize,
|
|
93
|
+
mediaCollection.admin.table.defaultPageSize
|
|
94
|
+
);
|
|
95
|
+
const initialData2 = await fetchQuery(
|
|
96
|
+
vexConvexApi.find,
|
|
97
|
+
{
|
|
98
|
+
collection: collectionSlug,
|
|
99
|
+
paginationOpts: { numItems: numItems2, totalDocs: true, cursor: null }
|
|
100
|
+
},
|
|
101
|
+
props.token ? { token: props.token } : void 0
|
|
102
|
+
);
|
|
103
|
+
return /* @__PURE__ */ jsx(
|
|
104
|
+
MediaCollectionListView,
|
|
105
|
+
{
|
|
106
|
+
collection: mediaCollection.slug,
|
|
107
|
+
initialData: initialData2
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (collection && documentId) {
|
|
112
|
+
const initialData2 = await fetchQuery(
|
|
113
|
+
vexConvexApi.get,
|
|
114
|
+
{ id: documentId, collection: collection.slug },
|
|
115
|
+
props.token ? { token: props.token } : void 0
|
|
116
|
+
);
|
|
117
|
+
return /* @__PURE__ */ jsx(
|
|
118
|
+
CollectionEditView,
|
|
119
|
+
{
|
|
120
|
+
collection: collection.slug,
|
|
121
|
+
documentId,
|
|
122
|
+
initialData: initialData2,
|
|
123
|
+
initialPreviewPanelOpen: readLivePreviewPanelCookie({
|
|
124
|
+
cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: collection.slug }))?.value,
|
|
125
|
+
defaultOpen: collection.admin.livePreview?.defaultOpen ?? false
|
|
126
|
+
}),
|
|
127
|
+
initialPreviewPanelSize: readLivePreviewLayoutCookie({
|
|
128
|
+
cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: collection.slug }))?.value
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (!collection) {
|
|
134
|
+
throw new Error("invalid collection slug");
|
|
135
|
+
}
|
|
136
|
+
const numItems = Math.max(
|
|
137
|
+
collection.admin.table.serverPageSize,
|
|
138
|
+
collection.admin.table.defaultPageSize
|
|
139
|
+
);
|
|
140
|
+
const initialData = await fetchQuery(
|
|
141
|
+
vexConvexApi.findPaginated,
|
|
142
|
+
{
|
|
143
|
+
collection: collectionSlug,
|
|
144
|
+
depth: 1,
|
|
145
|
+
paginationOpts: { cursor: null, numItems, totalDocs: true }
|
|
146
|
+
},
|
|
147
|
+
props.token ? { token: props.token } : void 0
|
|
148
|
+
);
|
|
149
|
+
return /* @__PURE__ */ jsx(CollectionListView, { collection: collection.slug, initialData });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
NextAdminPage
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=chunk-L6N3D6L5.js.map
|