@lemonadecms/client 0.1.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -2,7 +2,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- ## 0.1.1 — prepared release
5
+ ## 0.2.0
6
+
7
+ - Use `https://api.lemonadecms.com` for new SDK/CLI defaults. The server's strict
8
+ hostname mode intentionally returns 404 for old main-domain API URLs; upgrade
9
+ installed clients and configured targets before using that deployment.
10
+ Explicit CLI flags and environment precedence remain unchanged.
11
+ Studio/preview message origins stay separate; no redirect, fallback retry or
12
+ runtime dependency. Package publication and server deployment are separate.
13
+
14
+ - Add the optional zero-dependency `@lemonadecms/client/composer` browser bridge:
15
+ versioned website capabilities, stable section/slot locators, selection and
16
+ arrangement commands, cancellable serial rendering and latest-only commits.
17
+ Requires an explicitly opted-in private CMS destination and website renderer.
18
+ - Generate three-level nested component unions and localized text leaves in
19
+ TypeScript, Python and C#, retaining legacy flat schemas and CLI behavior.
20
+
21
+ ## 0.1.1 — published package
6
22
 
7
23
  - Add a native terminal environment picker for online `types generate` using
8
24
  validated configured targets and their credential variables. Explicit settings,
package/README.md CHANGED
@@ -30,7 +30,7 @@ runtime library or framework dependency is required.
30
30
 
31
31
  ## Configure and read content
32
32
 
33
- The SDK connects to `https://lemonadecms.com` automatically. Configure your
33
+ Version 0.2.0 connects to `https://api.lemonadecms.com` automatically. Strict hostname routing returns 404 for API URLs on `https://lemonadecms.com`. Upgrade earlier clients to 0.2.0 and change explicit CLI/configured-target URLs to `https://api.lemonadecms.com`. Explicit CLI settings still take precedence; no redirect or fallback is attempted. Configure your
34
34
  workspace and delivery key; there is no URL or local CMS option. Your website
35
35
  can run locally while reading content from Lemonade production. Installing the
36
36
  package does not create an account, workspace or delivery key.
