@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/dist/index.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var DEFAULT_BASE_URL = "https://api.typecms.com";
|
|
7
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
8
|
+
var TypeCMSError = class extends Error {
|
|
9
|
+
constructor(message, status, details) {
|
|
10
|
+
super(message);
|
|
11
|
+
/** HTTP status code */
|
|
12
|
+
__publicField(this, "status");
|
|
13
|
+
/** Validation error details from the API, when present */
|
|
14
|
+
__publicField(this, "details");
|
|
15
|
+
this.name = "TypeCMSError";
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.details = details;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function toApiSort(sort) {
|
|
21
|
+
return sort.startsWith("-") ? `${sort.slice(1)}:-1` : `${sort}:1`;
|
|
22
|
+
}
|
|
23
|
+
function createClient(config) {
|
|
24
|
+
const {
|
|
25
|
+
projectId,
|
|
26
|
+
token,
|
|
27
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
28
|
+
language: defaultLanguage,
|
|
29
|
+
variant: defaultVariant,
|
|
30
|
+
timeout = DEFAULT_TIMEOUT,
|
|
31
|
+
fetch: customFetch
|
|
32
|
+
} = config;
|
|
33
|
+
if (!projectId) throw new TypeCMSError("projectId is required", 0);
|
|
34
|
+
if (!token) throw new TypeCMSError("token is required", 0);
|
|
35
|
+
const origin = baseUrl.replace(/\/$/, "");
|
|
36
|
+
async function request(endpoint, body, params = {}) {
|
|
37
|
+
const url = new URL(`${origin}/api/projects/${projectId}${endpoint}`);
|
|
38
|
+
if (params.q !== void 0) url.searchParams.set("query", params.q);
|
|
39
|
+
if (params.skip !== void 0) url.searchParams.set("skip", String(params.skip));
|
|
40
|
+
if (params.limit !== void 0) url.searchParams.set("limit", String(params.limit));
|
|
41
|
+
if (params.sort) url.searchParams.set("sort", toApiSort(params.sort));
|
|
42
|
+
if (params.format) url.searchParams.set("format", params.format);
|
|
43
|
+
if (params.draft) url.searchParams.set("draft", "true");
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
46
|
+
try {
|
|
47
|
+
const doFetch = customFetch ?? globalThis.fetch;
|
|
48
|
+
const response = await doFetch(url.toString(), {
|
|
49
|
+
method: "POST",
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Bearer ${token}`,
|
|
53
|
+
"Content-Type": "application/json"
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify(body)
|
|
56
|
+
});
|
|
57
|
+
if (response.status === 404) return null;
|
|
58
|
+
let data;
|
|
59
|
+
try {
|
|
60
|
+
data = await response.json();
|
|
61
|
+
} catch {
|
|
62
|
+
data = void 0;
|
|
63
|
+
}
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const error = data ?? {};
|
|
66
|
+
throw new TypeCMSError(
|
|
67
|
+
error.message || `Request failed with status ${response.status}`,
|
|
68
|
+
response.status,
|
|
69
|
+
error.errors
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return data;
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
if (cause instanceof TypeCMSError) throw cause;
|
|
75
|
+
if (cause?.name === "AbortError") {
|
|
76
|
+
throw new TypeCMSError(`Request timed out after ${timeout}ms`, 0);
|
|
77
|
+
}
|
|
78
|
+
throw new TypeCMSError(cause?.message || "Network error", 0);
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timeoutId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function resolveVariant(optionVariant) {
|
|
84
|
+
if (optionVariant === null) return void 0;
|
|
85
|
+
return optionVariant ?? defaultVariant;
|
|
86
|
+
}
|
|
87
|
+
function baseBody(options) {
|
|
88
|
+
const language = options.language ?? defaultLanguage;
|
|
89
|
+
return language ? { language } : {};
|
|
90
|
+
}
|
|
91
|
+
async function fetchEntries(body, params) {
|
|
92
|
+
const raw = await request("/content", body, params);
|
|
93
|
+
return {
|
|
94
|
+
entries: raw?.results ?? [],
|
|
95
|
+
count: raw?.count ?? 0,
|
|
96
|
+
skip: raw?.skip ?? params.skip ?? 0,
|
|
97
|
+
limit: raw?.limit ?? params.limit ?? 0
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
async getEntry(options) {
|
|
102
|
+
const { path, slug, entryId, variantFallback = true, draft, format } = options;
|
|
103
|
+
const selectors = [path, slug, entryId].filter((s) => s !== void 0);
|
|
104
|
+
if (selectors.length !== 1) {
|
|
105
|
+
throw new TypeCMSError("Provide exactly one of: path, slug, entryId", 0);
|
|
106
|
+
}
|
|
107
|
+
const body = {
|
|
108
|
+
...baseBody(options),
|
|
109
|
+
...path !== void 0 && { path },
|
|
110
|
+
...slug !== void 0 && { slug },
|
|
111
|
+
...entryId !== void 0 && { entryId }
|
|
112
|
+
};
|
|
113
|
+
const params = { limit: 1, draft, format };
|
|
114
|
+
const variant = resolveVariant(options.variant);
|
|
115
|
+
if (variant) {
|
|
116
|
+
const withVariant = await fetchEntries({ ...body, variant }, params);
|
|
117
|
+
if (withVariant.entries.length > 0) return withVariant.entries[0];
|
|
118
|
+
if (!variantFallback) return null;
|
|
119
|
+
}
|
|
120
|
+
const result = await fetchEntries(body, params);
|
|
121
|
+
return result.entries[0] ?? null;
|
|
122
|
+
},
|
|
123
|
+
async getEntries(options = {}) {
|
|
124
|
+
const { templateId, filter, limit, skip, sort, variantFallback = true, draft, format } = options;
|
|
125
|
+
const body = {
|
|
126
|
+
...baseBody(options),
|
|
127
|
+
...templateId !== void 0 && { templateId },
|
|
128
|
+
...filter
|
|
129
|
+
};
|
|
130
|
+
const params = { limit, skip, sort, draft, format };
|
|
131
|
+
const variant = resolveVariant(options.variant);
|
|
132
|
+
if (variant) {
|
|
133
|
+
const withVariant = await fetchEntries({ ...body, variant }, params);
|
|
134
|
+
if (withVariant.entries.length > 0 || !variantFallback) return withVariant;
|
|
135
|
+
}
|
|
136
|
+
return fetchEntries(body, params);
|
|
137
|
+
},
|
|
138
|
+
async search(query, options = {}) {
|
|
139
|
+
const { fields, filters, limit, skip, sort } = options;
|
|
140
|
+
const body = {
|
|
141
|
+
...baseBody(options),
|
|
142
|
+
...fields !== void 0 && { fields },
|
|
143
|
+
...filters !== void 0 && { filters }
|
|
144
|
+
};
|
|
145
|
+
const raw = await request("/content/search", body, {
|
|
146
|
+
q: query,
|
|
147
|
+
limit,
|
|
148
|
+
skip,
|
|
149
|
+
sort
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
entries: raw?.entries ?? [],
|
|
153
|
+
count: raw?.count ?? 0,
|
|
154
|
+
skip: raw?.skip ?? skip ?? 0,
|
|
155
|
+
limit: raw?.limit ?? limit ?? 0
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
async query(body, params = {}) {
|
|
159
|
+
return fetchEntries(body, params);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/types/base.ts
|
|
165
|
+
function isComponentType(component, typename) {
|
|
166
|
+
return component.__typename === typename;
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
TypeCMSError,
|
|
170
|
+
createClient,
|
|
171
|
+
isComponentType,
|
|
172
|
+
toApiSort
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/types/base.ts"],"sourcesContent":["/**\n * Type CMS SDK Client\n *\n * Typed client for the Type CMS Content Delivery API\n * (`POST /api/projects/:projectId/content` and `/content/search`).\n *\n * Design notes:\n * - `getEntry` returns `null` (and `getEntries`/`search` return empty pages)\n * when nothing matches — the API's \"empty result\" 404 is not an exception.\n * - Personalization is first-class: set a client-wide `variant` or pass one\n * per call; when the variant has no published row the base content is\n * served automatically (opt out with `variantFallback: false`).\n * - Drafts: `draft: true` reads unpublished content through the same shapes,\n * gated server-side to preview-environment tokens.\n */\n\nimport type {\n TypeCMSClientConfig,\n TypeCMSClient,\n GetEntryOptions,\n GetEntriesOptions,\n ContentEntry,\n EntryList,\n SearchOptions,\n SearchResult,\n SortExpression,\n APIError,\n} from './types'\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DEFAULT_BASE_URL = 'https://api.typecms.com'\nconst DEFAULT_TIMEOUT = 30_000\n\n// =============================================================================\n// Errors\n// =============================================================================\n\n/**\n * Error thrown when an API request fails (auth, validation, server errors).\n * Empty results are NOT errors — the typed reads return `null`/empty pages.\n */\nexport class TypeCMSError extends Error {\n /** HTTP status code */\n public readonly status: number\n /** Validation error details from the API, when present */\n public readonly details?: unknown[]\n\n constructor(message: string, status: number, details?: unknown[]) {\n super(message)\n this.name = 'TypeCMSError'\n this.status = status\n this.details = details\n }\n}\n\n// =============================================================================\n// Internals\n// =============================================================================\n\n/** `'-publishedAt'` → `'publishedAt:-1'`, `'title'` → `'title:1'`. */\nexport function toApiSort(sort: SortExpression): string {\n return sort.startsWith('-') ? `${sort.slice(1)}:-1` : `${sort}:1`\n}\n\ninterface RequestParams {\n skip?: number\n limit?: number\n sort?: SortExpression\n format?: 'json' | 'html'\n draft?: boolean\n /** Search endpoint's query string */\n q?: string\n}\n\n// =============================================================================\n// Client\n// =============================================================================\n\n/**\n * Create a Type CMS content client.\n *\n * @example\n * ```typescript\n * import { createClient } from '@typecms/sdk'\n *\n * const cms = createClient({\n * projectId: '665f00000000000000000001',\n * token: process.env.TYPECMS_TOKEN!,\n * })\n *\n * // By URL path — null when nothing is published there\n * const home = await cms.getEntry<HomePage>({ path: '/' })\n *\n * // Personalized, with automatic fallback to base content\n * const vipHome = await cms.getEntry({ path: '/', variant: 'vip' })\n *\n * // Lists\n * const { entries, count } = await cms.getEntries({\n * templateId: '665f00000000000000000002',\n * sort: '-publishedAt',\n * limit: 10,\n * })\n *\n * // Drafts for live preview (preview-environment token)\n * const draft = await cms.getEntry({ path: '/', draft: true })\n *\n * // Full-text search\n * const hits = await cms.search('astronaut', { limit: 5 })\n * ```\n */\nexport function createClient(config: TypeCMSClientConfig): TypeCMSClient {\n const {\n projectId,\n token,\n baseUrl = DEFAULT_BASE_URL,\n language: defaultLanguage,\n variant: defaultVariant,\n timeout = DEFAULT_TIMEOUT,\n fetch: customFetch,\n } = config\n\n if (!projectId) throw new TypeCMSError('projectId is required', 0)\n if (!token) throw new TypeCMSError('token is required', 0)\n\n const origin = baseUrl.replace(/\\/$/, '')\n\n async function request<T>(\n endpoint: '/content' | '/content/search',\n body: Record<string, unknown>,\n params: RequestParams = {}\n ): Promise<T | null> {\n const url = new URL(`${origin}/api/projects/${projectId}${endpoint}`)\n if (params.q !== undefined) url.searchParams.set('query', params.q)\n if (params.skip !== undefined) url.searchParams.set('skip', String(params.skip))\n if (params.limit !== undefined) url.searchParams.set('limit', String(params.limit))\n if (params.sort) url.searchParams.set('sort', toApiSort(params.sort))\n if (params.format) url.searchParams.set('format', params.format)\n if (params.draft) url.searchParams.set('draft', 'true')\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n const doFetch = customFetch ?? globalThis.fetch\n const response = await doFetch(url.toString(), {\n method: 'POST',\n signal: controller.signal,\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n // The API answers 404 for \"nothing matched\" — that's an empty result,\n // not an exception.\n if (response.status === 404) return null\n\n let data: unknown\n try {\n data = await response.json()\n } catch {\n data = undefined\n }\n\n if (!response.ok) {\n const error = (data ?? {}) as APIError\n throw new TypeCMSError(\n error.message || `Request failed with status ${response.status}`,\n response.status,\n error.errors\n )\n }\n\n return data as T\n } catch (cause) {\n if (cause instanceof TypeCMSError) throw cause\n if ((cause as Error)?.name === 'AbortError') {\n throw new TypeCMSError(`Request timed out after ${timeout}ms`, 0)\n }\n throw new TypeCMSError((cause as Error)?.message || 'Network error', 0)\n } finally {\n clearTimeout(timeoutId)\n }\n }\n\n /** Resolve the effective variant for a call (per-call > client default; null forces base). */\n function resolveVariant(optionVariant: string | null | undefined): string | undefined {\n if (optionVariant === null) return undefined\n return optionVariant ?? defaultVariant\n }\n\n function baseBody(options: { language?: string }): Record<string, unknown> {\n const language = options.language ?? defaultLanguage\n return language ? { language } : {}\n }\n\n async function fetchEntries<T>(\n body: Record<string, unknown>,\n params: RequestParams\n ): Promise<EntryList<T>> {\n const raw = await request<{\n results: ContentEntry<T>[]\n count: number\n skip: number\n limit: number\n }>('/content', body, params)\n\n return {\n entries: raw?.results ?? [],\n count: raw?.count ?? 0,\n skip: raw?.skip ?? params.skip ?? 0,\n limit: raw?.limit ?? params.limit ?? 0,\n }\n }\n\n return {\n async getEntry<T = Record<string, unknown>>(\n options: GetEntryOptions\n ): Promise<ContentEntry<T> | null> {\n const { path, slug, entryId, variantFallback = true, draft, format } = options\n\n const selectors = [path, slug, entryId].filter((s) => s !== undefined)\n if (selectors.length !== 1) {\n throw new TypeCMSError('Provide exactly one of: path, slug, entryId', 0)\n }\n\n const body: Record<string, unknown> = {\n ...baseBody(options),\n ...(path !== undefined && { path }),\n ...(slug !== undefined && { slug }),\n ...(entryId !== undefined && { entryId }),\n }\n const params: RequestParams = { limit: 1, draft, format }\n\n const variant = resolveVariant(options.variant)\n if (variant) {\n const withVariant = await fetchEntries<T>({ ...body, variant }, params)\n if (withVariant.entries.length > 0) return withVariant.entries[0]\n if (!variantFallback) return null\n }\n\n const result = await fetchEntries<T>(body, params)\n return result.entries[0] ?? null\n },\n\n async getEntries<T = Record<string, unknown>>(\n options: GetEntriesOptions = {}\n ): Promise<EntryList<T>> {\n const { templateId, filter, limit, skip, sort, variantFallback = true, draft, format } = options\n\n const body: Record<string, unknown> = {\n ...baseBody(options),\n ...(templateId !== undefined && { templateId }),\n ...filter,\n }\n const params: RequestParams = { limit, skip, sort, draft, format }\n\n const variant = resolveVariant(options.variant)\n if (variant) {\n const withVariant = await fetchEntries<T>({ ...body, variant }, params)\n if (withVariant.entries.length > 0 || !variantFallback) return withVariant\n }\n\n return fetchEntries<T>(body, params)\n },\n\n async search(query: string, options: SearchOptions = {}): Promise<SearchResult> {\n const { fields, filters, limit, skip, sort } = options\n\n const body: Record<string, unknown> = {\n ...baseBody(options),\n ...(fields !== undefined && { fields }),\n ...(filters !== undefined && { filters }),\n }\n\n const raw = await request<SearchResult>('/content/search', body, {\n q: query,\n limit,\n skip,\n sort,\n })\n\n return {\n entries: raw?.entries ?? [],\n count: raw?.count ?? 0,\n skip: raw?.skip ?? skip ?? 0,\n limit: raw?.limit ?? limit ?? 0,\n }\n },\n\n async query<T = Record<string, unknown>>(\n body: Record<string, unknown>,\n params: { skip?: number; limit?: number; sort?: SortExpression; format?: 'json' | 'html'; draft?: boolean } = {}\n ): Promise<EntryList<T>> {\n return fetchEntries<T>(body, params)\n },\n }\n}\n","/**\n * Base types for Type CMS content.\n * These types are used by the SDK and can be imported by generated type files.\n */\n\n// =============================================================================\n// Asset Types\n// =============================================================================\n\n/**\n * Represents a file/media asset in Type CMS.\n */\nexport interface Asset {\n /** Unique identifier */\n _id: string\n /** Public URL of the asset */\n url: string\n /** Alt text for accessibility */\n alt?: string\n /** Display title */\n title?: string\n /** Original filename */\n name?: string\n /** Width in pixels (for images) */\n width?: number\n /** Height in pixels (for images) */\n height?: number\n /** File size in bytes */\n size?: number\n /** MIME type */\n mimeType?: string\n /** File type category */\n type?: AssetType\n}\n\n/**\n * Asset type categories\n */\nexport interface AssetType {\n category?: 'image' | 'video' | 'audio' | 'document' | 'archive' | 'other'\n extension?: string\n}\n\n// =============================================================================\n// Link Types\n// =============================================================================\n\n/**\n * Represents a hyperlink.\n */\nexport interface Link {\n /** Link URL */\n url: string\n /** Link display text */\n label?: string\n /** Open in new tab */\n newTab?: boolean\n}\n\n// =============================================================================\n// Address Types\n// =============================================================================\n\n/**\n * Represents a physical address.\n */\nexport interface Address {\n /** Street address line 1 */\n street?: string\n /** Street address line 2 */\n street2?: string\n /** City */\n city?: string\n /** State/Province */\n state?: string\n /** Postal/ZIP code */\n zip?: string\n /** Country */\n country?: string\n /** Latitude coordinate */\n lat?: number\n /** Longitude coordinate */\n lng?: number\n}\n\n// =============================================================================\n// Reference Types\n// =============================================================================\n\n/**\n * Reference to another entry.\n */\nexport interface EntryReference {\n /** Entry ID */\n _id: string\n /** Entry type slug */\n _type: string\n /** Entry title */\n title?: string\n /** Entry slug */\n slug?: string\n}\n\n/**\n * Reference to a collection item.\n */\nexport interface CollectionReference {\n /** Item ID */\n _id: string\n /** Collection ID */\n collectionId: string\n /** Item title */\n title?: string\n}\n\n// =============================================================================\n// Rich Text Types\n// =============================================================================\n\n/**\n * Slate.js rich text node (v1 rich text).\n */\nexport interface RichTextNode {\n type?: string\n children?: RichTextNode[]\n text?: string\n bold?: boolean\n italic?: boolean\n underline?: boolean\n strikethrough?: boolean\n code?: boolean\n url?: string\n [key: string]: unknown\n}\n\n/**\n * TipTap rich text node (v2 rich text).\n */\nexport interface TipTapNode {\n type: string\n content?: TipTapNode[]\n text?: string\n marks?: TipTapMark[]\n attrs?: Record<string, unknown>\n}\n\n/**\n * TipTap mark (formatting).\n */\nexport interface TipTapMark {\n type: string\n attrs?: Record<string, unknown>\n}\n\n// =============================================================================\n// Base Entry Types\n// =============================================================================\n\n/**\n * Base fields present on all entries.\n */\nexport interface BaseEntry {\n /** Unique identifier */\n _id: string\n /** Content type slug */\n _type: string\n /** Creation timestamp (ISO string) */\n _createdAt: string\n /** Last update timestamp (ISO string) */\n _updatedAt: string\n /** Publication timestamp (ISO string, if published) */\n _publishedAt?: string\n /** Locale code */\n _locale?: string\n /** Version number */\n _version?: number\n}\n\n/**\n * Base fields for canvas/component blocks.\n */\nexport interface BaseComponent {\n /** Component type identifier (discriminator) */\n __typename: string\n}\n\n// =============================================================================\n// Utility Types\n// =============================================================================\n\n/**\n * Extract component type from a union by __typename.\n */\nexport type ExtractComponent<\n TUnion extends BaseComponent,\n TTypename extends TUnion['__typename']\n> = Extract<TUnion, { __typename: TTypename }>\n\n/**\n * Type guard to narrow canvas component by __typename.\n */\nexport function isComponentType<\n TComponent extends BaseComponent,\n TTypename extends TComponent['__typename']\n>(\n component: TComponent,\n typename: TTypename\n): component is Extract<TComponent, { __typename: TTypename }> {\n return component.__typename === typename\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { P as PreviewState } from './types-BNHDdA2G.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Preview Overlay
|
|
5
|
+
*
|
|
6
|
+
* Draws a highlight box over the element whose `data-preview-field`
|
|
7
|
+
* matches the field currently focused in the Type CMS editor.
|
|
8
|
+
* Zero-markup alternative to hand-rolling highlights with useFocusedField().
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface PreviewOverlayOptions {
|
|
12
|
+
/** Outline color of the highlight box */
|
|
13
|
+
color?: string;
|
|
14
|
+
/** z-index of the highlight box */
|
|
15
|
+
zIndex?: number;
|
|
16
|
+
/** Scroll the highlighted element into view when focus changes */
|
|
17
|
+
scrollIntoView?: boolean;
|
|
18
|
+
/** Attribute used to locate field elements */
|
|
19
|
+
attribute?: string;
|
|
20
|
+
}
|
|
21
|
+
interface OverlayClient {
|
|
22
|
+
subscribe(listener: (state: PreviewState) => void): () => void;
|
|
23
|
+
getState(): PreviewState;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Attach a focus-highlight overlay to the document.
|
|
27
|
+
*
|
|
28
|
+
* Subscribes to the preview client's state; whenever `focusedPath` changes,
|
|
29
|
+
* positions a non-interactive highlight box over the matching
|
|
30
|
+
* `[data-preview-field]` element. Returns a detach function.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const preview = createPreviewClient()
|
|
35
|
+
* preview.connect()
|
|
36
|
+
* const detach = attachPreviewOverlay(preview)
|
|
37
|
+
* // ...on teardown
|
|
38
|
+
* detach()
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare function attachPreviewOverlay(client: OverlayClient, options?: PreviewOverlayOptions): () => void;
|
|
42
|
+
|
|
43
|
+
export { type PreviewOverlayOptions as P, attachPreviewOverlay as a };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { P as PreviewListener } from '../client-BW6BWpfF.mjs';
|
|
2
|
+
import { P as PreviewState, a as PreviewConfig } from '../types-BNHDdA2G.mjs';
|
|
3
|
+
|
|
4
|
+
interface PreviewDomBindingOptions {
|
|
5
|
+
/** Root node to search for bound elements (defaults to `document`) */
|
|
6
|
+
root?: ParentNode;
|
|
7
|
+
/** Attribute carrying the field path (defaults to `data-preview-field`) */
|
|
8
|
+
attribute?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Called for non-primitive values (objects, arrays, booleans, null).
|
|
11
|
+
* Return a string to set as the element's textContent, or null/undefined
|
|
12
|
+
* to skip the element. Without this callback, non-primitive values are
|
|
13
|
+
* skipped entirely.
|
|
14
|
+
*/
|
|
15
|
+
renderValue?: (el: Element, path: string, value: unknown) => string | null | undefined;
|
|
16
|
+
}
|
|
17
|
+
interface DomClient {
|
|
18
|
+
subscribe(listener: (state: PreviewState) => void): () => void;
|
|
19
|
+
getState(): PreviewState;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Bind preview values to the DOM.
|
|
23
|
+
*
|
|
24
|
+
* Subscribes to the preview client's state; whenever values change while
|
|
25
|
+
* preview is enabled, every `[data-preview-field]` element whose path
|
|
26
|
+
* resolves to a value (array notation like `items[0].name` is supported)
|
|
27
|
+
* has its textContent updated. Each element's original textContent is
|
|
28
|
+
* remembered the first time it is overwritten and restored on detach.
|
|
29
|
+
*
|
|
30
|
+
* SECURITY: values come from the editor via postMessage and are treated as
|
|
31
|
+
* untrusted content. This binding only ever writes `textContent` — never
|
|
32
|
+
* `innerHTML` — so preview values cannot inject markup or scripts (XSS).
|
|
33
|
+
*
|
|
34
|
+
* @returns A detach function that unsubscribes and restores originals.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const preview = createPreviewClient()
|
|
39
|
+
* preview.connect()
|
|
40
|
+
* const detach = bindPreviewToDom(preview)
|
|
41
|
+
* // ...on teardown
|
|
42
|
+
* detach()
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare function bindPreviewToDom(client: DomClient, options?: PreviewDomBindingOptions): () => void;
|
|
46
|
+
/**
|
|
47
|
+
* One-call live preview for vanilla DOM sites.
|
|
48
|
+
*
|
|
49
|
+
* Creates a preview client, connects it, and binds it to the DOM.
|
|
50
|
+
* Returns the client (for advanced use like `notifyFieldFocus`) and a
|
|
51
|
+
* `destroy` function that unbinds and disconnects.
|
|
52
|
+
*
|
|
53
|
+
* @example Astro — add to a shared layout:
|
|
54
|
+
* ```astro
|
|
55
|
+
* <script>
|
|
56
|
+
* import { initDomPreview } from '@typecms/sdk/preview/dom'
|
|
57
|
+
* initDomPreview()
|
|
58
|
+
* </script>
|
|
59
|
+
* ```
|
|
60
|
+
* Then annotate markup: `<h1 data-preview-field="title">{entry.title}</h1>`
|
|
61
|
+
*/
|
|
62
|
+
declare function initDomPreview(options?: PreviewConfig & PreviewDomBindingOptions): {
|
|
63
|
+
client: {
|
|
64
|
+
connect(): void;
|
|
65
|
+
disconnect(): void;
|
|
66
|
+
getState(): PreviewState;
|
|
67
|
+
isEnabled(): boolean;
|
|
68
|
+
getValue<T = unknown>(path: string): T | undefined;
|
|
69
|
+
subscribe(listener: PreviewListener): () => void;
|
|
70
|
+
notifyNavigation(pathname: string): void;
|
|
71
|
+
notifyFieldFocus(path: string): void;
|
|
72
|
+
};
|
|
73
|
+
destroy(): void;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export { type PreviewDomBindingOptions, bindPreviewToDom, initDomPreview };
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// src/preview/client.ts
|
|
2
|
+
var DEFAULT_ALLOWED_ORIGINS = [
|
|
3
|
+
"https://app.typecms.com",
|
|
4
|
+
"https://staging.typecms.com",
|
|
5
|
+
"http://localhost:3000",
|
|
6
|
+
"http://localhost:3001"
|
|
7
|
+
];
|
|
8
|
+
function createPreviewClient(config = {}) {
|
|
9
|
+
const {
|
|
10
|
+
allowedOrigins = DEFAULT_ALLOWED_ORIGINS,
|
|
11
|
+
onInit,
|
|
12
|
+
onUpdate,
|
|
13
|
+
onNavigate,
|
|
14
|
+
onFocus,
|
|
15
|
+
debug = false
|
|
16
|
+
} = config;
|
|
17
|
+
let state = {
|
|
18
|
+
enabled: false,
|
|
19
|
+
entryId: null,
|
|
20
|
+
projectId: null,
|
|
21
|
+
locale: null,
|
|
22
|
+
typeId: null,
|
|
23
|
+
values: {},
|
|
24
|
+
focusedPath: null
|
|
25
|
+
};
|
|
26
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
27
|
+
let isConnected = false;
|
|
28
|
+
let parentOrigin = null;
|
|
29
|
+
function log(...args) {
|
|
30
|
+
if (debug) {
|
|
31
|
+
console.log("[TypeCMS Preview]", ...args);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function setState(updates) {
|
|
35
|
+
state = { ...state, ...updates };
|
|
36
|
+
listeners.forEach((listener) => listener(state));
|
|
37
|
+
}
|
|
38
|
+
function isAllowedOrigin(origin) {
|
|
39
|
+
return allowedOrigins.some((allowed) => {
|
|
40
|
+
if (allowed === "*") return true;
|
|
41
|
+
const expected = allowed.endsWith("/*") ? allowed.slice(0, -2) : allowed;
|
|
42
|
+
return origin === expected;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function sendMessage(message) {
|
|
46
|
+
if (!parentOrigin) {
|
|
47
|
+
log("Cannot send message: no parent origin");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (typeof window === "undefined" || !window.parent) {
|
|
51
|
+
log("Cannot send message: not in iframe");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
log("Sending message:", message.type);
|
|
55
|
+
window.parent.postMessage(message, parentOrigin);
|
|
56
|
+
}
|
|
57
|
+
function handleMessage(event) {
|
|
58
|
+
if (!isAllowedOrigin(event.origin)) {
|
|
59
|
+
log("Ignored message from untrusted origin:", event.origin);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const message = event.data;
|
|
63
|
+
if (!message?.type?.startsWith("typecms:preview:")) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
log("Received message:", message.type, message);
|
|
67
|
+
parentOrigin = event.origin;
|
|
68
|
+
switch (message.type) {
|
|
69
|
+
case "typecms:preview:init": {
|
|
70
|
+
const initMsg = message;
|
|
71
|
+
setState({
|
|
72
|
+
enabled: true,
|
|
73
|
+
entryId: initMsg.payload.entryId,
|
|
74
|
+
projectId: initMsg.payload.projectId,
|
|
75
|
+
locale: initMsg.payload.locale,
|
|
76
|
+
typeId: initMsg.payload.typeId,
|
|
77
|
+
values: {},
|
|
78
|
+
focusedPath: null
|
|
79
|
+
});
|
|
80
|
+
onInit?.(initMsg.payload);
|
|
81
|
+
const readyMessage = {
|
|
82
|
+
type: "typecms:preview:ready",
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
payload: {
|
|
85
|
+
pathname: typeof window !== "undefined" ? window.location.pathname : "/"
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
sendMessage(readyMessage);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "typecms:preview:update": {
|
|
92
|
+
const updateMsg = message;
|
|
93
|
+
if (updateMsg.payload.values) {
|
|
94
|
+
setState({ values: updateMsg.payload.values });
|
|
95
|
+
} else {
|
|
96
|
+
const newValues = { ...state.values };
|
|
97
|
+
setNestedValue(newValues, updateMsg.payload.path, updateMsg.payload.value);
|
|
98
|
+
setState({ values: newValues });
|
|
99
|
+
}
|
|
100
|
+
onUpdate?.(updateMsg.payload);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "typecms:preview:navigate": {
|
|
104
|
+
onNavigate?.(message.payload.url);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "typecms:preview:focus": {
|
|
108
|
+
setState({ focusedPath: message.payload.path });
|
|
109
|
+
onFocus?.(message.payload.path);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
/**
|
|
116
|
+
* Start listening for preview messages.
|
|
117
|
+
* Call this on mount.
|
|
118
|
+
*/
|
|
119
|
+
connect() {
|
|
120
|
+
if (typeof window === "undefined") {
|
|
121
|
+
log("Cannot connect: not in browser");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (isConnected) {
|
|
125
|
+
log("Already connected");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
log("Connecting...");
|
|
129
|
+
window.addEventListener("message", handleMessage);
|
|
130
|
+
isConnected = true;
|
|
131
|
+
if (window.parent && window.parent !== window) {
|
|
132
|
+
const hello = {
|
|
133
|
+
type: "typecms:preview:ready",
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
payload: { pathname: window.location?.pathname ?? "/" }
|
|
136
|
+
};
|
|
137
|
+
for (const allowed of allowedOrigins) {
|
|
138
|
+
const target = allowed === "*" ? "*" : allowed.endsWith("/*") ? allowed.slice(0, -2) : allowed;
|
|
139
|
+
try {
|
|
140
|
+
window.parent.postMessage(hello, target);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
/**
|
|
147
|
+
* Stop listening for preview messages.
|
|
148
|
+
* Call this on unmount.
|
|
149
|
+
*/
|
|
150
|
+
disconnect() {
|
|
151
|
+
if (typeof window === "undefined") return;
|
|
152
|
+
log("Disconnecting...");
|
|
153
|
+
window.removeEventListener("message", handleMessage);
|
|
154
|
+
isConnected = false;
|
|
155
|
+
parentOrigin = null;
|
|
156
|
+
setState({
|
|
157
|
+
enabled: false,
|
|
158
|
+
entryId: null,
|
|
159
|
+
projectId: null,
|
|
160
|
+
locale: null,
|
|
161
|
+
typeId: null,
|
|
162
|
+
values: {},
|
|
163
|
+
focusedPath: null
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
/**
|
|
167
|
+
* Get current preview state.
|
|
168
|
+
*/
|
|
169
|
+
getState() {
|
|
170
|
+
return state;
|
|
171
|
+
},
|
|
172
|
+
/**
|
|
173
|
+
* Check if preview mode is active.
|
|
174
|
+
*/
|
|
175
|
+
isEnabled() {
|
|
176
|
+
return state.enabled;
|
|
177
|
+
},
|
|
178
|
+
/**
|
|
179
|
+
* Get a preview value by path.
|
|
180
|
+
* Returns undefined if no preview value exists.
|
|
181
|
+
*/
|
|
182
|
+
getValue(path) {
|
|
183
|
+
return getNestedValue(state.values, path);
|
|
184
|
+
},
|
|
185
|
+
/**
|
|
186
|
+
* Subscribe to state changes.
|
|
187
|
+
*/
|
|
188
|
+
subscribe(listener) {
|
|
189
|
+
listeners.add(listener);
|
|
190
|
+
return () => listeners.delete(listener);
|
|
191
|
+
},
|
|
192
|
+
/**
|
|
193
|
+
* Notify the editor that the preview has navigated.
|
|
194
|
+
*/
|
|
195
|
+
notifyNavigation(pathname) {
|
|
196
|
+
const message = {
|
|
197
|
+
type: "typecms:preview:ready",
|
|
198
|
+
timestamp: Date.now(),
|
|
199
|
+
payload: { pathname }
|
|
200
|
+
};
|
|
201
|
+
sendMessage(message);
|
|
202
|
+
},
|
|
203
|
+
/**
|
|
204
|
+
* Notify the editor that a field was clicked in the preview.
|
|
205
|
+
* The editor will scroll to the corresponding field.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* // On click of an element with data-preview-field attribute:
|
|
210
|
+
* const path = el.closest('[data-preview-field]')?.dataset.previewField
|
|
211
|
+
* if (path) preview.notifyFieldFocus(path)
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
notifyFieldFocus(path) {
|
|
215
|
+
const message = {
|
|
216
|
+
type: "typecms:preview:focus",
|
|
217
|
+
timestamp: Date.now(),
|
|
218
|
+
payload: { path }
|
|
219
|
+
};
|
|
220
|
+
sendMessage(message);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
225
|
+
function setNestedValue(obj, path, value) {
|
|
226
|
+
const parts = path.split(".");
|
|
227
|
+
let current = obj;
|
|
228
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
229
|
+
const part = parts[i];
|
|
230
|
+
const arrayMatch2 = part.match(/^(\w+)\[(\d+)\]$/);
|
|
231
|
+
if (arrayMatch2) {
|
|
232
|
+
const [, name, indexStr] = arrayMatch2;
|
|
233
|
+
const index = parseInt(indexStr, 10);
|
|
234
|
+
if (UNSAFE_KEYS.has(name)) continue;
|
|
235
|
+
if (!current[name]) {
|
|
236
|
+
current[name] = [];
|
|
237
|
+
}
|
|
238
|
+
const arr = current[name];
|
|
239
|
+
if (!arr[index]) {
|
|
240
|
+
arr[index] = {};
|
|
241
|
+
}
|
|
242
|
+
current = arr[index];
|
|
243
|
+
} else {
|
|
244
|
+
if (UNSAFE_KEYS.has(part)) continue;
|
|
245
|
+
if (!current[part] || typeof current[part] !== "object") {
|
|
246
|
+
current[part] = {};
|
|
247
|
+
}
|
|
248
|
+
current = current[part];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const lastPart = parts[parts.length - 1];
|
|
252
|
+
const arrayMatch = lastPart.match(/^(\w+)\[(\d+)\]$/);
|
|
253
|
+
if (arrayMatch) {
|
|
254
|
+
const [, name, indexStr] = arrayMatch;
|
|
255
|
+
const index = parseInt(indexStr, 10);
|
|
256
|
+
if (UNSAFE_KEYS.has(name)) return;
|
|
257
|
+
if (!current[name]) {
|
|
258
|
+
current[name] = [];
|
|
259
|
+
}
|
|
260
|
+
current[name][index] = value;
|
|
261
|
+
} else {
|
|
262
|
+
if (UNSAFE_KEYS.has(lastPart)) return;
|
|
263
|
+
current[lastPart] = value;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function getNestedValue(obj, path) {
|
|
267
|
+
const parts = path.split(".");
|
|
268
|
+
let current = obj;
|
|
269
|
+
for (const part of parts) {
|
|
270
|
+
if (current === null || current === void 0) {
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
|
|
274
|
+
if (arrayMatch) {
|
|
275
|
+
const [, name, indexStr] = arrayMatch;
|
|
276
|
+
const index = parseInt(indexStr, 10);
|
|
277
|
+
const arr = current[name];
|
|
278
|
+
if (!arr) return void 0;
|
|
279
|
+
current = arr[index];
|
|
280
|
+
} else {
|
|
281
|
+
current = current[part];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return current;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/preview/dom.ts
|
|
288
|
+
var DEFAULT_ATTRIBUTE = "data-preview-field";
|
|
289
|
+
function bindPreviewToDom(client, options = {}) {
|
|
290
|
+
if (typeof document === "undefined") {
|
|
291
|
+
return () => {
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const attribute = options.attribute ?? DEFAULT_ATTRIBUTE;
|
|
295
|
+
const renderValue = options.renderValue;
|
|
296
|
+
const originals = /* @__PURE__ */ new Map();
|
|
297
|
+
function update(state) {
|
|
298
|
+
if (!state.enabled) return;
|
|
299
|
+
const root = options.root ?? document;
|
|
300
|
+
const elements = root.querySelectorAll(`[${attribute}]`);
|
|
301
|
+
elements.forEach((el) => {
|
|
302
|
+
const path = el.getAttribute(attribute);
|
|
303
|
+
if (!path) return;
|
|
304
|
+
const value = getNestedValue(state.values, path);
|
|
305
|
+
if (value === void 0) return;
|
|
306
|
+
let text = null;
|
|
307
|
+
if (typeof value === "string") {
|
|
308
|
+
text = value;
|
|
309
|
+
} else if (typeof value === "number") {
|
|
310
|
+
text = String(value);
|
|
311
|
+
} else if (renderValue) {
|
|
312
|
+
const result = renderValue(el, path, value);
|
|
313
|
+
if (typeof result === "string") {
|
|
314
|
+
text = result;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (text === null) return;
|
|
318
|
+
if (el.textContent === text) return;
|
|
319
|
+
if (!originals.has(el)) {
|
|
320
|
+
originals.set(el, el.textContent);
|
|
321
|
+
}
|
|
322
|
+
el.textContent = text;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const unsubscribe = client.subscribe(update);
|
|
326
|
+
update(client.getState());
|
|
327
|
+
return () => {
|
|
328
|
+
unsubscribe();
|
|
329
|
+
originals.forEach((text, el) => {
|
|
330
|
+
el.textContent = text;
|
|
331
|
+
});
|
|
332
|
+
originals.clear();
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function initDomPreview(options = {}) {
|
|
336
|
+
const { root, attribute, renderValue, ...config } = options;
|
|
337
|
+
const client = createPreviewClient(config);
|
|
338
|
+
client.connect();
|
|
339
|
+
const unbind = bindPreviewToDom(client, { root, attribute, renderValue });
|
|
340
|
+
return {
|
|
341
|
+
client,
|
|
342
|
+
destroy() {
|
|
343
|
+
unbind();
|
|
344
|
+
client.disconnect();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
export {
|
|
349
|
+
bindPreviewToDom,
|
|
350
|
+
initDomPreview
|
|
351
|
+
};
|
|
352
|
+
//# sourceMappingURL=dom.mjs.map
|