@typecms/sdk 0.1.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 +151 -0
- package/dist/client-BW6BWpfF.d.mts +92 -0
- package/dist/flags/index.d.mts +100 -0
- package/dist/flags/index.mjs +99 -0
- package/dist/flags/index.mjs.map +1 -0
- package/dist/index.d.mts +67 -0
- package/dist/index.mjs +174 -0
- package/dist/index.mjs.map +1 -0
- package/dist/overlay-CLYAeNwg.d.mts +43 -0
- package/dist/preview/dom.d.mts +76 -0
- package/dist/preview/dom.mjs +352 -0
- package/dist/preview/dom.mjs.map +1 -0
- package/dist/preview/index.d.mts +438 -0
- package/dist/preview/index.mjs +736 -0
- package/dist/preview/index.mjs.map +1 -0
- package/dist/preview/next.d.mts +99 -0
- package/dist/preview/next.mjs +31 -0
- package/dist/preview/next.mjs.map +1 -0
- package/dist/preview/svelte.d.mts +99 -0
- package/dist/preview/svelte.mjs +437 -0
- package/dist/preview/svelte.mjs.map +1 -0
- package/dist/preview/vue.d.mts +113 -0
- package/dist/preview/vue.mjs +587 -0
- package/dist/preview/vue.mjs.map +1 -0
- package/dist/types/index.d.mts +358 -0
- package/dist/types/index.mjs +8 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-BNHDdA2G.d.mts +117 -0
- package/package.json +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# @typecms/sdk
|
|
2
|
+
|
|
3
|
+
Typed JavaScript/TypeScript SDK for Type CMS: content delivery, live preview, and feature flags.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @typecms/sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Content client
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { createClient } from '@typecms/sdk'
|
|
13
|
+
|
|
14
|
+
const cms = createClient({
|
|
15
|
+
projectId: 'your-project-id',
|
|
16
|
+
token: process.env.TYPECMS_TOKEN!, // Project Settings → Tokens
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The token is bound to an environment — content is always served from that environment.
|
|
21
|
+
|
|
22
|
+
### Fetch a single entry
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// By URL path (localized path aliases match too)
|
|
26
|
+
const home = await cms.getEntry({ path: '/' })
|
|
27
|
+
|
|
28
|
+
// By slug or entry id
|
|
29
|
+
const post = await cms.getEntry({ slug: 'hello-world', language: 'fr-FR' })
|
|
30
|
+
const byId = await cms.getEntry({ entryId: '665f...' })
|
|
31
|
+
|
|
32
|
+
// null when nothing is published — no try/catch needed for missing pages
|
|
33
|
+
if (!home) return notFound()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Type the `content` payload per call:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
interface HomePage {
|
|
40
|
+
heading: string
|
|
41
|
+
hero: { image: Asset; cta: Link }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const home = await cms.getEntry<HomePage>({ path: '/' })
|
|
45
|
+
home?.content.heading // typed
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Personalization (audiences)
|
|
49
|
+
|
|
50
|
+
Ask for an audience's variant; when it has no published row you get the base
|
|
51
|
+
content automatically. The row's `variantSlug` tells you which one was served.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const page = await cms.getEntry({ path: '/', variant: 'vip' })
|
|
55
|
+
page?.variantSlug // 'vip' or null (base)
|
|
56
|
+
|
|
57
|
+
// Client-wide personalization lens
|
|
58
|
+
const vipCms = createClient({ projectId, token, variant: 'vip' })
|
|
59
|
+
await vipCms.getEntry({ path: '/' }) // vip with base fallback
|
|
60
|
+
await vipCms.getEntry({ path: '/', variant: null }) // force base
|
|
61
|
+
|
|
62
|
+
// Strict mode: no fallback
|
|
63
|
+
await cms.getEntry({ path: '/', variant: 'vip', variantFallback: false })
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Lists
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const { entries, count } = await cms.getEntries({
|
|
70
|
+
templateId: '665f...',
|
|
71
|
+
filter: { tags: 'featured' },
|
|
72
|
+
sort: '-publishedAt', // '-' prefix = descending
|
|
73
|
+
limit: 10,
|
|
74
|
+
skip: 0,
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Drafts (live preview)
|
|
79
|
+
|
|
80
|
+
With a token bound to a **preview environment**, read unpublished content in
|
|
81
|
+
exactly the same shapes. Draft responses are never CDN-cached.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const draft = await cms.getEntry({ path: '/', draft: true })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Full-text search
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const { entries: hits } = await cms.search('astronaut', {
|
|
91
|
+
limit: 5,
|
|
92
|
+
filters: { publishedAt: ['2026-01-01', '2026-07-01'] },
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Escape hatch
|
|
97
|
+
|
|
98
|
+
Anything the helpers don't cover — POST a raw query body:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const page = await cms.query({ publishedBy: '665f...', language: 'de-DE' }, { limit: 20 })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Errors
|
|
105
|
+
|
|
106
|
+
Missing content is `null` / an empty page. Real failures (auth, validation,
|
|
107
|
+
network, timeouts) throw `TypeCMSError` with `status` and optional `details`.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { TypeCMSError } from '@typecms/sdk'
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await cms.getEntry({ path: '/' })
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof TypeCMSError && error.status === 401) {
|
|
116
|
+
// bad/disabled token
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Live preview — `@typecms/sdk/preview`
|
|
122
|
+
|
|
123
|
+
Real-time editing preview for sites embedded in the Type CMS editor's preview
|
|
124
|
+
pane. Edits stream over postMessage and merge over your fetched content.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { PreviewProvider, usePreviewContent } from '@typecms/sdk/preview'
|
|
128
|
+
|
|
129
|
+
function Page({ entry }) {
|
|
130
|
+
const content = usePreviewContent(entry.content) // live values merged in
|
|
131
|
+
return <h1 data-preview-field="title">{content.title}</h1>
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`data-preview-field="<path>"` enables click-to-edit: clicking the element in
|
|
136
|
+
the preview scrolls the editor to that field.
|
|
137
|
+
|
|
138
|
+
## Feature flags — `@typecms/sdk/flags`
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { initFlags } from '@typecms/sdk/flags'
|
|
142
|
+
|
|
143
|
+
const flags = await initFlags({
|
|
144
|
+
clientKey: 'sdk_xxxxxxxx',
|
|
145
|
+
attributes: { id: user.id, plan: user.plan },
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if (flags.isOn('new-checkout')) {
|
|
149
|
+
// ...
|
|
150
|
+
}
|
|
151
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { P as PreviewState, a as PreviewConfig } from './types-BNHDdA2G.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Preview Client
|
|
5
|
+
*
|
|
6
|
+
* Core client for handling live preview PostMessage communication.
|
|
7
|
+
* This runs on the preview site (your frontend) inside an iframe.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type PreviewListener = (state: PreviewState) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Create a preview client instance.
|
|
13
|
+
*
|
|
14
|
+
* This handles PostMessage communication between the Type CMS editor
|
|
15
|
+
* and your preview site. The client runs in the preview iframe.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { createPreviewClient } from '@typecms/sdk/preview'
|
|
20
|
+
*
|
|
21
|
+
* const preview = createPreviewClient({
|
|
22
|
+
* debug: true,
|
|
23
|
+
* onUpdate: (data) => {
|
|
24
|
+
* console.log('Value changed:', data.path, data.value)
|
|
25
|
+
* },
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* // Start listening for messages
|
|
29
|
+
* preview.connect()
|
|
30
|
+
*
|
|
31
|
+
* // Get current preview state
|
|
32
|
+
* const state = preview.getState()
|
|
33
|
+
*
|
|
34
|
+
* // Cleanup when done
|
|
35
|
+
* preview.disconnect()
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function createPreviewClient(config?: PreviewConfig): {
|
|
39
|
+
/**
|
|
40
|
+
* Start listening for preview messages.
|
|
41
|
+
* Call this on mount.
|
|
42
|
+
*/
|
|
43
|
+
connect(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Stop listening for preview messages.
|
|
46
|
+
* Call this on unmount.
|
|
47
|
+
*/
|
|
48
|
+
disconnect(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Get current preview state.
|
|
51
|
+
*/
|
|
52
|
+
getState(): PreviewState;
|
|
53
|
+
/**
|
|
54
|
+
* Check if preview mode is active.
|
|
55
|
+
*/
|
|
56
|
+
isEnabled(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Get a preview value by path.
|
|
59
|
+
* Returns undefined if no preview value exists.
|
|
60
|
+
*/
|
|
61
|
+
getValue<T = unknown>(path: string): T | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Subscribe to state changes.
|
|
64
|
+
*/
|
|
65
|
+
subscribe(listener: PreviewListener): () => void;
|
|
66
|
+
/**
|
|
67
|
+
* Notify the editor that the preview has navigated.
|
|
68
|
+
*/
|
|
69
|
+
notifyNavigation(pathname: string): void;
|
|
70
|
+
/**
|
|
71
|
+
* Notify the editor that a field was clicked in the preview.
|
|
72
|
+
* The editor will scroll to the corresponding field.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // On click of an element with data-preview-field attribute:
|
|
77
|
+
* const path = el.closest('[data-preview-field]')?.dataset.previewField
|
|
78
|
+
* if (path) preview.notifyFieldFocus(path)
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
notifyFieldFocus(path: string): void;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Get a nested value by path (supports "items[0].name" array notation)
|
|
85
|
+
*/
|
|
86
|
+
declare function getNestedValue(obj: Record<string, unknown>, path: string): unknown;
|
|
87
|
+
/**
|
|
88
|
+
* Deep merge two objects
|
|
89
|
+
*/
|
|
90
|
+
declare function deepMerge<T extends Record<string, unknown>>(target: T, source: Record<string, unknown>): T;
|
|
91
|
+
|
|
92
|
+
export { type PreviewListener as P, createPreviewClient as c, deepMerge as d, getNestedValue as g };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Attributes, GrowthBook, StickyBucketService } from '@growthbook/growthbook';
|
|
2
|
+
export { Attributes, GrowthBook } from '@growthbook/growthbook';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typecms/sdk/flags
|
|
6
|
+
*
|
|
7
|
+
* Feature flags client for Type CMS. A thin, typed wrapper around the
|
|
8
|
+
* GrowthBook JS SDK (peer dependency) pointed at Type CMS as the flag
|
|
9
|
+
* backend — Type CMS serves the GrowthBook SDK payload contract at
|
|
10
|
+
* `GET {apiHost}/api/features/{clientKey}`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { initFlags } from '@typecms/sdk/flags'
|
|
15
|
+
*
|
|
16
|
+
* const flags = await initFlags({
|
|
17
|
+
* clientKey: 'sdk_xxxxxxxx',
|
|
18
|
+
* attributes: { id: user.id, plan: user.plan },
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* if (flags.isOn('new-checkout')) {
|
|
22
|
+
* // ...
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
interface TypeCMSFlagsConfig {
|
|
28
|
+
/** SDK connection client key (Project settings → Feature Flags → SDK connections) */
|
|
29
|
+
clientKey: string;
|
|
30
|
+
/** Override for self-hosted / regional API hosts */
|
|
31
|
+
apiHost?: string;
|
|
32
|
+
/** Targeting attributes (id, plan, country, …) */
|
|
33
|
+
attributes?: Attributes;
|
|
34
|
+
/**
|
|
35
|
+
* Decryption key for connections created with payload encryption.
|
|
36
|
+
* Never ship alongside truly sensitive flag values — encryption hides
|
|
37
|
+
* payloads from casual inspection, it is not access control.
|
|
38
|
+
*/
|
|
39
|
+
decryptionKey?: string;
|
|
40
|
+
/** Callback fired on experiment exposure (wire to your analytics) */
|
|
41
|
+
trackingCallback?: ConstructorParameters<typeof GrowthBook>[0] extends infer C ? C extends {
|
|
42
|
+
trackingCallback?: infer T;
|
|
43
|
+
} ? T : never : never;
|
|
44
|
+
/** Enable SSE streaming updates when the host supports it (off by default) */
|
|
45
|
+
streaming?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve Type CMS audience membership for this visitor (headers, cookies,
|
|
48
|
+
* geo) and inject it as the `audiences` attribute, so flag conditions can
|
|
49
|
+
* target audiences: { audiences: { $in: ["beta-users"] } }. Adds one
|
|
50
|
+
* uncached request during init.
|
|
51
|
+
*/
|
|
52
|
+
resolveAudiences?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Sticky bucketing: keep experiment assignments consistent across
|
|
55
|
+
* sessions/devices even when weights or coverage change mid-experiment.
|
|
56
|
+
* 'cookie' / 'localStorage' use the GrowthBook browser implementations;
|
|
57
|
+
* pass a custom StickyBucketService for server-backed storage.
|
|
58
|
+
*/
|
|
59
|
+
stickyBucketing?: 'cookie' | 'localStorage' | StickyBucketService;
|
|
60
|
+
}
|
|
61
|
+
/** Structural match for GrowthBook's js-cookie-shaped `JsCookiesCompat`. */
|
|
62
|
+
interface CookieAttributes {
|
|
63
|
+
path?: string;
|
|
64
|
+
expires?: number | Date;
|
|
65
|
+
domain?: string;
|
|
66
|
+
secure?: boolean;
|
|
67
|
+
sameSite?: string;
|
|
68
|
+
}
|
|
69
|
+
interface DocumentCookieAdapter {
|
|
70
|
+
set(name: string, value: string, options?: CookieAttributes): string | undefined;
|
|
71
|
+
get(name: string): string | undefined;
|
|
72
|
+
get(): Record<string, string>;
|
|
73
|
+
remove(name: string, options?: CookieAttributes): void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Minimal document.cookie adapter satisfying GrowthBook's js-cookie-shaped
|
|
77
|
+
* dependency, so 'cookie' sticky bucketing works without shipping js-cookie.
|
|
78
|
+
* Exported for tests; not part of the public API surface.
|
|
79
|
+
*/
|
|
80
|
+
declare function createDocumentCookieAdapter(): DocumentCookieAdapter;
|
|
81
|
+
/**
|
|
82
|
+
* Create a GrowthBook instance configured for Type CMS. Does not fetch —
|
|
83
|
+
* call `init()` yourself or use `initFlags` below.
|
|
84
|
+
*/
|
|
85
|
+
declare function createFlagsClient(config: TypeCMSFlagsConfig): GrowthBook;
|
|
86
|
+
/**
|
|
87
|
+
* Fetch this visitor's Type CMS audience memberships (slugs). Audience rules
|
|
88
|
+
* evaluate from request attributes (IP/geo/headers) at the API edge, not from
|
|
89
|
+
* an authenticated session — so this request is credential-less (`omit`). The
|
|
90
|
+
* attributes endpoint deliberately does not accept credentials; sending them
|
|
91
|
+
* would only get the response blocked cross-origin. Failures return [] — flags
|
|
92
|
+
* must not break when the attributes endpoint is unreachable.
|
|
93
|
+
*/
|
|
94
|
+
declare function fetchAudiences(config: TypeCMSFlagsConfig): Promise<string[]>;
|
|
95
|
+
/**
|
|
96
|
+
* Create + initialize (fetch the payload) in one call.
|
|
97
|
+
*/
|
|
98
|
+
declare function initFlags(config: TypeCMSFlagsConfig): Promise<GrowthBook>;
|
|
99
|
+
|
|
100
|
+
export { type DocumentCookieAdapter, type TypeCMSFlagsConfig, createDocumentCookieAdapter, createFlagsClient, fetchAudiences, initFlags };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/flags/index.ts
|
|
2
|
+
import {
|
|
3
|
+
GrowthBook,
|
|
4
|
+
BrowserCookieStickyBucketService,
|
|
5
|
+
LocalStorageStickyBucketService
|
|
6
|
+
} from "@growthbook/growthbook";
|
|
7
|
+
var DEFAULT_API_HOST = "https://api.typecms.com";
|
|
8
|
+
function createDocumentCookieAdapter() {
|
|
9
|
+
const readAll = () => {
|
|
10
|
+
const out = {};
|
|
11
|
+
if (typeof document === "undefined" || !document.cookie) return out;
|
|
12
|
+
for (const pair of document.cookie.split("; ")) {
|
|
13
|
+
const eq = pair.indexOf("=");
|
|
14
|
+
if (eq === -1) continue;
|
|
15
|
+
out[decodeURIComponent(pair.slice(0, eq))] = decodeURIComponent(pair.slice(eq + 1));
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
};
|
|
19
|
+
function get(name) {
|
|
20
|
+
const all = readAll();
|
|
21
|
+
return name === void 0 ? all : all[name];
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
get,
|
|
25
|
+
set(name, value, options) {
|
|
26
|
+
if (typeof document === "undefined") return void 0;
|
|
27
|
+
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}; path=${options?.path ?? "/"}`;
|
|
28
|
+
if (options?.expires !== void 0) {
|
|
29
|
+
const expires = typeof options.expires === "number" ? new Date(Date.now() + options.expires * 864e5) : options.expires;
|
|
30
|
+
cookie += `; expires=${expires.toUTCString()}`;
|
|
31
|
+
}
|
|
32
|
+
if (options?.domain) cookie += `; domain=${options.domain}`;
|
|
33
|
+
if (options?.secure) cookie += "; secure";
|
|
34
|
+
if (options?.sameSite) cookie += `; samesite=${options.sameSite}`;
|
|
35
|
+
document.cookie = cookie;
|
|
36
|
+
return cookie;
|
|
37
|
+
},
|
|
38
|
+
remove(name, options) {
|
|
39
|
+
this.set(name, "", { ...options, expires: -1 });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function resolveStickyBucketService(config) {
|
|
44
|
+
if (!config) return void 0;
|
|
45
|
+
if (config === "cookie") {
|
|
46
|
+
return new BrowserCookieStickyBucketService({ jsCookie: createDocumentCookieAdapter() });
|
|
47
|
+
}
|
|
48
|
+
if (config === "localStorage") return new LocalStorageStickyBucketService();
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
function createFlagsClient(config) {
|
|
52
|
+
const { clientKey, apiHost, attributes, decryptionKey, trackingCallback, streaming } = config;
|
|
53
|
+
if (!clientKey || !clientKey.startsWith("sdk_")) {
|
|
54
|
+
throw new Error("@typecms/sdk/flags: clientKey must be an SDK connection key (sdk_...)");
|
|
55
|
+
}
|
|
56
|
+
return new GrowthBook({
|
|
57
|
+
apiHost: (apiHost ?? DEFAULT_API_HOST).replace(/\/$/, ""),
|
|
58
|
+
clientKey,
|
|
59
|
+
attributes,
|
|
60
|
+
decryptionKey,
|
|
61
|
+
trackingCallback,
|
|
62
|
+
backgroundSync: streaming === true,
|
|
63
|
+
stickyBucketService: resolveStickyBucketService(config.stickyBucketing)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function fetchAudiences(config) {
|
|
67
|
+
const host = (config.apiHost ?? DEFAULT_API_HOST).replace(/\/$/, "");
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(`${host}/api/features/${config.clientKey}/attributes`, {
|
|
70
|
+
credentials: "omit"
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) return [];
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
return Array.isArray(data.audiences) ? data.audiences : [];
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function initFlags(config) {
|
|
80
|
+
const gb = createFlagsClient(config);
|
|
81
|
+
if (config.resolveAudiences) {
|
|
82
|
+
const [audiences] = await Promise.all([
|
|
83
|
+
fetchAudiences(config),
|
|
84
|
+
gb.init({ streaming: config.streaming === true })
|
|
85
|
+
]);
|
|
86
|
+
gb.setAttributes({ ...gb.getAttributes(), audiences });
|
|
87
|
+
} else {
|
|
88
|
+
await gb.init({ streaming: config.streaming === true });
|
|
89
|
+
}
|
|
90
|
+
return gb;
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
GrowthBook,
|
|
94
|
+
createDocumentCookieAdapter,
|
|
95
|
+
createFlagsClient,
|
|
96
|
+
fetchAudiences,
|
|
97
|
+
initFlags
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/flags/index.ts"],"sourcesContent":["/**\n * @typecms/sdk/flags\n *\n * Feature flags client for Type CMS. A thin, typed wrapper around the\n * GrowthBook JS SDK (peer dependency) pointed at Type CMS as the flag\n * backend — Type CMS serves the GrowthBook SDK payload contract at\n * `GET {apiHost}/api/features/{clientKey}`.\n *\n * @example\n * ```typescript\n * import { initFlags } from '@typecms/sdk/flags'\n *\n * const flags = await initFlags({\n * clientKey: 'sdk_xxxxxxxx',\n * attributes: { id: user.id, plan: user.plan },\n * })\n *\n * if (flags.isOn('new-checkout')) {\n * // ...\n * }\n * ```\n */\n\nimport {\n GrowthBook,\n BrowserCookieStickyBucketService,\n LocalStorageStickyBucketService,\n} from '@growthbook/growthbook'\nimport type { Attributes, StickyBucketService } from '@growthbook/growthbook'\n\nconst DEFAULT_API_HOST = 'https://api.typecms.com'\n\nexport interface TypeCMSFlagsConfig {\n /** SDK connection client key (Project settings → Feature Flags → SDK connections) */\n clientKey: string\n /** Override for self-hosted / regional API hosts */\n apiHost?: string\n /** Targeting attributes (id, plan, country, …) */\n attributes?: Attributes\n /**\n * Decryption key for connections created with payload encryption.\n * Never ship alongside truly sensitive flag values — encryption hides\n * payloads from casual inspection, it is not access control.\n */\n decryptionKey?: string\n /** Callback fired on experiment exposure (wire to your analytics) */\n trackingCallback?: ConstructorParameters<typeof GrowthBook>[0] extends infer C\n ? C extends { trackingCallback?: infer T }\n ? T\n : never\n : never\n /** Enable SSE streaming updates when the host supports it (off by default) */\n streaming?: boolean\n /**\n * Resolve Type CMS audience membership for this visitor (headers, cookies,\n * geo) and inject it as the `audiences` attribute, so flag conditions can\n * target audiences: { audiences: { $in: [\"beta-users\"] } }. Adds one\n * uncached request during init.\n */\n resolveAudiences?: boolean\n /**\n * Sticky bucketing: keep experiment assignments consistent across\n * sessions/devices even when weights or coverage change mid-experiment.\n * 'cookie' / 'localStorage' use the GrowthBook browser implementations;\n * pass a custom StickyBucketService for server-backed storage.\n */\n stickyBucketing?: 'cookie' | 'localStorage' | StickyBucketService\n}\n\n/** Structural match for GrowthBook's js-cookie-shaped `JsCookiesCompat`. */\ninterface CookieAttributes {\n path?: string\n expires?: number | Date\n domain?: string\n secure?: boolean\n sameSite?: string\n}\n\nexport interface DocumentCookieAdapter {\n set(name: string, value: string, options?: CookieAttributes): string | undefined\n get(name: string): string | undefined\n get(): Record<string, string>\n remove(name: string, options?: CookieAttributes): void\n}\n\n/**\n * Minimal document.cookie adapter satisfying GrowthBook's js-cookie-shaped\n * dependency, so 'cookie' sticky bucketing works without shipping js-cookie.\n * Exported for tests; not part of the public API surface.\n */\nexport function createDocumentCookieAdapter(): DocumentCookieAdapter {\n const readAll = (): Record<string, string> => {\n const out: Record<string, string> = {}\n if (typeof document === 'undefined' || !document.cookie) return out\n for (const pair of document.cookie.split('; ')) {\n const eq = pair.indexOf('=')\n if (eq === -1) continue\n out[decodeURIComponent(pair.slice(0, eq))] = decodeURIComponent(pair.slice(eq + 1))\n }\n return out\n }\n\n function get(): Record<string, string>\n function get(name: string): string | undefined\n function get(name?: string): Record<string, string> | string | undefined {\n const all = readAll()\n return name === undefined ? all : all[name]\n }\n\n return {\n get,\n set(name, value, options) {\n if (typeof document === 'undefined') return undefined\n let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}; path=${options?.path ?? '/'}`\n if (options?.expires !== undefined) {\n const expires =\n typeof options.expires === 'number'\n ? new Date(Date.now() + options.expires * 864e5)\n : options.expires\n cookie += `; expires=${expires.toUTCString()}`\n }\n if (options?.domain) cookie += `; domain=${options.domain}`\n if (options?.secure) cookie += '; secure'\n if (options?.sameSite) cookie += `; samesite=${options.sameSite}`\n document.cookie = cookie\n return cookie\n },\n remove(name, options) {\n this.set(name, '', { ...options, expires: -1 })\n },\n }\n}\n\nfunction resolveStickyBucketService(\n config: TypeCMSFlagsConfig['stickyBucketing']\n): StickyBucketService | undefined {\n if (!config) return undefined\n if (config === 'cookie') {\n return new BrowserCookieStickyBucketService({ jsCookie: createDocumentCookieAdapter() })\n }\n if (config === 'localStorage') return new LocalStorageStickyBucketService()\n return config\n}\n\n/**\n * Create a GrowthBook instance configured for Type CMS. Does not fetch —\n * call `init()` yourself or use `initFlags` below.\n */\nexport function createFlagsClient(config: TypeCMSFlagsConfig): GrowthBook {\n const { clientKey, apiHost, attributes, decryptionKey, trackingCallback, streaming } = config\n\n if (!clientKey || !clientKey.startsWith('sdk_')) {\n throw new Error('@typecms/sdk/flags: clientKey must be an SDK connection key (sdk_...)')\n }\n\n return new GrowthBook({\n apiHost: (apiHost ?? DEFAULT_API_HOST).replace(/\\/$/, ''),\n clientKey,\n attributes,\n decryptionKey,\n trackingCallback,\n backgroundSync: streaming === true,\n stickyBucketService: resolveStickyBucketService(config.stickyBucketing),\n })\n}\n\n/**\n * Fetch this visitor's Type CMS audience memberships (slugs). Audience rules\n * evaluate from request attributes (IP/geo/headers) at the API edge, not from\n * an authenticated session — so this request is credential-less (`omit`). The\n * attributes endpoint deliberately does not accept credentials; sending them\n * would only get the response blocked cross-origin. Failures return [] — flags\n * must not break when the attributes endpoint is unreachable.\n */\nexport async function fetchAudiences(config: TypeCMSFlagsConfig): Promise<string[]> {\n const host = (config.apiHost ?? DEFAULT_API_HOST).replace(/\\/$/, '')\n try {\n const response = await fetch(`${host}/api/features/${config.clientKey}/attributes`, {\n credentials: 'omit',\n })\n if (!response.ok) return []\n const data = (await response.json()) as { audiences?: string[] }\n return Array.isArray(data.audiences) ? data.audiences : []\n } catch {\n return []\n }\n}\n\n/**\n * Create + initialize (fetch the payload) in one call.\n */\nexport async function initFlags(config: TypeCMSFlagsConfig): Promise<GrowthBook> {\n const gb = createFlagsClient(config)\n\n if (config.resolveAudiences) {\n // Audience resolution and payload fetch run concurrently\n const [audiences] = await Promise.all([\n fetchAudiences(config),\n gb.init({ streaming: config.streaming === true }),\n ])\n gb.setAttributes({ ...gb.getAttributes(), audiences })\n } else {\n await gb.init({ streaming: config.streaming === true })\n }\n\n return gb\n}\n\n// Re-export the evaluation surface so consumers don't need a direct\n// @growthbook/growthbook import for common usage.\nexport { GrowthBook }\nexport type { Attributes }\n"],"mappings":"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","names":[]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { TypeCMSClientConfig, TypeCMSClient, SortExpression } from './types/index.mjs';
|
|
2
|
+
export { APIError, Address, Asset, AssetType, BaseComponent, BaseEntry, CollectionReference, ContentEntry, EntryList, EntryReference, ExtractComponent, GetEntriesOptions, GetEntryOptions, Link, RichTextNode, SearchHit, SearchOptions, SearchResult, TipTapMark, TipTapNode, isComponentType } from './types/index.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type CMS SDK Client
|
|
6
|
+
*
|
|
7
|
+
* Typed client for the Type CMS Content Delivery API
|
|
8
|
+
* (`POST /api/projects/:projectId/content` and `/content/search`).
|
|
9
|
+
*
|
|
10
|
+
* Design notes:
|
|
11
|
+
* - `getEntry` returns `null` (and `getEntries`/`search` return empty pages)
|
|
12
|
+
* when nothing matches — the API's "empty result" 404 is not an exception.
|
|
13
|
+
* - Personalization is first-class: set a client-wide `variant` or pass one
|
|
14
|
+
* per call; when the variant has no published row the base content is
|
|
15
|
+
* served automatically (opt out with `variantFallback: false`).
|
|
16
|
+
* - Drafts: `draft: true` reads unpublished content through the same shapes,
|
|
17
|
+
* gated server-side to preview-environment tokens.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error thrown when an API request fails (auth, validation, server errors).
|
|
22
|
+
* Empty results are NOT errors — the typed reads return `null`/empty pages.
|
|
23
|
+
*/
|
|
24
|
+
declare class TypeCMSError extends Error {
|
|
25
|
+
/** HTTP status code */
|
|
26
|
+
readonly status: number;
|
|
27
|
+
/** Validation error details from the API, when present */
|
|
28
|
+
readonly details?: unknown[];
|
|
29
|
+
constructor(message: string, status: number, details?: unknown[]);
|
|
30
|
+
}
|
|
31
|
+
/** `'-publishedAt'` → `'publishedAt:-1'`, `'title'` → `'title:1'`. */
|
|
32
|
+
declare function toApiSort(sort: SortExpression): string;
|
|
33
|
+
/**
|
|
34
|
+
* Create a Type CMS content client.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { createClient } from '@typecms/sdk'
|
|
39
|
+
*
|
|
40
|
+
* const cms = createClient({
|
|
41
|
+
* projectId: '665f00000000000000000001',
|
|
42
|
+
* token: process.env.TYPECMS_TOKEN!,
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* // By URL path — null when nothing is published there
|
|
46
|
+
* const home = await cms.getEntry<HomePage>({ path: '/' })
|
|
47
|
+
*
|
|
48
|
+
* // Personalized, with automatic fallback to base content
|
|
49
|
+
* const vipHome = await cms.getEntry({ path: '/', variant: 'vip' })
|
|
50
|
+
*
|
|
51
|
+
* // Lists
|
|
52
|
+
* const { entries, count } = await cms.getEntries({
|
|
53
|
+
* templateId: '665f00000000000000000002',
|
|
54
|
+
* sort: '-publishedAt',
|
|
55
|
+
* limit: 10,
|
|
56
|
+
* })
|
|
57
|
+
*
|
|
58
|
+
* // Drafts for live preview (preview-environment token)
|
|
59
|
+
* const draft = await cms.getEntry({ path: '/', draft: true })
|
|
60
|
+
*
|
|
61
|
+
* // Full-text search
|
|
62
|
+
* const hits = await cms.search('astronaut', { limit: 5 })
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function createClient(config: TypeCMSClientConfig): TypeCMSClient;
|
|
66
|
+
|
|
67
|
+
export { SortExpression, TypeCMSClient, TypeCMSClientConfig, TypeCMSError, createClient, toApiSort };
|