@@ -63,7 +63,7 @@ if (first) {
63
63
  ```
64
64
 
65
65
  Entry values are under `fields`; metadata includes `id` and `publishedAt`.
66
- Unreleased source builds also expose `contentType` (ID/key/name), `projectId`,
66
+ The client also exposes `contentType` (ID/key/name), `projectId`,
67
67
  `environment` (ID/key) and resolved `locale`. Field projections retain metadata.
68
68
  For older CMS responses, the SDK derives only the request's model key/workspace
69
69
  and explicit locale; unavailable metadata stays absent. Metadata properties are
@@ -104,7 +104,7 @@ console.log(entry?.fields.title ?? 'No matching content');
104
104
 
105
105
  ## Counts, existence and bulk reads
106
106
 
107
- These unreleased methods require the updated SDK and CMS backend. They are
107
+ These methods require the current SDK and CMS backend. They are
108
108
  available in both published and server preview clients:
109
109
 
110
110
  ```js
@@ -240,13 +240,13 @@ placeholders with your workspace UUID; the target labels distinguish environment
240
240
  "version": 1,
241
241
  "targets": {
242
242
  "production": {
243
- "url": "https://lemonadecms.com",
243
+ "url": "https://api.lemonadecms.com",
244
244
  "workspace": "YOUR_WORKSPACE_UUID",
245
245
  "environment": "production",
246
246
  "credentialEnv": "CMS_PUBLIC_KEY"
247
247
  },
248
248
  "dev": {
249
- "url": "https://lemonadecms.com",
249
+ "url": "https://api.lemonadecms.com",
250
250
  "workspace": "YOUR_WORKSPACE_UUID",
251
251
  "environment": "dev",
252
252
  "credentialEnv": "CMS_DEV_PUBLIC_KEY"
@@ -275,7 +275,7 @@ Without configured targets, existing defaults remain unchanged. For automation:
275
275
  npx lemonade types generate --target dev --check
276
276
  ```
277
277
 
278
- This requires the updated source package; published 0.1.0 lacks the picker.
278
+ Version 0.2.0 includes the picker; historical 0.1.0 does not.
279
279
  It uses native Node APIs and retains zero runtime, peer or optional dependencies.
280
280
 
281
281
  ## Private previews
@@ -303,3 +303,16 @@ fetch client.
303
303
 
304
304
  This SDK package is licensed under the [MIT License](LICENSE). Its license does
305
305
  not grant a license to the entire CMS repository or the separate native clients.
306
+
307
+
308
+ ## Visual Composer
309
+
310
+ The optional Visual Composer adds approved nested sections, inspector editing and
311
+ private live updates. A developer connects the website registry and explicitly
312
+ enables unsaved projection on its trusted preview destination. Save draft and
313
+ Publish remain separate. Component text can be translated by stable block ID;
314
+ each language keeps its live tree until explicitly published. Existing form and
315
+ saved-draft previews remain supported. The browser helper is the dependency-free
316
+ `@lemonadecms/client/composer` export.
317
+
318
+ See [setup, editor workflow, Astro reference and protocol](https://github.com/ilara-booqself/lemonadecms/blob/main/docs/visual-composer.md).
@@ -229,7 +229,7 @@ export async function connection(
229
229
  fail(2, 'Selected target is not configured.');
230
230
  const target = name ? targets[name] : {};
231
231
  const selected = {
232
- url: opts.url || target.url || env.LEMONADE_URL || 'https://lemonadecms.com',
232
+ url: opts.url || target.url || env.LEMONADE_URL || 'https://api.lemonadecms.com',
233
233
  workspace:
234
234
  opts.workspace ||
235
235
  target.workspace ||
@@ -73,7 +73,7 @@ function bounded(value) {
73
73
  let nodes = 0;
74
74
  while (stack.length) {
75
75
  const [item, depth] = stack.pop();
76
- if (++nodes > 150000 || depth > 16) invalid();
76
+ if (++nodes > 150000 || depth > 24) invalid();
77
77
  if (
78
78
  typeof item === 'string' &&
79
79
  /(?:pk|sk)_[A-Za-z0-9_-]{43}|lmd_(?:pk|sk|mgmt)_[0-9a-f-]{36}\.[A-Za-z0-9_-]{43}/.test(
@@ -85,7 +85,7 @@ function bounded(value) {
85
85
  for (const child of Object.values(item)) stack.push([child, depth + 1]);
86
86
  }
87
87
  }
88
- function fields(values, keys, component = false) {
88
+ function fields(values, keys, component = false, ancestors = []) {
89
89
  if (
90
90
  !Array.isArray(values) ||
91
91
  values.length > 24 ||
@@ -135,6 +135,7 @@ function fields(values, keys, component = false) {
135
135
  'help_text',
136
136
  'validation',
137
137
  ...(!component ? ['group'] : []),
138
+ ...(!component && field.type === 'blocks' ? ['composer'] : []),
138
139
  ...extra,
139
140
  ...seo,
140
141
  ...(field.type === 'text' && !component ? ['slug'] : []),
@@ -155,8 +156,7 @@ function fields(values, keys, component = false) {
155
156
  !['text', 'long_text', 'markdown', 'rich_text'].includes(field.type)))
156
157
  )
157
158
  invalid();
158
- if (component && (composed.includes(field.type) || field.localized === true))
159
- invalid();
159
+ if (Object.hasOwn(field, 'composer') && typeof field.composer !== 'boolean') invalid();
160
160
  if (Object.hasOwn(field, 'default')) {
161
161
  const value = field.default;
162
162
  if (field.type === 'choices' && value === null) invalid();
@@ -207,12 +207,12 @@ function fields(values, keys, component = false) {
207
207
  for (const definition of field.components) {
208
208
  if (
209
209
  !own(definition, ['key', 'name', 'fields']) ||
210
- !apiKey(definition.key) ||
210
+ !apiKey(definition.key) || ancestors.includes(definition.key) || ancestors.length >= 3 ||
211
211
  !string(definition.name, 80) ||
212
212
  !definition.fields?.length
213
213
  )
214
214
  invalid();
215
- fields(definition.fields, keys, true);
215
+ fields(definition.fields, keys, true, [...ancestors, definition.key]);
216
216
  }
217
217
  }
218
218
  }
@@ -18,13 +18,15 @@ export function generateTypes(schema) {
18
18
  )
19
19
  throw new Error('Invalid schema export.');
20
20
  const keys = new Set(schema.models.map((model) => model.key));
21
- function type(field) {
21
+ let fieldNodes = 0;
22
+ function type(field, ancestors = []) {
23
+ if (++fieldNodes > 150000) throw new Error('Schema exceeds generation budget.');
22
24
  switch (field.type) {
23
25
  case 'component':
24
26
  case 'components':
25
27
  case 'blocks': {
26
28
  if (
27
- !Array.isArray(field.components) ||
29
+ ancestors.length >= 3 || !Array.isArray(field.components) ||
28
30
  field.components.length < 1 ||
29
31
  field.components.length > 12 ||
30
32
  (field.type !== 'blocks' && field.components.length !== 1)
@@ -33,19 +35,18 @@ export function generateTypes(schema) {
33
35
  const variants = field.components
34
36
  .map((component) => {
35
37
  if (
36
- !identifier.test(component.key) ||
38
+ !identifier.test(component.key) || ancestors.includes(component.key) ||
37
39
  !Array.isArray(component.fields) ||
38
40
  component.fields.length < 1 ||
39
41
  component.fields.length > 24 ||
40
42
  component.fields.some(
41
43
  (child) =>
42
- !identifier.test(child.key) ||
43
- ['component', 'components', 'blocks'].includes(child.type),
44
+ !identifier.test(child.key),
44
45
  )
45
46
  )
46
47
  throw new Error('Invalid component fields.');
47
48
  const fields = component.fields
48
- .map((child) => `${JSON.stringify(child.key)}?: ${type(child)} | null`)
49
+ .map((child) => `${JSON.stringify(child.key)}?: ${type(child, [...ancestors, component.key])} | null`)
49
50
  .join('; ');
50
51
  return `{ id: string; type: ${JSON.stringify(component.key)}; fields: { ${fields} } }`;
51
52
  })
@@ -146,7 +147,7 @@ async function main() {
146
147
  throw new Error('Use --help for codegen options.');
147
148
  options[args[i].slice(2)] = args[i + 1];
148
149
  }
149
- const url = new URL(options.url || process.env.CMS_URL || 'https://lemonadecms.com');
150
+ const url = new URL(options.url || process.env.CMS_URL || 'https://api.lemonadecms.com');
150
151
  if (
151
152
  !['http:', 'https:'].includes(url.protocol) ||
152
153
  url.username ||
@@ -0,0 +1,27 @@
1
+ /** Optional browser-only Composer bridge. Website code owns rendering.
2
+ * Enable only on an authenticated preview. No credentials or framework imports.
3
+ */
4
+ export interface ComposerUpdate {
5
+ channel: string;
6
+ entry: string;
7
+ locale: string;
8
+ root: string;
9
+ fields: Record<string, unknown>;
10
+ model_version: number;
11
+ entry_version: number;
12
+ renderer_revision: string;
13
+ }
14
+ export interface VisualComposerOptions {
15
+ studioOrigin: string;
16
+ channel: string;
17
+ entry: string;
18
+ locale: string;
19
+ roots: readonly string[];
20
+ components: readonly string[];
21
+ rendererRevision: string;
22
+ root?: HTMLElement;
23
+ /** Return a synchronous commit function; the bridge calls it only for the
24
+ * latest successful render. Honor signal and keep DOM changes in commit. */
25
+ render: (update: ComposerUpdate, signal: AbortSignal) => Promise<() => void>;
26
+ }
27
+ export declare function enableVisualComposer(options: VisualComposerOptions): () => void;
@@ -0,0 +1,240 @@
1
+ export function enableVisualComposer(options) {
2
+ const keyPattern = /^[a-z][a-z0-9_]{0,47}$/;
3
+ const idPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4
+ const { studioOrigin, channel, entry, locale, roots, components, rendererRevision } = options;
5
+ let origin;
6
+ try {
7
+ origin = new URL(studioOrigin);
8
+ }
9
+ catch {
10
+ return () => { };
11
+ }
12
+ if (origin.origin !== studioOrigin || !['http:', 'https:'].includes(origin.protocol) ||
13
+ window.parent === window || !idPattern.test(channel) || !idPattern.test(entry) ||
14
+ !/^[a-zA-Z0-9-]{2,20}$/.test(locale) || !/^[a-f0-9]{64}$/.test(rendererRevision) ||
15
+ roots.length < 1 || roots.length > 24 || components.length < 1 || components.length > 128 ||
16
+ ![...roots, ...components].every(key => keyPattern.test(key)) || typeof options.render !== 'function') {
17
+ return () => { };
18
+ }
19
+ const root = options.root ?? document.body;
20
+ let active = true;
21
+ let enabled = false;
22
+ let selectedRoot = '';
23
+ let generation = '';
24
+ let latest = 0;
25
+ let busy = false;
26
+ let pending;
27
+ let controller;
28
+ const originals = new Map();
29
+ const send = (type, extra = {}) => window.parent.postMessage({ type, version: 1, channel, entry, locale, root: selectedRoot, ...extra }, studioOrigin);
30
+ const announce = (request) => send('lemonade:composer-ready', {
31
+ roots: [...roots], components: [...components], renderer_revision: rendererRevision,
32
+ ...(request ? { request } : {}),
33
+ });
34
+ const restore = () => {
35
+ for (const [element, attrs] of originals)
36
+ for (const [name, value] of attrs) {
37
+ if (value === null)
38
+ element.removeAttribute(name);
39
+ else
40
+ element.setAttribute(name, value);
41
+ }
42
+ originals.clear();
43
+ };
44
+ const enhance = () => {
45
+ restore();
46
+ if (!enabled)
47
+ return;
48
+ for (const element of Array.from(root.querySelectorAll('[data-lemonade-block]')).slice(0, 100)) {
49
+ const id = element.dataset.lemonadeBlock ?? '';
50
+ if (!idPattern.test(id))
51
+ continue;
52
+ const attrs = new Map();
53
+ for (const name of ['tabindex', 'role', 'aria-label', 'draggable', 'style'])
54
+ attrs.set(name, element.getAttribute(name));
55
+ originals.set(element, attrs);
56
+ if (!element.matches('a[href],button,input,textarea,select'))
57
+ element.setAttribute('role', 'button');
58
+ element.tabIndex = 0;
59
+ element.setAttribute('aria-label', `Edit ${element.dataset.lemonadeName ?? 'section'} in Studio`);
60
+ element.draggable = true;
61
+ element.style.outline = '1px dashed currentColor';
62
+ element.style.outlineOffset = '3px';
63
+ }
64
+ };
65
+ const run = async () => {
66
+ if (!active || !enabled || busy || !pending)
67
+ return;
68
+ const work = pending;
69
+ pending = undefined;
70
+ busy = true;
71
+ const abort = new AbortController();
72
+ controller = abort;
73
+ let timeout;
74
+ try {
75
+ const commit = await Promise.race([
76
+ options.render(work.update, abort.signal),
77
+ new Promise((_, reject) => { timeout = setTimeout(() => { abort.abort(); reject(new Error('Composer render timed out')); }, 10000); }),
78
+ ]);
79
+ if (active && enabled && !abort.signal.aborted && work.sequence === latest && work.generation === generation) {
80
+ if (typeof commit !== 'function')
81
+ throw new Error('Composer renderer must return a commit function');
82
+ commit();
83
+ enhance();
84
+ send('lemonade:composer-rendered', { sequence: work.sequence, generation });
85
+ }
86
+ }
87
+ catch {
88
+ if (active && enabled && work.sequence === latest && work.generation === generation) {
89
+ send('lemonade:composer-error', { sequence: work.sequence, generation });
90
+ }
91
+ }
92
+ finally {
93
+ clearTimeout(timeout);
94
+ if (controller === abort)
95
+ controller = undefined;
96
+ busy = false;
97
+ void run();
98
+ }
99
+ };
100
+ const disable = () => {
101
+ enabled = false;
102
+ pending = undefined;
103
+ controller?.abort();
104
+ restore();
105
+ };
106
+ const receive = (event) => {
107
+ if (!active || event.source !== window.parent || event.origin !== studioOrigin || event.data?.channel !== channel)
108
+ return;
109
+ const data = event.data;
110
+ if (data.type === 'lemonade:preview-connect') {
111
+ if (typeof data.request === 'string' && data.request.length <= 64)
112
+ announce(data.request);
113
+ return;
114
+ }
115
+ if (data.version !== 1 || data.entry !== entry || data.locale !== locale)
116
+ return;
117
+ if (data.type === 'lemonade:composer-disable') {
118
+ disable();
119
+ return;
120
+ }
121
+ if (data.type === 'lemonade:composer-enable') {
122
+ if (!roots.includes(data.root) || typeof data.generation !== 'string' || data.generation.length > 64)
123
+ return;
124
+ if (generation !== data.generation) {
125
+ controller?.abort();
126
+ pending = undefined;
127
+ latest = 0;
128
+ }
129
+ enabled = true;
130
+ generation = data.generation;
131
+ selectedRoot = data.root;
132
+ enhance();
133
+ return;
134
+ }
135
+ if (!enabled || data.generation !== generation || data.root !== selectedRoot)
136
+ return;
137
+ if (data.type === 'lemonade:composer-highlight' && idPattern.test(data.id)) {
138
+ for (const element of originals.keys()) {
139
+ element.style.outlineWidth = element.dataset.lemonadeBlock === data.id ? '3px' : '1px';
140
+ }
141
+ return;
142
+ }
143
+ if (data.type === 'lemonade:composer-drop') {
144
+ if (!Number.isFinite(data.x) || !Number.isFinite(data.y) || data.x < 0 || data.y < 0 ||
145
+ data.x >= window.innerWidth || data.y >= window.innerHeight)
146
+ return;
147
+ const element = document.elementFromPoint(data.x, data.y);
148
+ if (element)
149
+ emitDrop(element, data.payload);
150
+ return;
151
+ }
152
+ if (data.type !== 'lemonade:composer-update' || !Number.isSafeInteger(data.sequence) || data.sequence <= latest ||
153
+ !data.fields || typeof data.fields !== 'object' || Array.isArray(data.fields) ||
154
+ data.renderer_revision !== rendererRevision || !Number.isSafeInteger(data.model_version) || !Number.isSafeInteger(data.entry_version))
155
+ return;
156
+ try {
157
+ if (new TextEncoder().encode(JSON.stringify(data.fields)).length > 65536)
158
+ return;
159
+ }
160
+ catch {
161
+ return;
162
+ }
163
+ latest = data.sequence;
164
+ pending = { update: { channel, entry, locale, root: selectedRoot, fields: data.fields,
165
+ model_version: data.model_version, entry_version: data.entry_version, renderer_revision: rendererRevision }, sequence: latest, generation };
166
+ controller?.abort();
167
+ void run();
168
+ };
169
+ const select = (event) => {
170
+ if (!enabled || !(event.target instanceof Element))
171
+ return;
172
+ if (event instanceof KeyboardEvent && !['Enter', ' '].includes(event.key))
173
+ return;
174
+ if (event instanceof MouseEvent && (event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey))
175
+ return;
176
+ const element = event.target.closest('[data-lemonade-block]');
177
+ if (!element || !originals.has(element))
178
+ return;
179
+ event.preventDefault();
180
+ event.stopImmediatePropagation();
181
+ send('lemonade:composer-select', { id: element.dataset.lemonadeBlock, root: selectedRoot, generation });
182
+ };
183
+ const dragstart = (event) => {
184
+ if (!enabled || !(event.target instanceof Element))
185
+ return;
186
+ const element = event.target.closest('[data-lemonade-block]');
187
+ if (!element || !originals.has(element))
188
+ return;
189
+ event.dataTransfer?.setData('application/x-lemonade-composer', JSON.stringify({ id: element.dataset.lemonadeBlock, channel, root: selectedRoot }));
190
+ };
191
+ const dragover = (event) => {
192
+ if (enabled && event.target instanceof Element && event.target.closest('[data-lemonade-slot]'))
193
+ event.preventDefault();
194
+ };
195
+ const emitDrop = (element, data) => {
196
+ const slot = element.closest('[data-lemonade-slot]');
197
+ const path = slot?.dataset.lemonadeSlot;
198
+ if (!slot || !path || path.length > 512 || !root.contains(slot) || !data ||
199
+ !(idPattern.test(data.id ?? '') || keyPattern.test(data.component ?? '')))
200
+ return;
201
+ if (path !== selectedRoot && !path.startsWith(selectedRoot + '.'))
202
+ return;
203
+ const block = element.closest('[data-lemonade-block]');
204
+ send('lemonade:composer-action', { action: data.id ? 'move' : 'add', id: data.id, component: data.component,
205
+ target: path, before: block?.dataset.lemonadeBlock, root: selectedRoot, generation });
206
+ };
207
+ const drop = (event) => {
208
+ if (!enabled || !(event.target instanceof Element))
209
+ return;
210
+ try {
211
+ const raw = event.dataTransfer?.getData('application/x-lemonade-composer') ?? '';
212
+ if (raw.length > 1024)
213
+ return;
214
+ const data = JSON.parse(raw);
215
+ if (data.channel !== channel || data.root !== selectedRoot)
216
+ return;
217
+ event.preventDefault();
218
+ event.stopPropagation();
219
+ emitDrop(event.target, data);
220
+ }
221
+ catch { /* Foreign drag payloads are not composition commands. */ }
222
+ };
223
+ window.addEventListener('message', receive);
224
+ root.addEventListener('click', select, true);
225
+ root.addEventListener('keydown', select, true);
226
+ root.addEventListener('dragstart', dragstart);
227
+ root.addEventListener('dragover', dragover);
228
+ root.addEventListener('drop', drop);
229
+ announce();
230
+ return () => {
231
+ active = false;
232
+ disable();
233
+ window.removeEventListener('message', receive);
234
+ root.removeEventListener('click', select, true);
235
+ root.removeEventListener('keydown', select, true);
236
+ root.removeEventListener('dragstart', dragstart);
237
+ root.removeEventListener('dragover', dragover);
238
+ root.removeEventListener('drop', drop);
239
+ };
240
+ }
package/dist/transport.js CHANGED
@@ -7,7 +7,7 @@ import { CMSConfigurationError, CMSRequestError, CMSResponseError, CMSTimeoutErr
7
7
  const identifier = /^[a-z][a-z0-9_]{0,47}$/;
8
8
  const cursor = (value) => uuid.test(value) || (value.length <= 8192 && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value));
9
9
  const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
- const serviceURL = "https://lemonadecms.com";
10
+ const serviceURL = "https://api.lemonadecms.com";
11
11
  const localeTag = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?$/;
12
12
  // Narrow unknown JSON to an object before accessing properties. Arrays and null
13
13
  // are not valid objects for the API envelope even though typeof reports object.
@@ -296,7 +296,7 @@ export function createTransport(options, key, mode) {
296
296
  // credentials to production. Check inherited options too: spreads and plain
297
297
  // JavaScript callers must receive the same migration error as TypeScript.
298
298
  if (["url", "baseURL", "baseUrl", "host", "endpoint"].some(option => option in options)) {
299
- throw new CMSConfigurationError("CMS clients connect to https://lemonadecms.com. Remove custom URL configuration.");
299
+ throw new CMSConfigurationError("CMS clients connect to https://api.lemonadecms.com. Remove custom URL configuration.");
300
300
  }
301
301
  const credential = mode === "content" ? "publicKey" : "secretKey";
302
302
  if (Object.keys(options).some(option => !["projectId", credential, "fetch", "timeoutMs"].includes(option)) || "environment" in options) {
package/dist/types.d.ts CHANGED
@@ -4,7 +4,7 @@ export type ModelMap<Models> = {
4
4
  export type UntypedModels = Record<string, Record<string, unknown>>;
5
5
  export type FieldName<Fields> = Extract<keyof Fields, string>;
6
6
  export interface CMSBaseOptions {
7
- /** Requests always use https://lemonadecms.com. Custom CMS hosts are unsupported. */
7
+ /** Requests always use https://api.lemonadecms.com. Custom CMS hosts are unsupported. */
8
8
  url?: never;
9
9
  projectId: string;
10
10
  /** Compatible transport for SSR, testing or polyfills; receives production URLs. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lemonadecms/client",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-independent content delivery and developer tooling for Lemonade CMS",
5
5
  "license": "MIT",
6
6
  "homepage": "https://lemonadecms.com/guides/sdk",
@@ -44,6 +44,10 @@
44
44
  "./preview": {
45
45
  "types": "./dist/preview.d.ts",
46
46
  "default": "./dist/preview.js"
47
+ },
48
+ "./composer": {
49
+ "types": "./dist/composer.d.ts",
50
+ "default": "./dist/composer.js"
47
51
  }
48
52
  },
49
53
  "bin": {