@obelism/content 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -5
- package/dist/bin/content.js +400 -12
- package/dist/bridge.d.mts +19 -3
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +19 -3
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +158 -0
- package/dist/bridge.js +156 -21
- package/dist/bridge.mjs +156 -22
- package/dist/index.d.mts +7 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.mjs +15 -1
- package/dist/site.js +4 -0
- package/dist/site.mjs +4 -0
- package/dist/{types-pa96z6po.d.ts → types-nyvor49r.d.ts} +7 -2
- package/dist/types-nyvor49r.d.ts.map +1 -0
- package/package.json +5 -2
- package/dist/types-pa96z6po.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -55,12 +55,35 @@ bridge.on('update', ({ entry }) => render(entry)) // unsaved edits, as typed
|
|
|
55
55
|
|
|
56
56
|
Mark blocks so editors can click them: `<section {...editable(block)}>`
|
|
57
57
|
(adds `data-cms-uid`). Without an `update` listener the page reloads after
|
|
58
|
-
each save.
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
each save. React sites use `@obelism/content-react`.
|
|
59
|
+
|
|
60
|
+
Without a bundler, the same bridge (prompt box included) is one script tag:
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<script src="https://content.obelism.studio/bridge.js"></script>
|
|
64
|
+
<script>
|
|
65
|
+
ObelismBridge.on('update', ({ entry }) => render(entry))
|
|
66
|
+
</script>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The CMS serves this package's `dist/bridge.global.js` there; npm CDNs serve
|
|
70
|
+
it too (then add `data-url` if your CMS isn't content.obelism.studio).
|
|
71
|
+
`data-prompt="false"` leaves the prompt box out. The tag puts the bridge on
|
|
72
|
+
`window.ObelismBridge` and `getContentBridge` / `mountPromptBox` on
|
|
73
|
+
`window.ObelismContent`; a bundled copy on the same page shares its bridge.
|
|
61
74
|
|
|
62
75
|
## Prompt editing
|
|
63
76
|
|
|
77
|
+
The prompt box is plain DOM in a shadow root, no framework. Start the
|
|
78
|
+
bridge with `prompt: false` to leave it out, and mount it yourself later:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { getContentBridge, mountPromptBox } from '@obelism/content/bridge'
|
|
82
|
+
|
|
83
|
+
const bridge = getContentBridge({ prompt: false })
|
|
84
|
+
const unmount = mountPromptBox(document, bridge, { labels: { open: 'Ask' } })
|
|
85
|
+
```
|
|
86
|
+
|
|
64
87
|
Inside the visual editor, people who may edit get a prompt box at the bottom
|
|
65
88
|
of the page: they type what to change and it shows on the page right away,
|
|
66
89
|
unsaved, with undo. It acts on the selected block, or the whole entry. The
|
|
@@ -77,6 +100,17 @@ const { summary } = await bridge.prompt('Make the heading shorter')
|
|
|
77
100
|
bridge.undo()
|
|
78
101
|
```
|
|
79
102
|
|
|
103
|
+
On a site signed in to edit (`@obelism/content/site`), the box also has **New
|
|
104
|
+
page**: a path (`/pricing`, or under an existing page) and what the page is
|
|
105
|
+
for. The CMS writes the page like the one the editor is on and stores it as a
|
|
106
|
+
draft, and the bridge opens it at `/` + its path. When your routes put CMS
|
|
107
|
+
pages elsewhere, say where:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
getContentBridge({ pagePath: (path) => `/en/${path}` })
|
|
111
|
+
await bridge.createPage('/pricing', 'Our plans, for agencies') // or your own UI
|
|
112
|
+
```
|
|
113
|
+
|
|
80
114
|
The CMS runs prompts on Workers AI. To use your own model, set a prompt
|
|
81
115
|
endpoint under Space settings → Prompt editing and answer it with
|
|
82
116
|
`createPromptHandler` (it verifies the signed request; you call the model):
|
|
@@ -98,12 +132,50 @@ export const POST = createPromptHandler({
|
|
|
98
132
|
- `richTextToHtml(doc, { renderEntry?, renderAsset? })`, `richTextToPlainText(doc)`
|
|
99
133
|
- `imageUrl(asset, { w, h, fit, format, q })`, `imageSrcSet(asset, [480, 960])`
|
|
100
134
|
|
|
135
|
+
## A/B tests and feature flags
|
|
136
|
+
|
|
137
|
+
Any blocks field can hold the built-in improve block (`ImproveBlock`,
|
|
138
|
+
`_type: '_improve'`): one list of blocks per option of an improve test or
|
|
139
|
+
flag. Ask improve's SDK for the visitor's option and render that variant:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { isImproveBlock, pickImproveVariant } from '@obelism/content'
|
|
143
|
+
|
|
144
|
+
// `improve` is improve's SDK (@obelism/improve-sdk), set up for the visitor.
|
|
145
|
+
const blocks = layout.flatMap((block) => {
|
|
146
|
+
if (!isImproveBlock(block)) return [block]
|
|
147
|
+
const option =
|
|
148
|
+
block.kind === 'flag'
|
|
149
|
+
? improve.getFlagValue(block.slug)
|
|
150
|
+
: improve.getTestValue(block.slug)
|
|
151
|
+
return pickImproveVariant(block, option)
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`pickImproveVariant(block, option, fallback?)` falls back to `fallback`'s
|
|
156
|
+
variant, else the first one, when the option is `null` (the test isn't
|
|
157
|
+
active or the visitor isn't in it) or has no variant.
|
|
158
|
+
|
|
101
159
|
## CLI
|
|
102
160
|
|
|
103
|
-
|
|
161
|
+
Start a site's repository on a space, in its root:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
export CONTENT_MANAGEMENT_TOKEN=cms_m_… # a management token, Space settings
|
|
165
|
+
npx @obelism/content init
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
It writes the model file (`content/content-model.json`), an example of what
|
|
169
|
+
the delivery API answers for every content type (`content/examples/`, with
|
|
170
|
+
the A/B test or flag block while your organization has improve on),
|
|
171
|
+
installs `@obelism/content` with your package manager, and adds the token variables to
|
|
172
|
+
`.env.example`. Options: `--dir` (default `content`), `--url`,
|
|
173
|
+
`--skip-install`. Agents get the same steps from the CMS's
|
|
174
|
+
`getProjectSetup` MCP tool.
|
|
175
|
+
|
|
176
|
+
Then keep the model file in Git in step with an environment:
|
|
104
177
|
|
|
105
178
|
```bash
|
|
106
|
-
export CONTENT_MANAGEMENT_TOKEN=cms_m_…
|
|
107
179
|
npx @obelism/content model pull # write content/content-model.json
|
|
108
180
|
npx @obelism/content model plan # what applying would change
|
|
109
181
|
npx @obelism/content model apply # apply it, all or nothing
|
package/dist/bin/content.js
CHANGED
|
@@ -1,9 +1,190 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var node_util = require('node:util');
|
|
3
3
|
var config = require('../config-ci2hxmas.js');
|
|
4
|
+
var node_child_process = require('node:child_process');
|
|
4
5
|
var promises = require('node:fs/promises');
|
|
5
6
|
var node_path = require('node:path');
|
|
6
7
|
|
|
8
|
+
// Blocks inside blocks stop here, so a type that allows itself ends.
|
|
9
|
+
const MAX_DEPTH = 3;
|
|
10
|
+
const EXAMPLE_DATE = '2026-01-01';
|
|
11
|
+
const EXAMPLE_DATE_TIME = '2026-01-01T09:00:00.000Z';
|
|
12
|
+
// An image unless the field only takes other files; then its first exact
|
|
13
|
+
// type, or a PDF.
|
|
14
|
+
const exampleAsset = (mimeTypes)=>{
|
|
15
|
+
const image = !mimeTypes || mimeTypes.some((type)=>type.startsWith('image/'));
|
|
16
|
+
const exact = mimeTypes?.find((type)=>/^[a-z]+\/[^*]+$/.test(type));
|
|
17
|
+
const mimeType = image ? 'image/jpeg' : exact ?? 'application/pdf';
|
|
18
|
+
const filename = image ? 'example.jpg' : `example.${mimeType.split('/')[1]?.split('+')[0] ?? 'bin'}`;
|
|
19
|
+
return {
|
|
20
|
+
id: 'asset_example',
|
|
21
|
+
url: `https://content.obelism.studio/files/asset_example/${filename}`,
|
|
22
|
+
filename,
|
|
23
|
+
mimeType,
|
|
24
|
+
size: 120_000,
|
|
25
|
+
width: image ? 1600 : null,
|
|
26
|
+
height: image ? 900 : null,
|
|
27
|
+
title: null,
|
|
28
|
+
alt: image ? 'An example image' : null
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const exampleRichText = (name)=>({
|
|
32
|
+
type: 'doc',
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: 'paragraph',
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: `Example ${name}, with `
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: 'bold',
|
|
44
|
+
marks: [
|
|
45
|
+
{
|
|
46
|
+
type: 'bold'
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: ' text.'
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
});
|
|
58
|
+
const truncate = (text, maxLength)=>maxLength === undefined ? text : text.slice(0, maxLength);
|
|
59
|
+
const buildExamples = (model, improve)=>{
|
|
60
|
+
const types = model.contentTypes;
|
|
61
|
+
const blockTypes = types.filter(({ kind })=>kind === 'block');
|
|
62
|
+
const entryTypes = types.filter(({ kind })=>kind !== 'block');
|
|
63
|
+
// Uids are unique within one example, like within one entry.
|
|
64
|
+
let next = 0;
|
|
65
|
+
const uid = ()=>`example${++next}`;
|
|
66
|
+
const allowedBlocks = (allowed)=>allowed ? blockTypes.filter(({ apiId })=>allowed.includes(apiId)) : blockTypes;
|
|
67
|
+
const improveBlock = (allowed, depth)=>{
|
|
68
|
+
const source = improve.sources[0] ?? {
|
|
69
|
+
kind: 'test',
|
|
70
|
+
slug: 'example-test',
|
|
71
|
+
options: [
|
|
72
|
+
{
|
|
73
|
+
slug: 'control',
|
|
74
|
+
name: 'Control'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
slug: 'variation',
|
|
78
|
+
name: 'Variation'
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
const first = allowedBlocks(allowed)[0];
|
|
83
|
+
return {
|
|
84
|
+
_uid: uid(),
|
|
85
|
+
_type: '_improve',
|
|
86
|
+
kind: source.kind,
|
|
87
|
+
slug: source.slug,
|
|
88
|
+
variants: Object.fromEntries(source.options.map((option)=>[
|
|
89
|
+
option.slug,
|
|
90
|
+
first && depth < MAX_DEPTH ? [
|
|
91
|
+
block(first, depth + 1)
|
|
92
|
+
] : []
|
|
93
|
+
]))
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const value = (field, depth)=>{
|
|
97
|
+
const name = field.name.toLowerCase();
|
|
98
|
+
switch(field.type){
|
|
99
|
+
case 'text':
|
|
100
|
+
return truncate(`Example ${name}`, field.maxLength);
|
|
101
|
+
case 'textarea':
|
|
102
|
+
return truncate(`Example ${name}. A sentence or two of plain text.`, field.maxLength);
|
|
103
|
+
case 'richtext':
|
|
104
|
+
return exampleRichText(name);
|
|
105
|
+
case 'number':
|
|
106
|
+
return field.min ?? (field.integer ? 1 : 1.5);
|
|
107
|
+
case 'boolean':
|
|
108
|
+
return true;
|
|
109
|
+
case 'datetime':
|
|
110
|
+
return field.withTime ? EXAMPLE_DATE_TIME : EXAMPLE_DATE;
|
|
111
|
+
case 'select':
|
|
112
|
+
{
|
|
113
|
+
const first = field.options[0]?.value ?? null;
|
|
114
|
+
return field.multiple ? first ? [
|
|
115
|
+
first
|
|
116
|
+
] : [] : first;
|
|
117
|
+
}
|
|
118
|
+
case 'asset':
|
|
119
|
+
{
|
|
120
|
+
const asset = exampleAsset(field.mimeTypes);
|
|
121
|
+
return field.multiple ? [
|
|
122
|
+
asset
|
|
123
|
+
] : asset;
|
|
124
|
+
}
|
|
125
|
+
case 'reference':
|
|
126
|
+
{
|
|
127
|
+
const target = field.contentTypes?.[0] ?? entryTypes[0]?.apiId ?? 'page';
|
|
128
|
+
const stub = {
|
|
129
|
+
id: 'entry_example',
|
|
130
|
+
contentType: target
|
|
131
|
+
};
|
|
132
|
+
return field.multiple ? [
|
|
133
|
+
stub
|
|
134
|
+
] : stub;
|
|
135
|
+
}
|
|
136
|
+
case 'blocks':
|
|
137
|
+
{
|
|
138
|
+
if (depth >= MAX_DEPTH) return [];
|
|
139
|
+
const allowed = allowedBlocks(field.allowedBlocks);
|
|
140
|
+
// At the top, one of every block the field allows (and the
|
|
141
|
+
// improve block); deeper, only the first.
|
|
142
|
+
const blocks = depth === 0 ? allowed.map((type)=>block(type, depth + 1)) : allowed.slice(0, 1).map((type)=>block(type, depth + 1));
|
|
143
|
+
return depth === 0 && improve.enabled ? [
|
|
144
|
+
...blocks,
|
|
145
|
+
improveBlock(field.allowedBlocks, depth + 1)
|
|
146
|
+
] : blocks;
|
|
147
|
+
}
|
|
148
|
+
case 'json':
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const fields = (type, depth)=>Object.fromEntries(type.fields.map((field)=>[
|
|
153
|
+
field.apiId,
|
|
154
|
+
value(field, depth)
|
|
155
|
+
]));
|
|
156
|
+
const block = (type, depth)=>({
|
|
157
|
+
_uid: uid(),
|
|
158
|
+
_type: type.apiId,
|
|
159
|
+
...fields(type, depth)
|
|
160
|
+
});
|
|
161
|
+
const examples = {};
|
|
162
|
+
for (const type of types){
|
|
163
|
+
next = 0;
|
|
164
|
+
if (type.kind === 'block') {
|
|
165
|
+
examples[type.apiId] = block(type, 0);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const slug = type.kind === 'page' ? 'example' : null;
|
|
169
|
+
examples[type.apiId] = {
|
|
170
|
+
id: 'entry_example',
|
|
171
|
+
contentType: type.apiId,
|
|
172
|
+
name: `Example ${type.name.toLowerCase()}`,
|
|
173
|
+
slug,
|
|
174
|
+
path: slug,
|
|
175
|
+
publishedAt: EXAMPLE_DATE_TIME,
|
|
176
|
+
firstPublishedAt: EXAMPLE_DATE_TIME,
|
|
177
|
+
updatedAt: EXAMPLE_DATE_TIME,
|
|
178
|
+
fields: fields(type, 0)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (improve.enabled) {
|
|
182
|
+
next = 0;
|
|
183
|
+
examples._improve = improveBlock(undefined, 0);
|
|
184
|
+
}
|
|
185
|
+
return examples;
|
|
186
|
+
};
|
|
187
|
+
|
|
7
188
|
// Turns a model plan from the management API into text for a terminal, or
|
|
8
189
|
// Markdown for a GitHub step summary or pull request comment.
|
|
9
190
|
const ACTION_SIGN = {
|
|
@@ -146,15 +327,205 @@ const runModelCommand = async ({ command, file, url, token, prune, log = console
|
|
|
146
327
|
return !ok || (plan.issues?.length ?? 0) > 0 ? 1 : 0;
|
|
147
328
|
};
|
|
148
329
|
|
|
149
|
-
const
|
|
330
|
+
const PACKAGE_MANAGERS = [
|
|
331
|
+
{
|
|
332
|
+
lockfile: 'pnpm-lock.yaml',
|
|
333
|
+
command: 'pnpm',
|
|
334
|
+
add: [
|
|
335
|
+
'add'
|
|
336
|
+
]
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
lockfile: 'yarn.lock',
|
|
340
|
+
command: 'yarn',
|
|
341
|
+
add: [
|
|
342
|
+
'add'
|
|
343
|
+
]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
lockfile: 'bun.lock',
|
|
347
|
+
command: 'bun',
|
|
348
|
+
add: [
|
|
349
|
+
'add'
|
|
350
|
+
]
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
lockfile: 'bun.lockb',
|
|
354
|
+
command: 'bun',
|
|
355
|
+
add: [
|
|
356
|
+
'add'
|
|
357
|
+
]
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
lockfile: 'package-lock.json',
|
|
361
|
+
command: 'npm',
|
|
362
|
+
add: [
|
|
363
|
+
'install'
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
];
|
|
367
|
+
const ENV_VARIABLES = [
|
|
368
|
+
[
|
|
369
|
+
'CONTENT_DELIVERY_TOKEN',
|
|
370
|
+
'reads published content (cms_d_…)'
|
|
371
|
+
],
|
|
372
|
+
[
|
|
373
|
+
'CONTENT_PREVIEW_TOKEN',
|
|
374
|
+
'also reads drafts, server-side only (cms_p_…)'
|
|
375
|
+
],
|
|
376
|
+
[
|
|
377
|
+
'CONTENT_MANAGEMENT_TOKEN',
|
|
378
|
+
'changes the content model: content model pull|plan|apply (cms_m_…)'
|
|
379
|
+
]
|
|
380
|
+
];
|
|
381
|
+
const exists = (path)=>promises.access(path).then(()=>true, ()=>false);
|
|
382
|
+
const runCommand = (command, args, cwd)=>new Promise((resolve)=>{
|
|
383
|
+
const child = node_child_process.spawn(command, args, {
|
|
384
|
+
cwd,
|
|
385
|
+
stdio: 'inherit',
|
|
386
|
+
shell: process.platform === 'win32'
|
|
387
|
+
});
|
|
388
|
+
child.on('error', ()=>resolve(127));
|
|
389
|
+
child.on('close', (code)=>resolve(code ?? 1));
|
|
390
|
+
});
|
|
391
|
+
const writeJson = async (path, value)=>promises.writeFile(path, `${JSON.stringify(value, null, '\t')}\n`);
|
|
392
|
+
// Whether the organization has improve on, and its tests and flags. A CMS
|
|
393
|
+
// without the endpoint has no improve block.
|
|
394
|
+
const readImprove = async (url, token, fetchImpl)=>{
|
|
395
|
+
const response = await fetchImpl(`${url}${config.MANAGEMENT_PATH}/improve`, {
|
|
396
|
+
headers: {
|
|
397
|
+
Authorization: `Bearer ${token}`
|
|
398
|
+
}
|
|
399
|
+
}).catch((error)=>{
|
|
400
|
+
throw new CliError(`Could not reach ${url}: ${error.message}`);
|
|
401
|
+
});
|
|
402
|
+
if (response.status === 404) return {
|
|
403
|
+
enabled: false,
|
|
404
|
+
sources: []
|
|
405
|
+
};
|
|
406
|
+
const json = await response.json().catch(()=>({}));
|
|
407
|
+
if (!response.ok) {
|
|
408
|
+
throw new CliError(String(json.error ?? `${response.status} from ${url}`));
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
enabled: json.enabled === true,
|
|
412
|
+
sources: json.sources ?? []
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
const install = async (cwd, log, run)=>{
|
|
416
|
+
const manifestPath = node_path.join(cwd, 'package.json');
|
|
417
|
+
let manifest;
|
|
418
|
+
try {
|
|
419
|
+
manifest = JSON.parse(await promises.readFile(manifestPath, 'utf8'));
|
|
420
|
+
} catch {
|
|
421
|
+
log('No package.json here, so nothing was installed: add @obelism/content to your project yourself.');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const dependencies = {
|
|
425
|
+
...manifest.dependencies,
|
|
426
|
+
...manifest.devDependencies
|
|
427
|
+
};
|
|
428
|
+
const wanted = [
|
|
429
|
+
'@obelism/content'
|
|
430
|
+
].filter((name)=>!dependencies[name]);
|
|
431
|
+
if (wanted.length === 0) {
|
|
432
|
+
log('The SDK is already installed.');
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
let manager = PACKAGE_MANAGERS.at(-1);
|
|
436
|
+
for (const candidate of PACKAGE_MANAGERS){
|
|
437
|
+
if (await exists(node_path.join(cwd, candidate.lockfile))) {
|
|
438
|
+
manager = candidate;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const args = [
|
|
443
|
+
...manager.add,
|
|
444
|
+
...wanted
|
|
445
|
+
];
|
|
446
|
+
log(`Installing: ${manager.command} ${args.join(' ')}`);
|
|
447
|
+
const code = await run(manager.command, args, cwd);
|
|
448
|
+
if (code !== 0) {
|
|
449
|
+
throw new CliError(`${manager.command} ${args.join(' ')} failed (exit ${code}). The files are written; install the SDK yourself.`);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
// Appends the variables .env.example doesn't have yet. Never values.
|
|
453
|
+
const writeEnvExample = async (cwd, url)=>{
|
|
454
|
+
const path = node_path.join(cwd, '.env.example');
|
|
455
|
+
const current = await promises.readFile(path, 'utf8').catch(()=>'');
|
|
456
|
+
const has = (name)=>new RegExp(`^\\s*${name}\\s*=`, 'm').test(current);
|
|
457
|
+
const lines = [
|
|
458
|
+
...ENV_VARIABLES.filter(([name])=>!has(name)).flatMap(([name, about])=>[
|
|
459
|
+
`# ${about}`,
|
|
460
|
+
`${name}=`
|
|
461
|
+
]),
|
|
462
|
+
...url !== config.DEFAULT_URL && !has('CONTENT_URL') ? [
|
|
463
|
+
`CONTENT_URL=${url}`
|
|
464
|
+
] : []
|
|
465
|
+
];
|
|
466
|
+
if (lines.length === 0) return false;
|
|
467
|
+
const header = '# Obelism Content (written by `content init`)';
|
|
468
|
+
const separator = current === '' || current.endsWith('\n') ? '' : '\n';
|
|
469
|
+
await promises.writeFile(path, `${current}${separator}${current ? '\n' : ''}${header}\n${lines.join('\n')}\n`);
|
|
470
|
+
return true;
|
|
471
|
+
};
|
|
472
|
+
const runInitCommand = async ({ url, token, dir, install: shouldInstall, cwd = process.cwd(), log = console.log, fetch: fetchImpl = fetch, run = runCommand })=>{
|
|
473
|
+
const base = node_path.join(cwd, dir);
|
|
474
|
+
const modelFile = node_path.join(base, 'content-model.json');
|
|
475
|
+
await runModelCommand({
|
|
476
|
+
command: 'pull',
|
|
477
|
+
// Relative, so the log names it the way the user would.
|
|
478
|
+
file: node_path.relative(process.cwd(), modelFile) || modelFile,
|
|
479
|
+
url,
|
|
480
|
+
token,
|
|
481
|
+
prune: false,
|
|
482
|
+
log,
|
|
483
|
+
fetch: fetchImpl
|
|
484
|
+
});
|
|
485
|
+
const model = JSON.parse(await promises.readFile(modelFile, 'utf8'));
|
|
486
|
+
const improve = await readImprove(url, token, fetchImpl);
|
|
487
|
+
const examples = buildExamples(model, improve);
|
|
488
|
+
const examplesDir = node_path.join(base, 'examples');
|
|
489
|
+
await promises.mkdir(examplesDir, {
|
|
490
|
+
recursive: true
|
|
491
|
+
});
|
|
492
|
+
for (const [apiId, example] of Object.entries(examples)){
|
|
493
|
+
await writeJson(node_path.join(examplesDir, `${apiId}.json`), example);
|
|
494
|
+
}
|
|
495
|
+
const count = Object.keys(examples).length;
|
|
496
|
+
log(`Wrote ${count} ${count === 1 ? 'example' : 'examples'} to ${node_path.join(dir, 'examples')}${improve.enabled ? ', with the A/B test or flag block (_improve.json): improve is on' : ''}.`);
|
|
497
|
+
if (await writeEnvExample(cwd, url)) {
|
|
498
|
+
log('Added the token variables to .env.example.');
|
|
499
|
+
}
|
|
500
|
+
if (shouldInstall) await install(cwd, log, run);
|
|
501
|
+
log([
|
|
502
|
+
'',
|
|
503
|
+
'Next:',
|
|
504
|
+
'- Create a delivery token (and a preview token for drafts) in Space settings, and set CONTENT_DELIVERY_TOKEN and CONTENT_PREVIEW_TOKEN.',
|
|
505
|
+
'- Fetch content with ContentClient from @obelism/content; the examples show what each type looks like.',
|
|
506
|
+
...improve.enabled ? [
|
|
507
|
+
"- Render _improve blocks: ask improve's SDK for the visitor's option (getTestValue / getFlagValue with the block's slug) and render pickImproveVariant(block, option)."
|
|
508
|
+
] : [],
|
|
509
|
+
`- Keep the model in step with \`content model pull|plan|apply\` (${node_path.join(dir, 'content-model.json')}).`
|
|
510
|
+
].join('\n'));
|
|
511
|
+
return 0;
|
|
512
|
+
};
|
|
150
513
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
apply apply the file in one transaction, or change nothing
|
|
514
|
+
const USAGE = `Usage: content init [--dir content] [--url origin] [--skip-install]
|
|
515
|
+
content model <pull|plan|apply> [--file path] [--url origin] [--prune]
|
|
154
516
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
517
|
+
init start a site's repository on the token's environment: the
|
|
518
|
+
model file, an example per content type, the SDK installed
|
|
519
|
+
and the token variables in .env.example
|
|
520
|
+
model pull write the environment's model to the file
|
|
521
|
+
model plan show what applying the file would change, and what stands in the way
|
|
522
|
+
model apply apply the file in one transaction, or change nothing
|
|
523
|
+
|
|
524
|
+
--dir where init writes content-model.json and examples/ (default content)
|
|
525
|
+
--skip-install init leaves dependencies alone
|
|
526
|
+
--file the model file (default content/content-model.json)
|
|
527
|
+
--url the CMS (default $CONTENT_URL, else ${config.DEFAULT_URL})
|
|
528
|
+
--prune plan and apply also delete content types the file leaves out
|
|
158
529
|
|
|
159
530
|
Needs CONTENT_MANAGEMENT_TOKEN: a management token from Space settings.`;
|
|
160
531
|
const main = async ()=>{
|
|
@@ -174,6 +545,14 @@ const main = async ()=>{
|
|
|
174
545
|
type: 'boolean',
|
|
175
546
|
default: false
|
|
176
547
|
},
|
|
548
|
+
dir: {
|
|
549
|
+
type: 'string',
|
|
550
|
+
default: 'content'
|
|
551
|
+
},
|
|
552
|
+
'skip-install': {
|
|
553
|
+
type: 'boolean',
|
|
554
|
+
default: false
|
|
555
|
+
},
|
|
177
556
|
help: {
|
|
178
557
|
type: 'boolean',
|
|
179
558
|
short: 'h',
|
|
@@ -190,17 +569,26 @@ const main = async ()=>{
|
|
|
190
569
|
console.log(USAGE);
|
|
191
570
|
return 0;
|
|
192
571
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
572
|
+
const isInit = group === 'init' && command === undefined;
|
|
573
|
+
const modelCommand = group === 'model' && (command === 'pull' || command === 'plan' || command === 'apply') ? command : null;
|
|
574
|
+
if (!isInit && !modelCommand) throw new CliError(USAGE, 2);
|
|
196
575
|
const token = process.env.CONTENT_MANAGEMENT_TOKEN;
|
|
197
576
|
if (!token) {
|
|
198
577
|
throw new CliError(`CONTENT_MANAGEMENT_TOKEN is not set.\n\n${USAGE}`, 2);
|
|
199
578
|
}
|
|
579
|
+
const url = config.normalizeUrl(values.url ?? process.env.CONTENT_URL ?? config.DEFAULT_URL);
|
|
580
|
+
if (!modelCommand) {
|
|
581
|
+
return runInitCommand({
|
|
582
|
+
url,
|
|
583
|
+
token,
|
|
584
|
+
dir: values.dir,
|
|
585
|
+
install: !values['skip-install']
|
|
586
|
+
});
|
|
587
|
+
}
|
|
200
588
|
return runModelCommand({
|
|
201
|
-
command,
|
|
589
|
+
command: modelCommand,
|
|
202
590
|
file: values.file,
|
|
203
|
-
url
|
|
591
|
+
url,
|
|
204
592
|
token,
|
|
205
593
|
prune: values.prune,
|
|
206
594
|
summaryFile: process.env.GITHUB_STEP_SUMMARY || undefined
|
package/dist/bridge.d.mts
CHANGED
|
@@ -24,6 +24,12 @@ type BridgeState = {
|
|
|
24
24
|
type PromptResult = {
|
|
25
25
|
summary: string;
|
|
26
26
|
};
|
|
27
|
+
type CreatePageResult = {
|
|
28
|
+
summary: string;
|
|
29
|
+
entryId: string;
|
|
30
|
+
path: string;
|
|
31
|
+
url: string;
|
|
32
|
+
};
|
|
27
33
|
type ContentBridge = {
|
|
28
34
|
readonly isEditing: boolean;
|
|
29
35
|
readonly mode: 'editor' | 'site' | null;
|
|
@@ -34,6 +40,7 @@ type ContentBridge = {
|
|
|
34
40
|
prompt(prompt: string, options?: {
|
|
35
41
|
uid?: string | null;
|
|
36
42
|
}): Promise<PromptResult>;
|
|
43
|
+
createPage(path: string, prompt: string): Promise<CreatePageResult>;
|
|
37
44
|
undo(): void;
|
|
38
45
|
save(): Promise<void>;
|
|
39
46
|
publish(): Promise<void>;
|
|
@@ -64,19 +71,28 @@ type PromptLabels = {
|
|
|
64
71
|
signOut: string;
|
|
65
72
|
viewingDrafts: string;
|
|
66
73
|
selectHint: string;
|
|
74
|
+
newPage: string;
|
|
75
|
+
newPageCancel: string;
|
|
76
|
+
newPagePlaceholder: string;
|
|
77
|
+
path: string;
|
|
78
|
+
pathPlaceholder: string;
|
|
79
|
+
creating: string;
|
|
80
|
+
created: string;
|
|
67
81
|
};
|
|
68
82
|
declare const DEFAULT_PROMPT_LABELS: PromptLabels;
|
|
69
83
|
type PromptBoxOptions = {
|
|
70
84
|
labels?: Partial<PromptLabels> | undefined;
|
|
71
85
|
};
|
|
86
|
+
declare const mountPromptBox: (doc: Document, bridge: ContentBridge, options?: PromptBoxOptions) => () => void;
|
|
72
87
|
|
|
73
88
|
type BridgeOptions = {
|
|
74
89
|
url?: string | undefined;
|
|
75
90
|
prompt?: boolean | PromptBoxOptions | undefined;
|
|
91
|
+
pagePath?: ((path: string) => string) | undefined;
|
|
76
92
|
};
|
|
77
93
|
declare const peekContentBridge: () => ContentBridge | null;
|
|
78
|
-
declare const getContentBridge: ({ url, prompt, }?: BridgeOptions) => ContentBridge;
|
|
94
|
+
declare const getContentBridge: ({ url, prompt, pagePath, }?: BridgeOptions) => ContentBridge;
|
|
79
95
|
|
|
80
|
-
export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, peekContentBridge };
|
|
81
|
-
export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, PromptBoxOptions, PromptLabels, PromptResult };
|
|
96
|
+
export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, peekContentBridge };
|
|
97
|
+
export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, CreatePageResult, PromptBoxOptions, PromptLabels, PromptResult };
|
|
82
98
|
//# sourceMappingURL=bridge.d.mts.map
|
package/dist/bridge.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.mts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAIA,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;
|
|
1
|
+
{"version":3,"file":"bridge.d.mts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAIA,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B,8CAA8C,OAAO,CAAC,gBAAgB;AACtE;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;ACrDxC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,qBAAqB,EAAE,YAAY;AACjD,KAAK,gBAAgB;AACrB,aAAa,OAAO,CAAC,YAAY;AACjC;AACA,cAAc,cAAc,QAAQ,QAAQ,UAAU,aAAa,YAAY,gBAAgB;;AC3B/F,KAAK,aAAa;AAClB;AACA,uBAAuB,gBAAgB;AACvC;AACA;AACA,cAAc,iBAAiB,QAAQ,aAAa;AACpD,cAAc,gBAAgB,gCAAgC,aAAa,KAAK,aAAa","names":[]}
|
package/dist/bridge.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ type BridgeState = {
|
|
|
24
24
|
type PromptResult = {
|
|
25
25
|
summary: string;
|
|
26
26
|
};
|
|
27
|
+
type CreatePageResult = {
|
|
28
|
+
summary: string;
|
|
29
|
+
entryId: string;
|
|
30
|
+
path: string;
|
|
31
|
+
url: string;
|
|
32
|
+
};
|
|
27
33
|
type ContentBridge = {
|
|
28
34
|
readonly isEditing: boolean;
|
|
29
35
|
readonly mode: 'editor' | 'site' | null;
|
|
@@ -34,6 +40,7 @@ type ContentBridge = {
|
|
|
34
40
|
prompt(prompt: string, options?: {
|
|
35
41
|
uid?: string | null;
|
|
36
42
|
}): Promise<PromptResult>;
|
|
43
|
+
createPage(path: string, prompt: string): Promise<CreatePageResult>;
|
|
37
44
|
undo(): void;
|
|
38
45
|
save(): Promise<void>;
|
|
39
46
|
publish(): Promise<void>;
|
|
@@ -64,19 +71,28 @@ type PromptLabels = {
|
|
|
64
71
|
signOut: string;
|
|
65
72
|
viewingDrafts: string;
|
|
66
73
|
selectHint: string;
|
|
74
|
+
newPage: string;
|
|
75
|
+
newPageCancel: string;
|
|
76
|
+
newPagePlaceholder: string;
|
|
77
|
+
path: string;
|
|
78
|
+
pathPlaceholder: string;
|
|
79
|
+
creating: string;
|
|
80
|
+
created: string;
|
|
67
81
|
};
|
|
68
82
|
declare const DEFAULT_PROMPT_LABELS: PromptLabels;
|
|
69
83
|
type PromptBoxOptions = {
|
|
70
84
|
labels?: Partial<PromptLabels> | undefined;
|
|
71
85
|
};
|
|
86
|
+
declare const mountPromptBox: (doc: Document, bridge: ContentBridge, options?: PromptBoxOptions) => () => void;
|
|
72
87
|
|
|
73
88
|
type BridgeOptions = {
|
|
74
89
|
url?: string | undefined;
|
|
75
90
|
prompt?: boolean | PromptBoxOptions | undefined;
|
|
91
|
+
pagePath?: ((path: string) => string) | undefined;
|
|
76
92
|
};
|
|
77
93
|
declare const peekContentBridge: () => ContentBridge | null;
|
|
78
|
-
declare const getContentBridge: ({ url, prompt, }?: BridgeOptions) => ContentBridge;
|
|
94
|
+
declare const getContentBridge: ({ url, prompt, pagePath, }?: BridgeOptions) => ContentBridge;
|
|
79
95
|
|
|
80
|
-
export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, peekContentBridge };
|
|
81
|
-
export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, PromptBoxOptions, PromptLabels, PromptResult };
|
|
96
|
+
export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, peekContentBridge };
|
|
97
|
+
export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, CreatePageResult, PromptBoxOptions, PromptLabels, PromptResult };
|
|
82
98
|
//# sourceMappingURL=bridge.d.ts.map
|
package/dist/bridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAIA,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAIA,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B,8CAA8C,OAAO,CAAC,gBAAgB;AACtE;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;ACrDxC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,qBAAqB,EAAE,YAAY;AACjD,KAAK,gBAAgB;AACrB,aAAa,OAAO,CAAC,YAAY;AACjC;AACA,cAAc,cAAc,QAAQ,QAAQ,UAAU,aAAa,YAAY,gBAAgB;;AC3B/F,KAAK,aAAa;AAClB;AACA,uBAAuB,gBAAgB;AACvC;AACA;AACA,cAAc,iBAAiB,QAAQ,aAAa;AACpD,cAAc,gBAAgB,gCAAgC,aAAa,KAAK,aAAa","names":[]}
|