@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 +17 -1
- package/README.md +19 -6
- package/bin/cli/config.mjs +1 -1
- package/bin/cli/schema.mjs +6 -6
- package/bin/generate-types.mjs +8 -7
- package/dist/composer.d.ts +27 -0
- package/dist/composer.js +240 -0
- package/dist/transport.js +2 -2
- package/dist/types.d.ts +1 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
## 0.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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).
|
package/bin/cli/config.mjs
CHANGED
|
@@ -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 ||
|
package/bin/cli/schema.mjs
CHANGED
|
@@ -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 >
|
|
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 (
|
|
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
|
}
|
package/bin/generate-types.mjs
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/composer.js
ADDED
|
@@ -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.
|
|
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": {
|