@bettercms-ai/react 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -2
- package/dist/index.d.ts +61 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -4
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bettercms-ai/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
BetterCMS for client-rendered React apps (Vite, CRA, any SPA). For Next.js use `@bettercms-ai/next`, for Astro `@bettercms-ai/astro`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i @bettercms-ai/react
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```env
|
|
10
|
+
VITE_BCMS_API_URL=https://api.bettercms.ai
|
|
11
|
+
VITE_BCMS_WORKSPACE=your-workspace
|
|
12
|
+
VITE_BCMS_PROJECT_ID=your-project-id
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
// main.tsx
|
|
17
|
+
import { BcmsProvider } from "@bettercms-ai/react";
|
|
18
|
+
createRoot(el).render(<BcmsProvider env={import.meta.env}><App /></BcmsProvider>);
|
|
19
|
+
|
|
20
|
+
// a page
|
|
21
|
+
import { BcmsBlocks, usePage, useComponents } from "@bettercms-ai/react";
|
|
22
|
+
export function Home() {
|
|
23
|
+
const { data: page } = usePage("home");
|
|
24
|
+
const { data: components } = useComponents();
|
|
25
|
+
return page ? <BcmsBlocks blocks={page.blocks} components={components} /> : null;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Hooks: `usePage(slug)`, `useEntry(slug)`, `useEntries(opts)`, `useLayout()`, `useComponents()`, `useBcmsQuery(key, read)`; each returns `{ data, error, loading }`.
|
|
30
|
+
|
|
31
|
+
Components (the same markup as `@bettercms-ai/next`): `BcmsBlocks`, `BcmsSections`, `BcmsSection`, `BcmsField`, `BcmsImage`, `BcmsButton`, `BcmsForm`, `BcmsSearch`.
|
|
32
|
+
|
|
33
|
+
## Draft mode
|
|
34
|
+
|
|
35
|
+
No server needed. The editor's "Preview on site" link opens `/api/draft?token=…&redirect=/page` on your site; an SPA host serves your app there, and `<BcmsProvider>` checks the token with BetterCMS, keeps it for the tab and moves to `redirect`. Reads then return drafts. `useBcms().exitDraft()` leaves draft mode. Set `draftPath` on the provider if your app uses another path (and the same path in the project's integration settings).
|
|
36
|
+
|
|
37
|
+
Your host must serve the app at that path, as it does for any client-side route: Netlify `/* /index.html 200` in `_redirects`, a Vercel rewrite of `/api/draft` to `/index.html` (Vercel keeps `/api` for functions otherwise), and in `vite dev` no proxy on `/api`. GitHub Pages cannot rewrite: set `draftPath` to a path your app serves (`/draft`) and the same path in the integration settings. A token the backend refuses, or a check that cannot reach it, leaves the app published; an expired token is dropped the next time the app loads.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { BetterCMSReadClient } from '@bettercms-ai/sdk/read';
|
|
4
|
+
import { BcmsSiteOptions } from '@bettercms-ai/sdk/config';
|
|
5
|
+
import * as _bettercms_ai_types from '@bettercms-ai/types';
|
|
6
|
+
export { BcmsBlocks } from '@bettercms-ai/next/blocks';
|
|
7
|
+
export { BcmsSection, BcmsSections } from '@bettercms-ai/next/sections';
|
|
8
|
+
export { BcmsButton, BcmsField, BcmsImage } from '@bettercms-ai/next/elements';
|
|
9
|
+
export { BcmsForm } from '@bettercms-ai/next/form';
|
|
10
|
+
export { BcmsSearch } from '@bettercms-ai/next/search';
|
|
11
|
+
|
|
12
|
+
/** Where the tab keeps a checked preview token: per tab, gone when it closes. */
|
|
13
|
+
declare const DRAFT_TOKEN_KEY = "bcms-preview-token";
|
|
14
|
+
interface BcmsContext {
|
|
15
|
+
client: BetterCMSReadClient;
|
|
16
|
+
/** The tab is previewing drafts. */
|
|
17
|
+
draft: boolean;
|
|
18
|
+
/** Leave draft mode (forget the token). */
|
|
19
|
+
exitDraft(): void;
|
|
20
|
+
}
|
|
21
|
+
/** A read client for these settings: drafts when a preview token is given. */
|
|
22
|
+
declare function createBcmsClient(options?: BcmsSiteOptions & {
|
|
23
|
+
env?: Record<string, unknown>;
|
|
24
|
+
previewToken?: string | null;
|
|
25
|
+
}): BetterCMSReadClient;
|
|
26
|
+
interface BcmsProviderProps extends BcmsSiteOptions {
|
|
27
|
+
/** The app's env, usually `import.meta.env` (a library cannot read it for you). */
|
|
28
|
+
env?: Record<string, unknown>;
|
|
29
|
+
/** The path the editor's draft link opens. Default `/api/draft`, what the editor uses. */
|
|
30
|
+
draftPath?: string;
|
|
31
|
+
/** A ready client instead of one built from settings (tests, SSR). */
|
|
32
|
+
client?: BetterCMSReadClient;
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
declare function BcmsProvider({ env, draftPath, client: given, children, ...site }: BcmsProviderProps): react.FunctionComponentElement<react.ProviderProps<BcmsContext | null>>;
|
|
36
|
+
/** The client and draft state from the nearest <BcmsProvider>. */
|
|
37
|
+
declare function useBcms(): BcmsContext;
|
|
38
|
+
|
|
39
|
+
interface BcmsQuery<T> {
|
|
40
|
+
data: T | undefined;
|
|
41
|
+
error: Error | null;
|
|
42
|
+
loading: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read with the provider's client; re-reads when `key` or the client (draft mode) changes. A
|
|
46
|
+
* stale answer never overwrites a newer one.
|
|
47
|
+
*/
|
|
48
|
+
declare function useBcmsQuery<T>(key: string, read: (client: BetterCMSReadClient) => Promise<T>): BcmsQuery<T>;
|
|
49
|
+
type Opts<K extends keyof BetterCMSReadClient> = BetterCMSReadClient[K] extends (...a: infer A) => unknown ? A : never;
|
|
50
|
+
/** A page by slug, with its `blocks` for <BcmsBlocks>. */
|
|
51
|
+
declare const usePage: (slug: string) => BcmsQuery<_bettercms_ai_types.DeliveryPage | null>;
|
|
52
|
+
/** An entry by slug. */
|
|
53
|
+
declare const useEntry: <T = Record<string, unknown>>(slug: string, opts?: Opts<"getEntry">[1]) => BcmsQuery<_bettercms_ai_types.DeliveryEntry<T>>;
|
|
54
|
+
/** A list of entries. */
|
|
55
|
+
declare const useEntries: <T = Record<string, unknown>>(opts?: Opts<"listEntries">[0]) => BcmsQuery<_bettercms_ai_types.DeliveryList<_bettercms_ai_types.DeliveryEntry<T>>>;
|
|
56
|
+
/** The project's Global Layout (navigation, footer). */
|
|
57
|
+
declare const useLayout: () => BcmsQuery<_bettercms_ai_types.DeliveredLayout | null>;
|
|
58
|
+
/** Component definitions for <BcmsBlocks components={…}>. */
|
|
59
|
+
declare const useComponents: () => BcmsQuery<_bettercms_ai_types.DeliveryComponent[]>;
|
|
60
|
+
|
|
61
|
+
export { type BcmsContext, BcmsProvider, type BcmsProviderProps, type BcmsQuery, DRAFT_TOKEN_KEY, createBcmsClient, useBcms, useBcmsQuery, useComponents, useEntries, useEntry, useLayout, usePage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/provider.tsx
|
|
2
|
+
import { createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { createClient } from "@bettercms-ai/sdk/read";
|
|
4
|
+
import { resolveBcmsConfig } from "@bettercms-ai/sdk/config";
|
|
5
|
+
import { previewTokenValid, safeRedirectPath } from "@bettercms-ai/sdk/draft";
|
|
6
|
+
var DRAFT_TOKEN_KEY = "bcms-preview-token";
|
|
7
|
+
function createBcmsClient(options = {}) {
|
|
8
|
+
const { env, previewToken, ...site } = options;
|
|
9
|
+
const { config, missing } = resolveBcmsConfig({ options: site, env: env ?? {}, prefixes: ["VITE_", ""] });
|
|
10
|
+
if (missing.length > 0) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`[@bettercms-ai/react] Missing ${missing.join(" and ")}: pass <BcmsProvider env={import.meta.env}> with VITE_BCMS_API_URL and VITE_BCMS_WORKSPACE set, or the values as props.`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return createClient({
|
|
16
|
+
apiUrl: config.apiUrl,
|
|
17
|
+
workspace: config.workspace,
|
|
18
|
+
projectId: config.projectId,
|
|
19
|
+
...previewToken ? { perspective: "drafts", previewToken } : {}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
var Ctx = createContext(null);
|
|
23
|
+
function storedToken() {
|
|
24
|
+
try {
|
|
25
|
+
return sessionStorage.getItem(DRAFT_TOKEN_KEY);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function BcmsProvider({ env, draftPath = "/api/draft", client: given, children, ...site }) {
|
|
31
|
+
const [token, setToken] = useState(() => typeof window === "undefined" ? null : storedToken());
|
|
32
|
+
const { config } = resolveBcmsConfig({ options: site, env: env ?? {}, prefixes: ["VITE_", ""] });
|
|
33
|
+
const { apiUrl, workspace, projectId, mediaUrl, studioUrl } = config;
|
|
34
|
+
const forget = () => {
|
|
35
|
+
try {
|
|
36
|
+
sessionStorage.removeItem(DRAFT_TOKEN_KEY);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
setToken(null);
|
|
40
|
+
};
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (typeof window === "undefined" || !apiUrl) return;
|
|
43
|
+
let live = true;
|
|
44
|
+
const here = window.location.pathname.replace(/\/+$/, "") || "/";
|
|
45
|
+
if (here === (draftPath.replace(/\/+$/, "") || "/")) {
|
|
46
|
+
const url = new URL(window.location.href);
|
|
47
|
+
const fresh = url.searchParams.get("token");
|
|
48
|
+
const to = safeRedirectPath(url.searchParams.get("redirect") ?? "/");
|
|
49
|
+
const land = () => {
|
|
50
|
+
window.history.replaceState(null, "", to);
|
|
51
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
52
|
+
};
|
|
53
|
+
if (!fresh) {
|
|
54
|
+
land();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
void previewTokenValid(apiUrl, fresh).catch(() => false).then((ok) => {
|
|
58
|
+
if (!live) return;
|
|
59
|
+
if (ok) {
|
|
60
|
+
try {
|
|
61
|
+
sessionStorage.setItem(DRAFT_TOKEN_KEY, fresh);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
setToken(fresh);
|
|
65
|
+
}
|
|
66
|
+
land();
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
const kept = storedToken();
|
|
70
|
+
if (kept) void previewTokenValid(apiUrl, kept).catch(() => true).then((ok) => live && !ok && forget());
|
|
71
|
+
}
|
|
72
|
+
return () => {
|
|
73
|
+
live = false;
|
|
74
|
+
};
|
|
75
|
+
}, [draftPath, apiUrl]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (document.querySelector('meta[name="generator"][content="BetterCMS"]')) return;
|
|
78
|
+
document.head.append(Object.assign(document.createElement("meta"), { name: "generator", content: "BetterCMS" }));
|
|
79
|
+
}, []);
|
|
80
|
+
const value = useMemo(
|
|
81
|
+
() => ({
|
|
82
|
+
client: given ?? createBcmsClient({ apiUrl, workspace, projectId, mediaUrl, studioUrl, previewToken: token }),
|
|
83
|
+
draft: Boolean(token),
|
|
84
|
+
exitDraft: forget
|
|
85
|
+
}),
|
|
86
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
+
[given, token, apiUrl, workspace, projectId, mediaUrl, studioUrl]
|
|
88
|
+
);
|
|
89
|
+
return createElement(Ctx.Provider, { value }, children);
|
|
90
|
+
}
|
|
91
|
+
function useBcms() {
|
|
92
|
+
const ctx = useContext(Ctx);
|
|
93
|
+
if (!ctx) throw new Error("[@bettercms-ai/react] useBcms() needs a <BcmsProvider> above it.");
|
|
94
|
+
return ctx;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/hooks.ts
|
|
98
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
99
|
+
function useBcmsQuery(key, read) {
|
|
100
|
+
const { client } = useBcms();
|
|
101
|
+
const [state, setState] = useState2({ data: void 0, error: null, loading: true });
|
|
102
|
+
useEffect2(() => {
|
|
103
|
+
let current = true;
|
|
104
|
+
setState({ data: void 0, error: null, loading: true });
|
|
105
|
+
read(client).then(
|
|
106
|
+
(data) => current && setState({ data, error: null, loading: false }),
|
|
107
|
+
(error) => current && setState({ data: void 0, error: error instanceof Error ? error : new Error(String(error)), loading: false })
|
|
108
|
+
);
|
|
109
|
+
return () => {
|
|
110
|
+
current = false;
|
|
111
|
+
};
|
|
112
|
+
}, [client, key]);
|
|
113
|
+
return state;
|
|
114
|
+
}
|
|
115
|
+
var usePage = (slug) => useBcmsQuery(`page:${slug}`, (c) => c.getPage(slug));
|
|
116
|
+
var useEntry = (slug, opts) => useBcmsQuery(`entry:${slug}:${JSON.stringify(opts ?? {})}`, (c) => c.getEntry(slug, opts));
|
|
117
|
+
var useEntries = (opts) => useBcmsQuery(`entries:${JSON.stringify(opts ?? {})}`, (c) => c.listEntries(opts));
|
|
118
|
+
var useLayout = () => useBcmsQuery("layout", (c) => c.getLayout());
|
|
119
|
+
var useComponents = () => useBcmsQuery("components", (c) => c.listComponents());
|
|
120
|
+
|
|
121
|
+
// src/index.ts
|
|
122
|
+
import { BcmsBlocks } from "@bettercms-ai/next/blocks";
|
|
123
|
+
import { BcmsSections, BcmsSection } from "@bettercms-ai/next/sections";
|
|
124
|
+
import { BcmsField, BcmsImage, BcmsButton } from "@bettercms-ai/next/elements";
|
|
125
|
+
import { BcmsForm } from "@bettercms-ai/next/form";
|
|
126
|
+
import { BcmsSearch } from "@bettercms-ai/next/search";
|
|
127
|
+
export {
|
|
128
|
+
BcmsBlocks,
|
|
129
|
+
BcmsButton,
|
|
130
|
+
BcmsField,
|
|
131
|
+
BcmsForm,
|
|
132
|
+
BcmsImage,
|
|
133
|
+
BcmsProvider,
|
|
134
|
+
BcmsSearch,
|
|
135
|
+
BcmsSection,
|
|
136
|
+
BcmsSections,
|
|
137
|
+
DRAFT_TOKEN_KEY,
|
|
138
|
+
createBcmsClient,
|
|
139
|
+
useBcms,
|
|
140
|
+
useBcmsQuery,
|
|
141
|
+
useComponents,
|
|
142
|
+
useEntries,
|
|
143
|
+
useEntry,
|
|
144
|
+
useLayout,
|
|
145
|
+
usePage
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx","../src/hooks.ts","../src/index.ts"],"sourcesContent":["import { createContext, createElement, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk/read\";\nimport { resolveBcmsConfig, type BcmsSiteOptions } from \"@bettercms-ai/sdk/config\";\nimport { previewTokenValid, safeRedirectPath } from \"@bettercms-ai/sdk/draft\";\n\n/** Where the tab keeps a checked preview token: per tab, gone when it closes. */\nexport const DRAFT_TOKEN_KEY = \"bcms-preview-token\";\n\nexport interface BcmsContext {\n client: BetterCMSReadClient;\n /** The tab is previewing drafts. */\n draft: boolean;\n /** Leave draft mode (forget the token). */\n exitDraft(): void;\n}\n\n/** A read client for these settings: drafts when a preview token is given. */\nexport function createBcmsClient(\n options: BcmsSiteOptions & { env?: Record<string, unknown>; previewToken?: string | null } = {},\n): BetterCMSReadClient {\n const { env, previewToken, ...site } = options;\n const { config, missing } = resolveBcmsConfig({ options: site, env: (env ?? {}) as Record<string, string | undefined>, prefixes: [\"VITE_\", \"\"] });\n if (missing.length > 0) {\n throw new Error(\n `[@bettercms-ai/react] Missing ${missing.join(\" and \")}: pass <BcmsProvider env={import.meta.env}> with VITE_BCMS_API_URL and VITE_BCMS_WORKSPACE set, or the values as props.`,\n );\n }\n return createClient({\n apiUrl: config.apiUrl,\n workspace: config.workspace,\n projectId: config.projectId,\n ...(previewToken ? { perspective: \"drafts\" as const, previewToken } : {}),\n });\n}\n\nconst Ctx = createContext<BcmsContext | null>(null);\n\nexport interface BcmsProviderProps extends BcmsSiteOptions {\n /** The app's env, usually `import.meta.env` (a library cannot read it for you). */\n env?: Record<string, unknown>;\n /** The path the editor's draft link opens. Default `/api/draft`, what the editor uses. */\n draftPath?: string;\n /** A ready client instead of one built from settings (tests, SSR). */\n client?: BetterCMSReadClient;\n children?: ReactNode;\n}\n\nfunction storedToken(): string | null {\n try {\n return sessionStorage.getItem(DRAFT_TOKEN_KEY);\n } catch {\n return null;\n }\n}\n\nexport function BcmsProvider({ env, draftPath = \"/api/draft\", client: given, children, ...site }: BcmsProviderProps) {\n const [token, setToken] = useState<string | null>(() => (typeof window === \"undefined\" ? null : storedToken()));\n // Keyed on the resolved strings, never on `env` itself: a Vite production build inlines\n // `import.meta.env` as a fresh object at each use, so an object key would build a new client\n // (and refetch every hook) on every render.\n const { config } = resolveBcmsConfig({ options: site, env: (env ?? {}) as Record<string, string | undefined>, prefixes: [\"VITE_\", \"\"] });\n const { apiUrl, workspace, projectId, mediaUrl, studioUrl } = config;\n\n const forget = () => {\n try {\n sessionStorage.removeItem(DRAFT_TOKEN_KEY);\n } catch {\n // Nothing kept.\n }\n setToken(null);\n };\n\n useEffect(() => {\n if (typeof window === \"undefined\" || !apiUrl) return;\n let live = true;\n const here = window.location.pathname.replace(/\\/+$/, \"\") || \"/\";\n if (here === (draftPath.replace(/\\/+$/, \"\") || \"/\")) {\n // The editor's draft link: check the token with the backend (the signature authority), keep\n // it for this tab, and land on the page it names. A refused or unreachable check leaves the\n // app published, and the token never stays in the address bar either way.\n const url = new URL(window.location.href);\n const fresh = url.searchParams.get(\"token\");\n const to = safeRedirectPath(url.searchParams.get(\"redirect\") ?? \"/\");\n const land = () => {\n window.history.replaceState(null, \"\", to);\n window.dispatchEvent(new PopStateEvent(\"popstate\"));\n };\n if (!fresh) {\n land();\n return;\n }\n void previewTokenValid(apiUrl, fresh)\n .catch(() => false)\n .then((ok) => {\n if (!live) return;\n if (ok) {\n try {\n sessionStorage.setItem(DRAFT_TOKEN_KEY, fresh);\n } catch {\n // Storage refused (a sandboxed frame): draft mode lasts this page only.\n }\n setToken(fresh);\n }\n land();\n });\n } else {\n // A token kept from earlier in the tab is checked again: an expired one would fail every read.\n const kept = storedToken();\n if (kept) void previewTokenValid(apiUrl, kept).catch(() => true).then((ok) => live && !ok && forget());\n }\n return () => {\n live = false;\n };\n }, [draftPath, apiUrl]);\n\n // `<meta name=\"generator\">`, what Wappalyzer and BuiltWith read to name the CMS. A client-rendered\n // app has no server `<head>` to carry it, so the provider adds it once.\n useEffect(() => {\n if (document.querySelector('meta[name=\"generator\"][content=\"BetterCMS\"]')) return;\n document.head.append(Object.assign(document.createElement(\"meta\"), { name: \"generator\", content: \"BetterCMS\" }));\n }, []);\n\n const value = useMemo<BcmsContext>(\n () => ({\n client: given ?? createBcmsClient({ apiUrl, workspace, projectId, mediaUrl, studioUrl, previewToken: token }),\n draft: Boolean(token),\n exitDraft: forget,\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [given, token, apiUrl, workspace, projectId, mediaUrl, studioUrl],\n );\n return createElement(Ctx.Provider, { value }, children);\n}\n\n/** The client and draft state from the nearest <BcmsProvider>. */\nexport function useBcms(): BcmsContext {\n const ctx = useContext(Ctx);\n if (!ctx) throw new Error(\"[@bettercms-ai/react] useBcms() needs a <BcmsProvider> above it.\");\n return ctx;\n}\n","import { useEffect, useState } from \"react\";\nimport type { BetterCMSReadClient } from \"@bettercms-ai/sdk/read\";\nimport { useBcms } from \"./provider.js\";\n\nexport interface BcmsQuery<T> {\n data: T | undefined;\n error: Error | null;\n loading: boolean;\n}\n\n/**\n * Read with the provider's client; re-reads when `key` or the client (draft mode) changes. A\n * stale answer never overwrites a newer one.\n */\nexport function useBcmsQuery<T>(key: string, read: (client: BetterCMSReadClient) => Promise<T>): BcmsQuery<T> {\n const { client } = useBcms();\n const [state, setState] = useState<BcmsQuery<T>>({ data: undefined, error: null, loading: true });\n useEffect(() => {\n let current = true;\n // A new key shows nothing of the old one's answer while it loads.\n setState({ data: undefined, error: null, loading: true });\n read(client).then(\n (data) => current && setState({ data, error: null, loading: false }),\n (error: unknown) => current && setState({ data: undefined, error: error instanceof Error ? error : new Error(String(error)), loading: false }),\n );\n return () => {\n current = false;\n };\n // `read` is the caller's inline function: the key says when it means something else.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, key]);\n return state;\n}\n\ntype Opts<K extends keyof BetterCMSReadClient> = BetterCMSReadClient[K] extends (...a: infer A) => unknown ? A : never;\n\n/** A page by slug, with its `blocks` for <BcmsBlocks>. */\nexport const usePage = (slug: string) => useBcmsQuery(`page:${slug}`, (c) => c.getPage(slug));\n/** An entry by slug. */\nexport const useEntry = <T = Record<string, unknown>>(slug: string, opts?: Opts<\"getEntry\">[1]) =>\n useBcmsQuery(`entry:${slug}:${JSON.stringify(opts ?? {})}`, (c) => c.getEntry<T>(slug, opts));\n/** A list of entries. */\nexport const useEntries = <T = Record<string, unknown>>(opts?: Opts<\"listEntries\">[0]) =>\n useBcmsQuery(`entries:${JSON.stringify(opts ?? {})}`, (c) => c.listEntries<T>(opts));\n/** The project's Global Layout (navigation, footer). */\nexport const useLayout = () => useBcmsQuery(\"layout\", (c) => c.getLayout());\n/** Component definitions for <BcmsBlocks components={…}>. */\nexport const useComponents = () => useBcmsQuery(\"components\", (c) => c.listComponents());\n","/**\n * @bettercms-ai/react — BetterCMS for client-rendered React apps (Vite, CRA, any SPA).\n *\n * // main.tsx\n * import { BcmsProvider } from \"@bettercms-ai/react\";\n * <BcmsProvider env={import.meta.env}><App /></BcmsProvider>\n *\n * // a page\n * const { data: page } = usePage(\"home\");\n * return page ? <BcmsBlocks blocks={page.blocks} /> : null;\n *\n * Settings resolve the way every adapter resolves them (@bettercms-ai/sdk/config): props, then\n * `VITE_BCMS_*`, then `BCMS_*`. Draft mode needs no server: the editor's draft link lands on the\n * app's own draft path (`/api/draft?token=…&redirect=/x`, which an SPA host serves as the app),\n * the provider checks the token with the backend, keeps it for the tab and moves to `redirect`.\n * The components are @bettercms-ai/next's browser-safe ones, the same markup on both.\n */\nexport { BcmsProvider, useBcms, createBcmsClient, DRAFT_TOKEN_KEY, type BcmsProviderProps, type BcmsContext } from \"./provider.js\";\nexport { usePage, useEntry, useEntries, useLayout, useComponents, useBcmsQuery, type BcmsQuery } from \"./hooks.js\";\nexport { BcmsBlocks } from \"@bettercms-ai/next/blocks\";\nexport { BcmsSections, BcmsSection } from \"@bettercms-ai/next/sections\";\nexport { BcmsField, BcmsImage, BcmsButton } from \"@bettercms-ai/next/elements\";\nexport { BcmsForm } from \"@bettercms-ai/next/form\";\nexport { BcmsSearch } from \"@bettercms-ai/next/search\";\n"],"mappings":";AAAA,SAAS,eAAe,eAAe,YAAY,WAAW,SAAS,gBAAgC;AACvG,SAAS,oBAA8C;AACvD,SAAS,yBAA+C;AACxD,SAAS,mBAAmB,wBAAwB;AAG7C,IAAM,kBAAkB;AAWxB,SAAS,iBACd,UAA6F,CAAC,GACzE;AACrB,QAAM,EAAE,KAAK,cAAc,GAAG,KAAK,IAAI;AACvC,QAAM,EAAE,QAAQ,QAAQ,IAAI,kBAAkB,EAAE,SAAS,MAAM,KAAM,OAAO,CAAC,GAA0C,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC;AAChJ,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI;AAAA,MACR,iCAAiC,QAAQ,KAAK,OAAO,CAAC;AAAA,IACxD;AAAA,EACF;AACA,SAAO,aAAa;AAAA,IAClB,QAAQ,OAAO;AAAA,IACf,WAAW,OAAO;AAAA,IAClB,WAAW,OAAO;AAAA,IAClB,GAAI,eAAe,EAAE,aAAa,UAAmB,aAAa,IAAI,CAAC;AAAA,EACzE,CAAC;AACH;AAEA,IAAM,MAAM,cAAkC,IAAI;AAYlD,SAAS,cAA6B;AACpC,MAAI;AACF,WAAO,eAAe,QAAQ,eAAe;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,aAAa,EAAE,KAAK,YAAY,cAAc,QAAQ,OAAO,UAAU,GAAG,KAAK,GAAsB;AACnH,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,MAAO,OAAO,WAAW,cAAc,OAAO,YAAY,CAAE;AAI9G,QAAM,EAAE,OAAO,IAAI,kBAAkB,EAAE,SAAS,MAAM,KAAM,OAAO,CAAC,GAA0C,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC;AACvI,QAAM,EAAE,QAAQ,WAAW,WAAW,UAAU,UAAU,IAAI;AAE9D,QAAM,SAAS,MAAM;AACnB,QAAI;AACF,qBAAe,WAAW,eAAe;AAAA,IAC3C,QAAQ;AAAA,IAER;AACA,aAAS,IAAI;AAAA,EACf;AAEA,YAAU,MAAM;AACd,QAAI,OAAO,WAAW,eAAe,CAAC,OAAQ;AAC9C,QAAI,OAAO;AACX,UAAM,OAAO,OAAO,SAAS,SAAS,QAAQ,QAAQ,EAAE,KAAK;AAC7D,QAAI,UAAU,UAAU,QAAQ,QAAQ,EAAE,KAAK,MAAM;AAInD,YAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,YAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAC1C,YAAM,KAAK,iBAAiB,IAAI,aAAa,IAAI,UAAU,KAAK,GAAG;AACnE,YAAM,OAAO,MAAM;AACjB,eAAO,QAAQ,aAAa,MAAM,IAAI,EAAE;AACxC,eAAO,cAAc,IAAI,cAAc,UAAU,CAAC;AAAA,MACpD;AACA,UAAI,CAAC,OAAO;AACV,aAAK;AACL;AAAA,MACF;AACA,WAAK,kBAAkB,QAAQ,KAAK,EACjC,MAAM,MAAM,KAAK,EACjB,KAAK,CAAC,OAAO;AACZ,YAAI,CAAC,KAAM;AACX,YAAI,IAAI;AACN,cAAI;AACF,2BAAe,QAAQ,iBAAiB,KAAK;AAAA,UAC/C,QAAQ;AAAA,UAER;AACA,mBAAS,KAAK;AAAA,QAChB;AACA,aAAK;AAAA,MACP,CAAC;AAAA,IACL,OAAO;AAEL,YAAM,OAAO,YAAY;AACzB,UAAI,KAAM,MAAK,kBAAkB,QAAQ,IAAI,EAAE,MAAM,MAAM,IAAI,EAAE,KAAK,CAAC,OAAO,QAAQ,CAAC,MAAM,OAAO,CAAC;AAAA,IACvG;AACA,WAAO,MAAM;AACX,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,WAAW,MAAM,CAAC;AAItB,YAAU,MAAM;AACd,QAAI,SAAS,cAAc,6CAA6C,EAAG;AAC3E,aAAS,KAAK,OAAO,OAAO,OAAO,SAAS,cAAc,MAAM,GAAG,EAAE,MAAM,aAAa,SAAS,YAAY,CAAC,CAAC;AAAA,EACjH,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL,QAAQ,SAAS,iBAAiB,EAAE,QAAQ,WAAW,WAAW,UAAU,WAAW,cAAc,MAAM,CAAC;AAAA,MAC5G,OAAO,QAAQ,KAAK;AAAA,MACpB,WAAW;AAAA,IACb;AAAA;AAAA,IAEA,CAAC,OAAO,OAAO,QAAQ,WAAW,WAAW,UAAU,SAAS;AAAA,EAClE;AACA,SAAO,cAAc,IAAI,UAAU,EAAE,MAAM,GAAG,QAAQ;AACxD;AAGO,SAAS,UAAuB;AACrC,QAAM,MAAM,WAAW,GAAG;AAC1B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,kEAAkE;AAC5F,SAAO;AACT;;;AC3IA,SAAS,aAAAA,YAAW,YAAAC,iBAAgB;AAc7B,SAAS,aAAgB,KAAa,MAAiE;AAC5G,QAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAM,CAAC,OAAO,QAAQ,IAAIC,UAAuB,EAAE,MAAM,QAAW,OAAO,MAAM,SAAS,KAAK,CAAC;AAChG,EAAAC,WAAU,MAAM;AACd,QAAI,UAAU;AAEd,aAAS,EAAE,MAAM,QAAW,OAAO,MAAM,SAAS,KAAK,CAAC;AACxD,SAAK,MAAM,EAAE;AAAA,MACX,CAAC,SAAS,WAAW,SAAS,EAAE,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC;AAAA,MACnE,CAAC,UAAmB,WAAW,SAAS,EAAE,MAAM,QAAW,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,GAAG,SAAS,MAAM,CAAC;AAAA,IAC/I;AACA,WAAO,MAAM;AACX,gBAAU;AAAA,IACZ;AAAA,EAGF,GAAG,CAAC,QAAQ,GAAG,CAAC;AAChB,SAAO;AACT;AAKO,IAAM,UAAU,CAAC,SAAiB,aAAa,QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,QAAQ,IAAI,CAAC;AAErF,IAAM,WAAW,CAA8B,MAAc,SAClE,aAAa,SAAS,IAAI,IAAI,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,SAAY,MAAM,IAAI,CAAC;AAEvF,IAAM,aAAa,CAA8B,SACtD,aAAa,WAAW,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,YAAe,IAAI,CAAC;AAE9E,IAAM,YAAY,MAAM,aAAa,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC;AAEnE,IAAM,gBAAgB,MAAM,aAAa,cAAc,CAAC,MAAM,EAAE,eAAe,CAAC;;;AC5BvF,SAAS,kBAAkB;AAC3B,SAAS,cAAc,mBAAmB;AAC1C,SAAS,WAAW,WAAW,kBAAkB;AACjD,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;","names":["useEffect","useState","useState","useEffect"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettercms-ai/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "BetterCMS for React apps (Vite and other client-rendered React): provider, hooks, draft mode by link, and the section and field components.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@bettercms-ai/next": "^0.30.0",
|
|
23
|
+
"@bettercms-ai/sdk": "^1.34.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"prepublishOnly": "bun run build"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20",
|
|
36
|
+
"@types/react": "^19.0.0",
|
|
37
|
+
"react": "^19.0.0",
|
|
38
|
+
"react-dom": "^19.0.0",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"typescript": "^5",
|
|
41
|
+
"vitest": "^4.1.4"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"registry": "https://registry.npmjs.org/"
|
|
46
|
+
}
|
|
47
|
+
}
|