@avocadostudio-ai/site-sdk 0.3.3 → 0.5.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 +277 -23
- package/dist/blocks.d.ts +3 -0
- package/dist/blocks.js +29 -0
- package/dist/coverage.d.ts +4 -0
- package/dist/coverage.js +24 -0
- package/dist/draft-fetch.js +37 -1
- package/dist/draft-fetch.test.js +67 -1
- package/dist/editor.d.ts +2 -14
- package/dist/editor.js +21 -12
- package/dist/live-preview-blocks.js +1 -1
- package/dist/markers.d.ts +78 -0
- package/dist/markers.js +87 -0
- package/dist/next-config.test.js +43 -5
- package/dist/render-blocks.js +1 -1
- package/dist/server/orchestrator.d.ts +1 -1
- package/dist/server/orchestrator.js +1 -1
- package/next-config.d.ts +20 -0
- package/next-config.mjs +86 -2
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -10,6 +10,25 @@ SDK for integrating any Next.js site with the Avocado Studio. Provides the contr
|
|
|
10
10
|
npm install @avocadostudio-ai/site-sdk
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
That is enough to render a site and serve the editor API. **Library mode — the
|
|
14
|
+
orchestrator running inside your Next app — needs two more:**
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @avocadostudio-ai/orchestrator-core better-sqlite3
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`orchestrator-core` is an *optional* peer dependency, which means no package
|
|
21
|
+
manager installs it for you, and `@avocadostudio-ai/site-sdk/server` — where
|
|
22
|
+
`createOrchestrator` lives — is a hard import of it.
|
|
23
|
+
|
|
24
|
+
Nothing else. In particular you do **not** add `@avocadostudio-ai/shared` to
|
|
25
|
+
describe your own blocks: `registerBlock`, `z` and the block-meta types are
|
|
26
|
+
re-exported from [`@avocadostudio-ai/site-sdk/blocks`](#registering-your-own-block-schemas).
|
|
27
|
+
The SDK does depend on `shared`, `blocks`, `preview-adapter` and `zod`, but a
|
|
28
|
+
dependency of a dependency is not a specifier your own source may import —
|
|
29
|
+
under pnpm those live in `.pnpm/`, where the SDK can reach them and your site
|
|
30
|
+
cannot.
|
|
31
|
+
|
|
13
32
|
### 2. Wrap your Next config
|
|
14
33
|
|
|
15
34
|
```ts
|
|
@@ -132,7 +151,33 @@ DRAFT_MODE_SECRET=<random-secret>
|
|
|
132
151
|
PUBLISH_TOKEN=<publish-auth-token>
|
|
133
152
|
```
|
|
134
153
|
|
|
135
|
-
That
|
|
154
|
+
### That is not it — six more steps, and each one fails silently
|
|
155
|
+
|
|
156
|
+
Steps 1–6 give you a site that renders `PageDoc` content and an editor API the
|
|
157
|
+
editor can read. They do not give you an editable site, and every gap below
|
|
158
|
+
presents as something *other* than a missing step:
|
|
159
|
+
|
|
160
|
+
| Still missing | What it looks like when you skip it |
|
|
161
|
+
|---|---|
|
|
162
|
+
| the proxy (Next 16) or middleware (Next 15) | `?__editor=1` never reaches the preview route, so the editor frames your **published** page and edits appear to do nothing |
|
|
163
|
+
| a `/preview-draft` route | nothing renders drafts; the iframe shows published content |
|
|
164
|
+
| `EditorOverlay` on that route | the page frames and renders, and does not respond to a click |
|
|
165
|
+
| `createOrchestrator` (library mode) | there is no orchestrator; the chat has nothing to talk to |
|
|
166
|
+
| `registerBlocks`, if you have your own blocks | everything looks wired up until the first AI edit returns `Unknown block type` |
|
|
167
|
+
| `editableProps` in your components | block selection works; not one *field* is editable |
|
|
168
|
+
|
|
169
|
+
In order:
|
|
170
|
+
|
|
171
|
+
- **[Library mode mounts two handlers, not one](#library-mode-mounts-two-handlers-not-one)** — `createOrchestrator` alongside the route in step 4, and both need the same `registerBlocks` and `blockTypes`.
|
|
172
|
+
- **[The editor frames your page, not the preview route](#the-editor-frames-your-page-not-the-preview-route)** — the proxy, and the CSP that lets the editor frame you at all.
|
|
173
|
+
- **[Registering your own block schemas](#registering-your-own-block-schemas)** — if your blocks are not Avocado's.
|
|
174
|
+
- **[Mounting the overlay](#mounting-the-overlay)** — and turning selection *on* in the editor, which defaults to off and makes a correct integration look inert.
|
|
175
|
+
- **[Marking up your components](#marking-up-your-components)** — the block wrapper first, then the fields. Budget for this one; it is the largest single cost.
|
|
176
|
+
|
|
177
|
+
The first editor API request also prints a warning block for the configuration
|
|
178
|
+
it can check from the inside (draft secret, editor origin, orchestrator URL).
|
|
179
|
+
It cannot see the last three rows of that table — those live in your components,
|
|
180
|
+
and the [coverage gate](#budget-for-this-part) is what keeps them honest.
|
|
136
181
|
|
|
137
182
|
## What You Implement
|
|
138
183
|
|
|
@@ -145,7 +190,7 @@ That's it. Your site now works with the AI editor.
|
|
|
145
190
|
|
|
146
191
|
## Exports
|
|
147
192
|
|
|
148
|
-
All
|
|
193
|
+
All twenty, because the six this table used to list were not the six an
|
|
149
194
|
integration needs — `createOrchestrator` is the whole of library mode and was in
|
|
150
195
|
none of them.
|
|
151
196
|
|
|
@@ -163,15 +208,20 @@ none of them.
|
|
|
163
208
|
| `.../draft` | `resolveEditorContext`, `fetchEditorPage`, `fetchEditorSlugs`, `fetchEditorSiteConfig` |
|
|
164
209
|
| `.../draft/core` | The draft context without Next's cookie APIs |
|
|
165
210
|
| `.../editor` | `EditorOverlay`, `LivePreviewProvider`, `buildEditorQuerySuffix` |
|
|
211
|
+
| `.../markers` | `editableProps`, `getPreviewWrapperProps` — the preview attributes, with no React behind them. **Import them from here**, not from `.../editor`, in any component a public page renders |
|
|
212
|
+
| `.../blocks` | `registerBlock`, `z`, and the block-meta types (`FieldKind`, `FieldMeta`, `ListFieldMeta`, `BlockMeta`), so describing your own blocks needs no second package |
|
|
213
|
+
| `.../coverage` | `editableCoverage` (the preview markers) and `panelCoverage` (the property panel) — gate both in CI |
|
|
166
214
|
| `.../editor-manifest` | `buildBlockManifest` — the built-in blocks as a manifest |
|
|
167
215
|
| `.../publish` | `diffPage`, `groupPatches`, `describeUnsupported` — the field-level publish walk |
|
|
168
216
|
| `.../publish-handlers/json-file` | A ready-made `onPublish` that writes a JSON file |
|
|
169
217
|
| `.../navigation` | `buildNavItems`, `buildSiteHeaderBlock` |
|
|
170
218
|
| `.../seo` | `buildPageMetadata` and the title/description derivation |
|
|
171
219
|
|
|
172
|
-
`useLivePreviewBlocks` is
|
|
173
|
-
|
|
174
|
-
|
|
220
|
+
`useLivePreviewBlocks` is in `.../editor` too, beside `LivePreviewProvider`. It
|
|
221
|
+
used to be the one import in our own walkthrough that a site following our own
|
|
222
|
+
install line could not resolve: the provider was re-exported and the hook was
|
|
223
|
+
not, so the example said to import it from `@avocadostudio-ai/preview-adapter`,
|
|
224
|
+
which is a dependency of *this* package and not of yours.
|
|
175
225
|
|
|
176
226
|
It is worth being precise about what it is for, because an earlier version of
|
|
177
227
|
this paragraph was not. Rendering a live draft through **your own** components
|
|
@@ -452,6 +502,36 @@ A workspace link to `@avocadostudio-ai/orchestrator-core` is **not** enough. A
|
|
|
452
502
|
linked package's own dependencies are never materialised in the host's tree, and
|
|
453
503
|
a native module has to be resolvable from there.
|
|
454
504
|
|
|
505
|
+
|
|
506
|
+
### `withAvocado` writes the framing headers for you
|
|
507
|
+
|
|
508
|
+
As of this version you do not hand-write the CSP pair. `withAvocado` appends
|
|
509
|
+
both rules, derived from the same `EDITOR_CORS_ORIGINS` /
|
|
510
|
+
`NEXT_PUBLIC_EDITOR_ORIGIN` allowlist the editor API uses — so the two halves of
|
|
511
|
+
the contract cannot disagree:
|
|
512
|
+
|
|
513
|
+
```
|
|
514
|
+
frame-ancestors 'self' http://localhost:4100 http://127.0.0.1:4100;
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Two things it gets right that hand-written versions did not. It names **both
|
|
518
|
+
loopback spellings**, because the CLI binds `127.0.0.1` and prints that while
|
|
519
|
+
every doc says `localhost`, and `frame-ancestors` compares origins as strings.
|
|
520
|
+
And it puts `missing: [{ type: "query", key: "__editor" }]` on the public rule —
|
|
521
|
+
without that, both rules match an editor request, the browser receives two CSP
|
|
522
|
+
headers and enforces their *intersection*, putting `frame-ancestors 'self'` back
|
|
523
|
+
and blocking the frame it just allowed.
|
|
524
|
+
|
|
525
|
+
Your own `headers()` entries come first and win, since Next uses the first match
|
|
526
|
+
per header. Opt out entirely with `withAvocado(config, { framing: false })`.
|
|
527
|
+
|
|
528
|
+
Serving the editor anywhere but `:4100` — which anyone already running the
|
|
529
|
+
standalone stack has to — means setting `EDITOR_CORS_ORIGINS`, and that one
|
|
530
|
+
variable now covers both halves. Left unset, the API answers **200 with no
|
|
531
|
+
`access-control-allow-origin`** (the Sites page reports your site as *offline*)
|
|
532
|
+
and the browser refuses the frame (**a blank rectangle**). Neither failure names
|
|
533
|
+
a port, and neither says CORS or CSP.
|
|
534
|
+
|
|
455
535
|
## The editor frames your page, not the preview route
|
|
456
536
|
|
|
457
537
|
The iframe's `src` is your site's own URL with a query parameter added:
|
|
@@ -626,7 +706,7 @@ until the first AI edit:
|
|
|
626
706
|
|
|
627
707
|
```ts
|
|
628
708
|
// lib/register-blocks.ts
|
|
629
|
-
import { registerBlock, z } from "@avocadostudio-ai/
|
|
709
|
+
import { registerBlock, z } from "@avocadostudio-ai/site-sdk/blocks"
|
|
630
710
|
|
|
631
711
|
export function registerMyBlocks() {
|
|
632
712
|
registerBlock("PricingTable", {
|
|
@@ -663,7 +743,7 @@ last in the file. Next's bundler does not reliably preserve module order across
|
|
|
663
743
|
the RSC, SSR and route-handler layers, so the built-in schemas sometimes
|
|
664
744
|
re-register on top of yours. The hook exists to replace that trick.
|
|
665
745
|
|
|
666
|
-
### Import `z` from
|
|
746
|
+
### Import `z` from `site-sdk/blocks`, not from `zod`
|
|
667
747
|
|
|
668
748
|
`registerBlock` takes a `ZodObject`, and a Zod object is assignable only to one
|
|
669
749
|
built by the *same copy* of the library. Your own `import { z } from "zod"`
|
|
@@ -671,7 +751,60 @@ resolves to whatever your tree hoisted — on any site that also uses Sanity tha
|
|
|
671
751
|
is zod 3 — and the mismatch reports as a structural type error listing methods
|
|
672
752
|
you have never called (`loose`, `safeExtend`, `exactPartial`, `def`, "and 21
|
|
673
753
|
more"), with nothing anywhere saying there are two copies of zod. Importing `z`
|
|
674
|
-
from `
|
|
754
|
+
from `site-sdk/blocks` gives you ours, and you need no direct `zod` dependency
|
|
755
|
+
at all.
|
|
756
|
+
|
|
757
|
+
`site-sdk/blocks` re-exports these from `@avocadostudio-ai/shared`, which the
|
|
758
|
+
SDK already depends on. Import them from there and the rule above stops being
|
|
759
|
+
something you have to remember: there is only one copy to reach.
|
|
760
|
+
|
|
761
|
+
### What `kind` may be
|
|
762
|
+
|
|
763
|
+
`meta.fields[…].kind` is a closed list, and it is not the list of HTML element
|
|
764
|
+
names — `"select"` and `"textarea"` are both wrong:
|
|
765
|
+
|
|
766
|
+
| What you want | What you write |
|
|
767
|
+
|---|---|
|
|
768
|
+
| a single-line string | `{ kind: "text" }` |
|
|
769
|
+
| a multi-line box | `{ kind: "text", multiline: true }` — `multiline` is a flag, not a kind |
|
|
770
|
+
| markdown / rich text | `{ kind: "richtext" }` |
|
|
771
|
+
| a closed list of values | `{ kind: "enum", options: ["a", "b"] }` — a `string[]`, not `{ value, label }[]` |
|
|
772
|
+
| an image | `{ kind: "image" }`, with its alt text as `{ kind: "imageAlt" }` |
|
|
773
|
+
| a link | `{ kind: "url" }` for a bare href, `{ kind: "link" }` for `{ href, label }` |
|
|
774
|
+
| an uploaded file | `{ kind: "file" }` |
|
|
775
|
+
| the rest | `{ kind: "number" }`, `{ kind: "boolean" }`, `{ kind: "color" }`, `{ kind: "headingLevel" }` |
|
|
776
|
+
|
|
777
|
+
In TypeScript a wrong `kind` is a compile error naming the whole union, which is
|
|
778
|
+
how you would find this out anyway. In JavaScript it is silent: the panel falls
|
|
779
|
+
back to a plain text input and the preview loses whatever affordance the right
|
|
780
|
+
kind would have given the field.
|
|
781
|
+
|
|
782
|
+
### Lists whose rows are not all the same shape
|
|
783
|
+
|
|
784
|
+
`listFields` has a second form for that, and it is easy to miss because the
|
|
785
|
+
common case does not use it. Give the list a `discriminator` naming the row
|
|
786
|
+
property that says what the row is, and `itemFieldsByType` mapping each value of
|
|
787
|
+
it to that variant's fields; `itemFields` stays as the fallback for a row whose
|
|
788
|
+
type is not listed:
|
|
789
|
+
|
|
790
|
+
```ts
|
|
791
|
+
listFields: {
|
|
792
|
+
content: {
|
|
793
|
+
label: "Content",
|
|
794
|
+
discriminator: "type",
|
|
795
|
+
itemFields: { type: { kind: "text" }, text: { kind: "text", multiline: true } },
|
|
796
|
+
itemFieldsByType: {
|
|
797
|
+
heading: { text: { kind: "text", label: "Heading" } },
|
|
798
|
+
paragraph: { text: { kind: "text", multiline: true } },
|
|
799
|
+
image: { src: { kind: "image" }, alt: { kind: "imageAlt" } },
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
Without it, a mixed list is described by whichever single shape you picked, and
|
|
806
|
+
every row of every other shape renders that shape's fields against props it does
|
|
807
|
+
not have.
|
|
675
808
|
|
|
676
809
|
### `schema` and `meta` are two halves that must agree
|
|
677
810
|
|
|
@@ -801,38 +934,159 @@ second false is the default.
|
|
|
801
934
|
Once the picker is on, blocks are selectable. Individual *fields* are not,
|
|
802
935
|
until:
|
|
803
936
|
|
|
804
|
-
##
|
|
937
|
+
## Marking up your components
|
|
938
|
+
|
|
939
|
+
Two attributes carry the whole preview contract, and there is a helper for each.
|
|
940
|
+
Import both from `@avocadostudio-ai/site-sdk/markers` — *not* from
|
|
941
|
+
`.../editor`, which also exports `EditorOverlay` and will pull the editor into
|
|
942
|
+
every public page that renders the same component ([see below](#import-the-markers-from-markers-not-from-editor)).
|
|
943
|
+
|
|
944
|
+
### First, the block wrapper — without it nothing is selectable
|
|
945
|
+
|
|
946
|
+
Selection is built entirely on `data-block-id`: a click resolves through
|
|
947
|
+
`closest("[data-block-id]")`, and *no match* is read as "clicked outside any
|
|
948
|
+
block", which **clears** the selection. So a preview that marks every field and
|
|
949
|
+
no block frames, renders, scrolls and validates correctly, and deselects on
|
|
950
|
+
every click — which looks exactly like the picker being switched off.
|
|
951
|
+
|
|
952
|
+
Do it once, wherever you dispatch on block type, rather than in each block:
|
|
953
|
+
|
|
954
|
+
```tsx
|
|
955
|
+
import { getPreviewWrapperProps } from "@avocadostudio-ai/site-sdk/markers"
|
|
956
|
+
|
|
957
|
+
export function BlockRenderer({ block, editorMode = false }) {
|
|
958
|
+
const rendered = renderBlock(block)
|
|
959
|
+
if (!editorMode || rendered === null) return rendered
|
|
960
|
+
return <div {...getPreviewWrapperProps(true, block.id, block.type)}>{rendered}</div>
|
|
961
|
+
}
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
It returns `{}` when `editorMode` is false, so the published page renders
|
|
965
|
+
precisely the markup it rendered before.
|
|
966
|
+
|
|
967
|
+
### Then the fields
|
|
805
968
|
|
|
806
969
|
The manifest tells the editor which blocks exist and what props they take. It
|
|
807
970
|
does not tell it **where on the page a prop is rendered**, and nothing can derive
|
|
808
|
-
that — so a
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
Put `data-editable-target` on the DOM node that renders each prop:
|
|
971
|
+
that — so a block with no field markers gets selection, the badge, move and
|
|
972
|
+
delete, and not one editable field.
|
|
812
973
|
|
|
813
974
|
```tsx
|
|
814
|
-
|
|
815
|
-
|
|
975
|
+
import { editableProps } from "@avocadostudio-ai/site-sdk/markers"
|
|
976
|
+
|
|
977
|
+
<h1 {...editableProps("heading")}>{heading}</h1>
|
|
978
|
+
<p {...editableProps("subheading", { kind: "text" })}>{subheading}</p>
|
|
816
979
|
|
|
817
980
|
{cards.map((card, i) => (
|
|
818
981
|
<article key={card._key}>
|
|
819
|
-
<h3
|
|
820
|
-
<img
|
|
982
|
+
<h3 {...editableProps(`cards[${i}].title`)}>{card.title}</h3>
|
|
983
|
+
{/* on the wrapper, never on the <img> — see below */}
|
|
984
|
+
<div {...editableProps(`cards[${i}].imageUrl`, { kind: "image" })}>
|
|
985
|
+
<img src={card.imageUrl} alt={card.alt} />
|
|
986
|
+
</div>
|
|
821
987
|
</article>
|
|
822
988
|
))}
|
|
823
989
|
```
|
|
824
990
|
|
|
825
991
|
The path grammar is the same one operations use: `heading` for a scalar,
|
|
826
992
|
`cards[0].title` for a list item's field, `cards[0].imageUrl` for its image.
|
|
993
|
+
What the overlay reads here is what it sends back as the field to patch.
|
|
994
|
+
|
|
995
|
+
There is no `editorMode` argument on `editableProps`, on purpose: these are
|
|
996
|
+
inert data attributes that do nothing unless the overlay is mounted, and
|
|
997
|
+
threading a flag down to every field is the step that does not get done.
|
|
998
|
+
|
|
999
|
+
### An image field goes on the wrapper, not on the `<img>`
|
|
1000
|
+
|
|
1001
|
+
The overlay *appends* its Change button into the marked element, and nothing can
|
|
1002
|
+
be appended into an `<img>` — it is a void element. Marking the image itself
|
|
1003
|
+
gives you a field that is listed in the property panel, highlights on hover, and
|
|
1004
|
+
has no button, with no error on either side.
|
|
1005
|
+
|
|
1006
|
+
### `kind` is how your site says what a field is, in its own words
|
|
1007
|
+
|
|
1008
|
+
Without it the overlay guesses an image from the prop name, against Avocado's
|
|
1009
|
+
own naming (`imageUrl`, `*.src`). A site whose field is `photoUrl` or `heroSrc`
|
|
1010
|
+
gets a picker in the property panel and no button in the preview. Pass the same
|
|
1011
|
+
word the block manifest uses — the [`kind` table](#what-kind-may-be) — and the
|
|
1012
|
+
guess never runs.
|
|
1013
|
+
|
|
1014
|
+
### Import the markers from `/markers`, not from `/editor`
|
|
1015
|
+
|
|
1016
|
+
`@avocadostudio-ai/site-sdk/editor` re-exports both helpers, so older code keeps
|
|
1017
|
+
working. But that entry also exports `EditorOverlay` and the live-preview
|
|
1018
|
+
provider, and most sites mark up components that their **public** pages render
|
|
1019
|
+
too. A `'use client'` component importing the two-line attribute helper from
|
|
1020
|
+
there drags the whole editor into the public bundle — measured on a real
|
|
1021
|
+
integration at **+66 kB First Load JS on every page**, for byte-identical
|
|
1022
|
+
markup. `/markers` has no React in it at all.
|
|
1023
|
+
|
|
1024
|
+
### Budget for this part
|
|
827
1025
|
|
|
828
1026
|
This is the one part of an integration that **cannot** live in an integration
|
|
829
|
-
layer
|
|
830
|
-
|
|
831
|
-
|
|
1027
|
+
layer: it goes inside your own components, one call per prop you want editable.
|
|
1028
|
+
It is usually the largest single cost of adopting the editor, and there is no
|
|
1029
|
+
way to add it from the outside. Two real integrations landed at 36 and 51
|
|
1030
|
+
emission sites.
|
|
1031
|
+
|
|
1032
|
+
Gate it in CI rather than trusting it to stay done. `editableCoverage` from
|
|
1033
|
+
`@avocadostudio-ai/site-sdk/coverage` compares the manifest's fields against the
|
|
1034
|
+
markers a rendered page actually carries, so a field that loses its marker in a
|
|
1035
|
+
refactor fails a build instead of silently losing inline editing:
|
|
1036
|
+
|
|
1037
|
+
```js
|
|
1038
|
+
import { extractMarkedBlocks, editableCoverage, formatEditableCoverage }
|
|
1039
|
+
from "@avocadostudio-ai/site-sdk/coverage"
|
|
1040
|
+
|
|
1041
|
+
const manifest = await (await fetch(`${BASE}/api/editor/blocks`)).json()
|
|
1042
|
+
const html = await (await fetch(`${BASE}/?__editor=1`)).text()
|
|
1043
|
+
const report = editableCoverage(manifest, extractMarkedBlocks(html), { props })
|
|
1044
|
+
console.log(formatEditableCoverage(report))
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
Pass `props` — the blocks as rendered — or the report cannot tell a field that
|
|
1048
|
+
lost its marker from one that was empty on that page. Expect a ceiling below
|
|
1049
|
+
100%: a field that draws nothing (`sectionId`, an input's `placeholder`, a
|
|
1050
|
+
`<video>` poster) has no element to mark and stays panel-only.
|
|
1051
|
+
|
|
1052
|
+
Declare those panel-only fields as `{ kind: "text", panelOnly: true }` and the
|
|
1053
|
+
ceiling goes away — the denominator becomes the set of fields that *could* carry
|
|
1054
|
+
a marker, so 100% is reachable and anything less is actionable.
|
|
832
1055
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
`
|
|
1056
|
+
### And check the panel, which is the other half
|
|
1057
|
+
|
|
1058
|
+
`editableCoverage` asks whether the preview offers each field. It says nothing
|
|
1059
|
+
about whether the property panel is *intelligible*, and that fails in ways a
|
|
1060
|
+
build, a type check and a manifest validation all pass through:
|
|
1061
|
+
|
|
1062
|
+
```js
|
|
1063
|
+
import { panelCoverage, formatPanelCoverage } from "@avocadostudio-ai/site-sdk/coverage"
|
|
1064
|
+
import { getAllBlockMeta } from "@avocadostudio-ai/site-sdk/blocks"
|
|
1065
|
+
|
|
1066
|
+
const report = panelCoverage(manifest, pages, { builtinTypes: getAllBlockMeta() })
|
|
1067
|
+
console.log(formatPanelCoverage(report))
|
|
1068
|
+
if (report.findings.length > 0) process.exit(1)
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
It compares what the panel will render against what your content actually holds,
|
|
1072
|
+
so it needs no browser and no screenshots:
|
|
1073
|
+
|
|
1074
|
+
| finding | what it means |
|
|
1075
|
+
|---|---|
|
|
1076
|
+
| `colliding_type` | your block type name also exists in the editor's built-ins with a different shape. **Read this one first** — it usually explains several of the others |
|
|
1077
|
+
| `unlabelled_row` | the panel titles this list row `Item 4`; nobody can tell it from its neighbour |
|
|
1078
|
+
| `unmatched_branch` | a row's discriminant value has no `itemFieldsByType` entry, so it is edited against the union of every branch |
|
|
1079
|
+
| `incomplete_polymorphism` | a `discriminator` with no branch map, or the reverse — nothing narrows |
|
|
1080
|
+
| `orphan_prop` | your content holds it and nothing describes it, so the panel cannot show or edit it |
|
|
1081
|
+
| `filename_row_label` | an image row titled `IMG_20250904.webp` while a populated alt sits beside it |
|
|
1082
|
+
| `phantom_field` | declared for rows that never have it |
|
|
1083
|
+
|
|
1084
|
+
Pass `builtinTypes` only if you want the collision findings; without it they are
|
|
1085
|
+
simply not reported, because an absent input is not evidence.
|
|
1086
|
+
|
|
1087
|
+
An agent integrating a site can call the same check as
|
|
1088
|
+
`avocado-check-editing-surface` over MCP, and should, before reporting an
|
|
1089
|
+
integration as done.
|
|
836
1090
|
|
|
837
1091
|
## Environment Variables
|
|
838
1092
|
|
package/dist/blocks.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { z } from "@avocadostudio-ai/shared";
|
|
2
|
+
export { registerBlock, declareBlockCatalogue, getBlockMeta, getAllBlockMeta, getChromeTypes, isChrome, IMAGE_PLACEHOLDER, isImagePlaceholder, blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "@avocadostudio-ai/shared";
|
|
3
|
+
export type { FieldKind, FieldMeta, ListFieldMeta, ImageSpec, BlockMeta, BlockType, BlockInstance, BlockRegistration, } from "@avocadostudio-ai/shared";
|
package/dist/blocks.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Everything a site needs to describe its own blocks, re-exported from the one
|
|
3
|
+
* package its install line names.
|
|
4
|
+
*
|
|
5
|
+
* `registerBlock` and the block-meta types live in `@avocadostudio-ai/shared`,
|
|
6
|
+
* which the SDK depends on — and a dependency of a dependency is not a
|
|
7
|
+
* specifier your own source may import. Under pnpm's isolated `node_modules`
|
|
8
|
+
* it sits under `.pnpm/` where the SDK can reach it and the site cannot, so
|
|
9
|
+
* `import { registerBlock } from "@avocadostudio-ai/shared"` fails outright on
|
|
10
|
+
* a clean `pnpm add @avocadostudio-ai/site-sdk`. Under npm's flat hoisting it
|
|
11
|
+
* resolves, which is worse: it keeps working until an unrelated dependency
|
|
12
|
+
* change re-hoists, and then breaks in a build that never mentions us.
|
|
13
|
+
*
|
|
14
|
+
* Importing `z` from here rather than from `zod` also makes the "same copy of
|
|
15
|
+
* zod" rule structural instead of merely documented: `registerBlock` takes a
|
|
16
|
+
* `z.ZodObject`, and a Zod object is only assignable to one built by the same
|
|
17
|
+
* copy of the library. A site that also uses a zod-3 SDK (Sanity, for one) and
|
|
18
|
+
* writes its schemas against its own hoisted `zod` gets a wall of structural
|
|
19
|
+
* type errors naming methods nobody has heard of, none of which say "you have
|
|
20
|
+
* two copies of zod".
|
|
21
|
+
*
|
|
22
|
+
* This entry is deliberately free of React components — importing it costs a
|
|
23
|
+
* site nothing in its client bundle.
|
|
24
|
+
*/
|
|
25
|
+
export { z } from "@avocadostudio-ai/shared";
|
|
26
|
+
export { registerBlock, declareBlockCatalogue, getBlockMeta, getAllBlockMeta, getChromeTypes, isChrome, IMAGE_PLACEHOLDER, isImagePlaceholder,
|
|
27
|
+
// Block-type name conversions, for CMS adapters whose document types are
|
|
28
|
+
// spelled differently from Avocado's (`Hero` vs `hero` vs `featureGrid`).
|
|
29
|
+
blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "@avocadostudio-ai/shared";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { extractMarkedBlocks, editableCoverage, formatEditableCoverage } from "@avocadostudio-ai/shared";
|
|
2
|
+
export type { MarkedBlock, BlockCoverageGap, EditableCoverage } from "@avocadostudio-ai/shared";
|
|
3
|
+
export { panelCoverage, formatPanelCoverage } from "@avocadostudio-ai/shared";
|
|
4
|
+
export type { PanelCoverage, PanelFinding, PanelFindingCode } from "@avocadostudio-ai/shared";
|
package/dist/coverage.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The CI-side half of the marker contract.
|
|
3
|
+
*
|
|
4
|
+
* `editableCoverage` compares the fields a block manifest declares against the
|
|
5
|
+
* `data-editable-target` markers a rendered page actually carries, so a field
|
|
6
|
+
* that loses its marker in a refactor fails a build instead of silently losing
|
|
7
|
+
* inline editing, its hover pill and its image Change button.
|
|
8
|
+
*
|
|
9
|
+
* Re-exported here because the site cannot import `@avocadostudio-ai/shared`
|
|
10
|
+
* directly — see `./blocks.ts`. Kept out of `./markers` so that nothing a page
|
|
11
|
+
* renders pulls an HTML scanner into its bundle.
|
|
12
|
+
*/
|
|
13
|
+
export { extractMarkedBlocks, editableCoverage, formatEditableCoverage } from "@avocadostudio-ai/shared";
|
|
14
|
+
/*
|
|
15
|
+
* The other half. `editableCoverage` asks whether the *preview* offers each
|
|
16
|
+
* field; `panelCoverage` asks whether the *property panel* is intelligible —
|
|
17
|
+
* whether every list row can be told apart, whether polymorphic branches
|
|
18
|
+
* actually narrow, whether anything in your content is described by nothing.
|
|
19
|
+
*
|
|
20
|
+
* Pass the editor's own registry as `builtinTypes` to also learn where your
|
|
21
|
+
* block type names collide with Avocado's built-ins, which is the one finding
|
|
22
|
+
* that explains several of the others.
|
|
23
|
+
*/
|
|
24
|
+
export { panelCoverage, formatPanelCoverage } from "@avocadostudio-ai/shared";
|
package/dist/draft-fetch.js
CHANGED
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
import { pageDocSchemaLenient, siteConfigSchema } from "@avocadostudio-ai/shared";
|
|
2
|
+
/*
|
|
3
|
+
* The address of the orchestrator this site should read drafts from.
|
|
4
|
+
*
|
|
5
|
+
* The `127.0.0.1:4200` fallback is the *standalone* orchestrator, and a
|
|
6
|
+
* library-mode site by definition does not run one — its orchestrator is a
|
|
7
|
+
* route inside itself. Falling through to :4200 there is silent when nothing is
|
|
8
|
+
* listening (the fetch fails, the preview quietly serves published content, and
|
|
9
|
+
* every edit looks like it did nothing) and worse when something is: the fetch
|
|
10
|
+
* succeeds against a foreign process and the preview renders another project's
|
|
11
|
+
* pages, with a 200 on both sides. Anyone integrating is likely to have a :4200
|
|
12
|
+
* up, because that is exactly what the standalone stack runs on.
|
|
13
|
+
*
|
|
14
|
+
* So a mounted `createOrchestrator` now publishes its own address on
|
|
15
|
+
* `globalThis`, and it is preferred over the fallback. The key is a literal on
|
|
16
|
+
* both sides because `@avocadostudio-ai/orchestrator-core` is an optional peer
|
|
17
|
+
* — a site not in library mode does not have it installed, so neither package
|
|
18
|
+
* may import the other. See `orchestrator-core/src/handler/library-mount.ts`.
|
|
19
|
+
*/
|
|
20
|
+
const LIBRARY_MOUNT_KEY = "__avocado_library_mount__";
|
|
21
|
+
function libraryModeOrchestratorUrl() {
|
|
22
|
+
const mount = globalThis[LIBRARY_MOUNT_KEY];
|
|
23
|
+
return mount?.observed ?? mount?.declared ?? null;
|
|
24
|
+
}
|
|
25
|
+
let warnedAboutFallback = false;
|
|
2
26
|
export function getOrchestratorUrl() {
|
|
3
27
|
const value = process.env.ORCHESTRATOR_URL?.trim();
|
|
4
28
|
if (value)
|
|
5
29
|
return value.replace(/\/$/, "");
|
|
6
|
-
|
|
30
|
+
const mounted = libraryModeOrchestratorUrl();
|
|
31
|
+
if (mounted)
|
|
32
|
+
return mounted;
|
|
33
|
+
if (process.env.NODE_ENV !== "production") {
|
|
34
|
+
if (!warnedAboutFallback) {
|
|
35
|
+
warnedAboutFallback = true;
|
|
36
|
+
console.warn("[avocado] ORCHESTRATOR_URL is not set, and no library-mode orchestrator " +
|
|
37
|
+
"has registered itself — falling back to http://127.0.0.1:4200. If you " +
|
|
38
|
+
"mounted createOrchestrator inside this app, set ORCHESTRATOR_URL to its " +
|
|
39
|
+
"own URL (e.g. http://localhost:3000/api/avocado). Whatever is listening " +
|
|
40
|
+
"on :4200 is a different process, and it will answer.");
|
|
41
|
+
}
|
|
7
42
|
return "http://127.0.0.1:4200";
|
|
43
|
+
}
|
|
8
44
|
return null;
|
|
9
45
|
}
|
|
10
46
|
/**
|
package/dist/draft-fetch.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test, beforeEach, afterEach } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { fetchEditorPage, fetchEditorSlugs, resetDraftFetchWarnings } from "./draft-fetch.js";
|
|
3
|
+
import { fetchEditorPage, fetchEditorSlugs, getOrchestratorUrl, resetDraftFetchWarnings } from "./draft-fetch.js";
|
|
4
4
|
let calls = [];
|
|
5
5
|
let warnings = [];
|
|
6
6
|
const realFetch = globalThis.fetch;
|
|
@@ -85,3 +85,69 @@ test("fetchEditorSlugs refuses the same way, and returns an empty list", async (
|
|
|
85
85
|
assert.equal(calls.length, 1);
|
|
86
86
|
assert.match(warnings[0], /fetchEditorSlugs/);
|
|
87
87
|
});
|
|
88
|
+
/*
|
|
89
|
+
* VI-04. With `ORCHESTRATOR_URL` unset, a library-mode site used to read its
|
|
90
|
+
* drafts from `127.0.0.1:4200` — the standalone orchestrator it does not run.
|
|
91
|
+
* When nothing is listening there the preview silently serves published content
|
|
92
|
+
* and every edit looks inert; when something is, the fetch succeeds and the
|
|
93
|
+
* preview renders a foreign project's pages. A mounted `createOrchestrator`
|
|
94
|
+
* now publishes its own address, and it must win.
|
|
95
|
+
*/
|
|
96
|
+
test("a mounted library-mode orchestrator beats the :4200 fallback", () => {
|
|
97
|
+
const prev = process.env.ORCHESTRATOR_URL;
|
|
98
|
+
const g = globalThis;
|
|
99
|
+
const prevMount = g["__avocado_library_mount__"];
|
|
100
|
+
try {
|
|
101
|
+
delete process.env.ORCHESTRATOR_URL;
|
|
102
|
+
g["__avocado_library_mount__"] = { declared: "http://localhost:3001/api/avocado" };
|
|
103
|
+
assert.equal(getOrchestratorUrl(), "http://localhost:3001/api/avocado");
|
|
104
|
+
// A real request the handler served outranks what config declared.
|
|
105
|
+
g["__avocado_library_mount__"] = {
|
|
106
|
+
declared: "http://localhost:3001/api/avocado",
|
|
107
|
+
observed: "https://villa.example.com/api/avocado"
|
|
108
|
+
};
|
|
109
|
+
assert.equal(getOrchestratorUrl(), "https://villa.example.com/api/avocado");
|
|
110
|
+
// And an explicit env var still outranks both.
|
|
111
|
+
process.env.ORCHESTRATOR_URL = "http://localhost:9999/api/avocado";
|
|
112
|
+
assert.equal(getOrchestratorUrl(), "http://localhost:9999/api/avocado");
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
if (prev === undefined)
|
|
116
|
+
delete process.env.ORCHESTRATOR_URL;
|
|
117
|
+
else
|
|
118
|
+
process.env.ORCHESTRATOR_URL = prev;
|
|
119
|
+
if (prevMount === undefined)
|
|
120
|
+
delete g["__avocado_library_mount__"];
|
|
121
|
+
else
|
|
122
|
+
g["__avocado_library_mount__"] = prevMount;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
test("with nothing mounted the fallback still applies, and says so out loud", () => {
|
|
126
|
+
const prev = process.env.ORCHESTRATOR_URL;
|
|
127
|
+
const g = globalThis;
|
|
128
|
+
const prevMount = g["__avocado_library_mount__"];
|
|
129
|
+
const said = [];
|
|
130
|
+
const prevWarn = console.warn;
|
|
131
|
+
try {
|
|
132
|
+
delete process.env.ORCHESTRATOR_URL;
|
|
133
|
+
delete g["__avocado_library_mount__"];
|
|
134
|
+
console.warn = (...args) => { said.push(args.join(" ")); };
|
|
135
|
+
assert.equal(getOrchestratorUrl(), "http://127.0.0.1:4200");
|
|
136
|
+
assert.equal(said.length, 1, "the fallback warns exactly once");
|
|
137
|
+
assert.match(said[0], /ORCHESTRATOR_URL is not set/);
|
|
138
|
+
assert.match(said[0], /createOrchestrator/);
|
|
139
|
+
getOrchestratorUrl();
|
|
140
|
+
assert.equal(said.length, 1, "and not on every subsequent read");
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
console.warn = prevWarn;
|
|
144
|
+
if (prev === undefined)
|
|
145
|
+
delete process.env.ORCHESTRATOR_URL;
|
|
146
|
+
else
|
|
147
|
+
process.env.ORCHESTRATOR_URL = prev;
|
|
148
|
+
if (prevMount === undefined)
|
|
149
|
+
delete g["__avocado_library_mount__"];
|
|
150
|
+
else
|
|
151
|
+
g["__avocado_library_mount__"] = prevMount;
|
|
152
|
+
}
|
|
153
|
+
});
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
export { EditorOverlay } from "./editor-overlay.tsx";
|
|
2
2
|
export { buildEditorQuerySuffix } from "./editor-query.ts";
|
|
3
|
-
export
|
|
4
|
-
readonly "data-block-id"?: undefined;
|
|
5
|
-
readonly "data-block-type"?: undefined;
|
|
6
|
-
readonly className?: undefined;
|
|
7
|
-
readonly style?: undefined;
|
|
8
|
-
} | {
|
|
9
|
-
readonly "data-block-id": string;
|
|
10
|
-
readonly "data-block-type": string;
|
|
11
|
-
readonly className: "editor-selectable";
|
|
12
|
-
readonly style: {
|
|
13
|
-
readonly viewTransitionName: `block-${string}`;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
3
|
+
export { getPreviewWrapperProps, editableProps } from "./markers.ts";
|
|
16
4
|
export { renderBlocks } from "./render-blocks.tsx";
|
|
17
5
|
export { RenderedBlocks, PreviewBlock } from "./live-preview-blocks.tsx";
|
|
18
|
-
export { LivePreviewProvider } from "@avocadostudio-ai/preview-adapter";
|
|
6
|
+
export { LivePreviewProvider, useLivePreviewBlocks } from "@avocadostudio-ai/preview-adapter";
|
|
19
7
|
export type { LivePreviewPage, LivePreviewBridgeApi } from "@avocadostudio-ai/preview-adapter";
|
|
20
8
|
export { getEditorCorsOrigins, applyEditorCors, createEditorCorsOptionsHandler } from "./editor-cors.ts";
|
package/dist/editor.js
CHANGED
|
@@ -2,21 +2,30 @@
|
|
|
2
2
|
export { EditorOverlay } from "./editor-overlay.js";
|
|
3
3
|
// Editor query param utilities
|
|
4
4
|
export { buildEditorQuerySuffix } from "./editor-query.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
5
|
+
/*
|
|
6
|
+
* The two field/block attribute helpers live in `site-sdk/markers`, a leaf
|
|
7
|
+
* entry with no React in it. They are re-exported here so existing imports
|
|
8
|
+
* keep working — but import them from `@avocadostudio-ai/site-sdk/markers` in
|
|
9
|
+
* anything a public page can reach. This entry also exports `EditorOverlay`
|
|
10
|
+
* and the live-preview provider, and a `'use client'` component that reaches
|
|
11
|
+
* for the two-line attribute helper from here drags the whole editor into the
|
|
12
|
+
* public bundle: +66 kB First Load JS, for identical markup.
|
|
13
|
+
*/
|
|
14
|
+
export { getPreviewWrapperProps, editableProps } from "./markers.js";
|
|
16
15
|
// Block rendering helper
|
|
17
16
|
export { renderBlocks } from "./render-blocks.js";
|
|
18
17
|
// Live-preview store renderer (streams field drafts through React)
|
|
19
18
|
export { RenderedBlocks, PreviewBlock } from "./live-preview-blocks.js";
|
|
20
|
-
|
|
19
|
+
/*
|
|
20
|
+
* The React live-preview path, both halves.
|
|
21
|
+
*
|
|
22
|
+
* `useLivePreviewBlocks` used to be the one documented import a site could not
|
|
23
|
+
* satisfy: `LivePreviewProvider` was re-exported here and the hook was not, so
|
|
24
|
+
* the walkthrough's own example told the reader to import from
|
|
25
|
+
* `@avocadostudio-ai/preview-adapter` — a dependency of this package, not of
|
|
26
|
+
* theirs. Under pnpm that does not resolve at all; under npm it resolves until
|
|
27
|
+
* a re-hoist. A provider with no way to read it was never a coherent boundary.
|
|
28
|
+
*/
|
|
29
|
+
export { LivePreviewProvider, useLivePreviewBlocks } from "@avocadostudio-ai/preview-adapter";
|
|
21
30
|
// Editor CORS utilities
|
|
22
31
|
export { getEditorCorsOrigins, applyEditorCors, createEditorCorsOptionsHandler } from "./editor-cors.js";
|
|
@@ -17,7 +17,7 @@ import { memo } from "react";
|
|
|
17
17
|
import { SharedBlockRenderer, BlockErrorBoundary } from "@avocadostudio-ai/blocks";
|
|
18
18
|
import { getChromeTypes } from "@avocadostudio-ai/shared";
|
|
19
19
|
import { useLivePreviewBlocks } from "@avocadostudio-ai/preview-adapter";
|
|
20
|
-
import { getPreviewWrapperProps } from "./
|
|
20
|
+
import { getPreviewWrapperProps } from "./markers.js";
|
|
21
21
|
const CHROME_BLOCK_TYPES = new Set(getChromeTypes());
|
|
22
22
|
export const PreviewBlock = memo(function PreviewBlock({ block }) {
|
|
23
23
|
return (_jsx("div", { id: block.id, ...getPreviewWrapperProps(true, block.id, block.type), children: _jsx(BlockErrorBoundary, { blockId: block.id, blockType: block.type, resetKey: block, children: _jsx(SharedBlockRenderer, { block: block }) }) }));
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { FieldKind } from "@avocadostudio-ai/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Mark the element that is one block, so the editor can select it.
|
|
4
|
+
*
|
|
5
|
+
* Selection is built entirely on `[data-block-id]`: a click in the preview
|
|
6
|
+
* resolves through `closest("[data-block-id]")`, and no match is read as
|
|
7
|
+
* "clicked outside any block", which *clears* the selection. Marking fields
|
|
8
|
+
* without this gets you a preview that frames, renders and scrolls correctly
|
|
9
|
+
* and deselects on every click — which looks exactly like selection mode being
|
|
10
|
+
* switched off.
|
|
11
|
+
*
|
|
12
|
+
* Returns `{}` when `editorMode` is false, so the published page renders
|
|
13
|
+
* precisely the markup it rendered before.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getPreviewWrapperProps(editorMode: boolean, blockId: string, blockType: string): {
|
|
16
|
+
readonly "data-block-id"?: undefined;
|
|
17
|
+
readonly "data-block-type"?: undefined;
|
|
18
|
+
readonly className?: undefined;
|
|
19
|
+
readonly style?: undefined;
|
|
20
|
+
} | {
|
|
21
|
+
readonly "data-block-id": string;
|
|
22
|
+
readonly "data-block-type": string;
|
|
23
|
+
readonly className: "editor-selectable";
|
|
24
|
+
readonly style: {
|
|
25
|
+
readonly viewTransitionName: `block-${string}`;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Mark the element that draws one editable field.
|
|
30
|
+
*
|
|
31
|
+
* This is the second half of instrumenting a preview, and until recently it was
|
|
32
|
+
* the half with no helper. `getPreviewWrapperProps` covers the block boundary
|
|
33
|
+
* and is named, exported and documented; the per-field attributes were prose in
|
|
34
|
+
* the integration guide with a link to a renderer to copy from. The predictable
|
|
35
|
+
* result is an integration that wraps every block and marks no fields — which
|
|
36
|
+
* frames, renders, selects, scrolls and opens the property panel correctly, and
|
|
37
|
+
* silently has no inline text editing, no field pills and no image buttons in
|
|
38
|
+
* the preview, because every one of those is found by walking
|
|
39
|
+
* `[data-editable-target]`.
|
|
40
|
+
*
|
|
41
|
+
* ```tsx
|
|
42
|
+
* <div className="hero__media" {...editableProps("imageUrl", { kind: "image" })}>
|
|
43
|
+
* <Image src={props.imageUrl} … />
|
|
44
|
+
* </div>
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* The path is the same grammar an operation uses — `title`, `cards[0].title`,
|
|
48
|
+
* `links[0].children[1].label` — because it is the same path: what the overlay
|
|
49
|
+
* reads here is what it sends back as the field to patch.
|
|
50
|
+
*
|
|
51
|
+
* **Put it on an element, not on the image.** For an image field the attribute
|
|
52
|
+
* belongs on the wrapper around the `<img>`, never on the image itself: the
|
|
53
|
+
* overlay appends its Change button *into* the marked element, and nothing can
|
|
54
|
+
* be appended into an `<img>`.
|
|
55
|
+
*
|
|
56
|
+
* **`kind` is how a site says what a field is in its own vocabulary.** Without
|
|
57
|
+
* it the overlay has to guess an image from the prop name, against Avocado's
|
|
58
|
+
* own naming (`imageUrl`, `*.src`) — so a site whose field is `photoUrl` or
|
|
59
|
+
* `heroSrc` gets a picker in the property panel and no button in the preview,
|
|
60
|
+
* with no error on either side. Pass the same word the block manifest uses and
|
|
61
|
+
* the guess never runs.
|
|
62
|
+
*
|
|
63
|
+
* There is no `editorMode` argument on purpose. These are inert data attributes
|
|
64
|
+
* that cost a few bytes and do nothing unless the overlay is mounted (all of
|
|
65
|
+
* its styling is scoped under `[data-editor-active]`), and most sites render
|
|
66
|
+
* their blocks through components shared with the public pages, where threading
|
|
67
|
+
* a flag down to every field is the step that does not get done.
|
|
68
|
+
*/
|
|
69
|
+
export declare function editableProps(path: string, options?: {
|
|
70
|
+
/** Text for the hover pill. Defaults to the path. */
|
|
71
|
+
label?: string;
|
|
72
|
+
/** What kind of field this is — the same vocabulary as the block manifest. */
|
|
73
|
+
kind?: FieldKind;
|
|
74
|
+
}): {
|
|
75
|
+
readonly "data-editable-kind"?: FieldKind | undefined;
|
|
76
|
+
readonly "data-editable-target": string;
|
|
77
|
+
readonly "data-editable-target-label": string;
|
|
78
|
+
};
|
package/dist/markers.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The two attribute helpers that instrument a preview — and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* These are pure functions returning plain data attributes, but until this
|
|
5
|
+
* entry existed the only way to import them was `@avocadostudio-ai/site-sdk/
|
|
6
|
+
* editor`, which also exports `EditorOverlay` and the live-preview provider.
|
|
7
|
+
* Most sites render their blocks through components shared with their public
|
|
8
|
+
* pages, so following our own advice — mark the fields in the components you
|
|
9
|
+
* already have — pulled the whole editor into the public bundle. Measured on a
|
|
10
|
+
* real integration: **+66 kB First Load JS on every page**, for markup that was
|
|
11
|
+
* byte-for-byte identical.
|
|
12
|
+
*
|
|
13
|
+
* `site-sdk/editor` still re-exports both names, so nothing that already
|
|
14
|
+
* imports them breaks. Import them from here in anything a public page can
|
|
15
|
+
* reach.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Mark the element that is one block, so the editor can select it.
|
|
19
|
+
*
|
|
20
|
+
* Selection is built entirely on `[data-block-id]`: a click in the preview
|
|
21
|
+
* resolves through `closest("[data-block-id]")`, and no match is read as
|
|
22
|
+
* "clicked outside any block", which *clears* the selection. Marking fields
|
|
23
|
+
* without this gets you a preview that frames, renders and scrolls correctly
|
|
24
|
+
* and deselects on every click — which looks exactly like selection mode being
|
|
25
|
+
* switched off.
|
|
26
|
+
*
|
|
27
|
+
* Returns `{}` when `editorMode` is false, so the published page renders
|
|
28
|
+
* precisely the markup it rendered before.
|
|
29
|
+
*/
|
|
30
|
+
export function getPreviewWrapperProps(editorMode, blockId, blockType) {
|
|
31
|
+
if (!editorMode)
|
|
32
|
+
return {};
|
|
33
|
+
return {
|
|
34
|
+
"data-block-id": blockId,
|
|
35
|
+
"data-block-type": blockType,
|
|
36
|
+
className: "editor-selectable",
|
|
37
|
+
style: { viewTransitionName: `block-${blockId}` }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Mark the element that draws one editable field.
|
|
42
|
+
*
|
|
43
|
+
* This is the second half of instrumenting a preview, and until recently it was
|
|
44
|
+
* the half with no helper. `getPreviewWrapperProps` covers the block boundary
|
|
45
|
+
* and is named, exported and documented; the per-field attributes were prose in
|
|
46
|
+
* the integration guide with a link to a renderer to copy from. The predictable
|
|
47
|
+
* result is an integration that wraps every block and marks no fields — which
|
|
48
|
+
* frames, renders, selects, scrolls and opens the property panel correctly, and
|
|
49
|
+
* silently has no inline text editing, no field pills and no image buttons in
|
|
50
|
+
* the preview, because every one of those is found by walking
|
|
51
|
+
* `[data-editable-target]`.
|
|
52
|
+
*
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <div className="hero__media" {...editableProps("imageUrl", { kind: "image" })}>
|
|
55
|
+
* <Image src={props.imageUrl} … />
|
|
56
|
+
* </div>
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* The path is the same grammar an operation uses — `title`, `cards[0].title`,
|
|
60
|
+
* `links[0].children[1].label` — because it is the same path: what the overlay
|
|
61
|
+
* reads here is what it sends back as the field to patch.
|
|
62
|
+
*
|
|
63
|
+
* **Put it on an element, not on the image.** For an image field the attribute
|
|
64
|
+
* belongs on the wrapper around the `<img>`, never on the image itself: the
|
|
65
|
+
* overlay appends its Change button *into* the marked element, and nothing can
|
|
66
|
+
* be appended into an `<img>`.
|
|
67
|
+
*
|
|
68
|
+
* **`kind` is how a site says what a field is in its own vocabulary.** Without
|
|
69
|
+
* it the overlay has to guess an image from the prop name, against Avocado's
|
|
70
|
+
* own naming (`imageUrl`, `*.src`) — so a site whose field is `photoUrl` or
|
|
71
|
+
* `heroSrc` gets a picker in the property panel and no button in the preview,
|
|
72
|
+
* with no error on either side. Pass the same word the block manifest uses and
|
|
73
|
+
* the guess never runs.
|
|
74
|
+
*
|
|
75
|
+
* There is no `editorMode` argument on purpose. These are inert data attributes
|
|
76
|
+
* that cost a few bytes and do nothing unless the overlay is mounted (all of
|
|
77
|
+
* its styling is scoped under `[data-editor-active]`), and most sites render
|
|
78
|
+
* their blocks through components shared with the public pages, where threading
|
|
79
|
+
* a flag down to every field is the step that does not get done.
|
|
80
|
+
*/
|
|
81
|
+
export function editableProps(path, options) {
|
|
82
|
+
return {
|
|
83
|
+
"data-editable-target": path,
|
|
84
|
+
"data-editable-target-label": options?.label ?? path,
|
|
85
|
+
...(options?.kind ? { "data-editable-kind": options.kind } : {})
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/next-config.test.js
CHANGED
|
@@ -97,14 +97,20 @@ test("withAvocado adds what is missing and keeps what was declared", () => {
|
|
|
97
97
|
});
|
|
98
98
|
test("a config that already declares everything is returned unchanged", () => {
|
|
99
99
|
/*
|
|
100
|
-
* The other
|
|
101
|
-
*
|
|
102
|
-
* transpile contract on its own: when there is nothing to
|
|
103
|
-
* hands back the very object it was given rather than a copy.
|
|
100
|
+
* The other halves are switched off because they always have something to add
|
|
101
|
+
* — image hosts, the server externals, and now the framing headers — and this
|
|
102
|
+
* test is about the transpile contract on its own: when there is nothing to
|
|
103
|
+
* add, the wrapper hands back the very object it was given rather than a copy.
|
|
104
104
|
*/
|
|
105
105
|
const root = fixture({ "@avocadostudio-ai/shared": { main: "src/index.ts" } });
|
|
106
106
|
const input = { transpilePackages: ["@avocadostudio-ai/shared"] };
|
|
107
|
-
assert.equal(withAvocado(input, {
|
|
107
|
+
assert.equal(withAvocado(input, {
|
|
108
|
+
cwd: root,
|
|
109
|
+
silent: true,
|
|
110
|
+
images: false,
|
|
111
|
+
serverExternals: false,
|
|
112
|
+
framing: false
|
|
113
|
+
}), input);
|
|
108
114
|
});
|
|
109
115
|
test("merging image hosts does not disturb the transpile list", () => {
|
|
110
116
|
const root = fixture({ "@avocadostudio-ai/shared": { main: "src/index.ts" } });
|
|
@@ -353,3 +359,35 @@ test("trailingSlash: false leaves the whole thing to the app", () => {
|
|
|
353
359
|
const config = withAvocado({ trailingSlash: true }, { cwd: root, silent: true, trailingSlash: false });
|
|
354
360
|
assert.equal(config.skipTrailingSlashRedirect, undefined);
|
|
355
361
|
});
|
|
362
|
+
test("the framing headers are derived from the editor allowlist, both spellings", async () => {
|
|
363
|
+
const config = withAvocado({}, { cwd: EMPTY_DIR, env: { EDITOR_CORS_ORIGINS: "http://localhost:4103" } });
|
|
364
|
+
const rules = await config.headers();
|
|
365
|
+
const editorRule = rules.find((r) => r.has?.[0]?.key === "__editor");
|
|
366
|
+
const csp = editorRule.headers.find((h) => h.key === "Content-Security-Policy").value;
|
|
367
|
+
assert.match(csp, /http:\/\/localhost:4103/);
|
|
368
|
+
assert.match(csp, /http:\/\/127\.0\.0\.1:4103/, "the CLI binds 127.0.0.1 and prints it");
|
|
369
|
+
assert.match(csp, /^frame-ancestors 'self'/);
|
|
370
|
+
});
|
|
371
|
+
test("the public rule uses `missing:`, or the browser intersects two CSP headers", async () => {
|
|
372
|
+
const config = withAvocado({}, { cwd: EMPTY_DIR, env: {} });
|
|
373
|
+
const rules = await config.headers();
|
|
374
|
+
const publicRule = rules.find((r) => r.missing?.[0]?.key === "__editor");
|
|
375
|
+
assert.ok(publicRule, "without `missing:` both rules match an editor request");
|
|
376
|
+
assert.equal(publicRule.headers.find((h) => h.key === "Content-Security-Policy").value, "frame-ancestors 'self';");
|
|
377
|
+
assert.equal(publicRule.headers.find((h) => h.key === "X-Frame-Options").value, "SAMEORIGIN");
|
|
378
|
+
});
|
|
379
|
+
test("a site's own headers come first, and framing can be turned off", async () => {
|
|
380
|
+
const own = { source: "/:path*", headers: [{ key: "X-Mine", value: "1" }] };
|
|
381
|
+
const config = withAvocado({ headers: async () => [own] }, { cwd: EMPTY_DIR, env: {} });
|
|
382
|
+
const rules = await config.headers();
|
|
383
|
+
assert.deepEqual(rules[0], own, "Next uses the first match per header");
|
|
384
|
+
assert.equal(rules.length, 3);
|
|
385
|
+
const off = withAvocado({}, { cwd: EMPTY_DIR, env: {}, framing: false });
|
|
386
|
+
assert.equal(off.headers, undefined);
|
|
387
|
+
});
|
|
388
|
+
test("in production nothing is allowed to frame until a deployment names its editor", async () => {
|
|
389
|
+
const config = withAvocado({}, { cwd: EMPTY_DIR, env: { NODE_ENV: "production" } });
|
|
390
|
+
const rules = await config.headers();
|
|
391
|
+
const editorRule = rules.find((r) => r.has?.[0]?.key === "__editor");
|
|
392
|
+
assert.equal(editorRule.headers.find((h) => h.key === "Content-Security-Policy").value, "frame-ancestors 'self';");
|
|
393
|
+
});
|
package/dist/render-blocks.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { SharedBlockRenderer, BlockErrorBoundary, getCustomRenderer } from "@avocadostudio-ai/blocks";
|
|
3
3
|
import { getChromeTypes } from "@avocadostudio-ai/shared";
|
|
4
|
-
import { getPreviewWrapperProps } from "./
|
|
4
|
+
import { getPreviewWrapperProps } from "./markers.js";
|
|
5
5
|
/**
|
|
6
6
|
* Renders a list of blocks with error boundaries.
|
|
7
7
|
* When `editable` is true, adds preview wrapper attributes for editor overlay selection.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createOrchestrator, jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaLabel, type CreateOrchestratorConfig, type OrchestratorHandler, type OrchestratorAuth, type AuthContext, type CmsAdapter, type CmsCapabilities, type CmsInlineAsset, type CmsPublishContext, type CmsPublishResult, type CmsPerspective, type CmsReadOptions, type CmsMediaItem, type CmsMediaPage, type CmsMediaQuery, type CmsMediaSource, type CmsMediaSourceConfig, type ResolvedCapabilities } from "@avocadostudio-ai/orchestrator-core";
|
|
1
|
+
export { createOrchestrator, jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaUploader, cmsMediaLabel, type CreateOrchestratorConfig, type OrchestratorHandler, type OrchestratorAuth, type AuthContext, type CmsAdapter, type CmsCapabilities, type CmsInlineAsset, type CmsPublishContext, type CmsPublishResult, type CmsPerspective, type CmsReadOptions, type CmsMediaItem, type CmsMediaPage, type CmsMediaQuery, type CmsMediaUpload, type CmsMediaSource, type CmsMediaUploader, type CmsMediaSourceConfig, type ResolvedCapabilities } from "@avocadostudio-ai/orchestrator-core";
|
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
//
|
|
12
12
|
// This file stays so `@avocadostudio-ai/site-sdk/server` — the entry point
|
|
13
13
|
// every example, README and docs page uses — keeps resolving unchanged.
|
|
14
|
-
export { createOrchestrator, jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaLabel } from "@avocadostudio-ai/orchestrator-core";
|
|
14
|
+
export { createOrchestrator, jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaUploader, cmsMediaLabel } from "@avocadostudio-ai/orchestrator-core";
|
package/next-config.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export const AVOCADO_SERVER_EXTERNALS: string[]
|
|
|
44
44
|
export function withAvocado<
|
|
45
45
|
T extends {
|
|
46
46
|
transpilePackages?: string[]
|
|
47
|
+
headers?: () => Promise<unknown[]> | unknown[]
|
|
47
48
|
images?: { remotePatterns?: unknown[] }
|
|
48
49
|
serverExternalPackages?: string[]
|
|
49
50
|
trailingSlash?: boolean
|
|
@@ -70,9 +71,28 @@ export function withAvocado<
|
|
|
70
71
|
* `createEditorProxy({ trailingSlash: true })`.
|
|
71
72
|
*/
|
|
72
73
|
trailingSlash?: boolean
|
|
74
|
+
/**
|
|
75
|
+
* Set false to write the framing headers yourself.
|
|
76
|
+
*
|
|
77
|
+
* When true (the default), `withAvocado` appends a pair of `headers()` rules
|
|
78
|
+
* that let the editor frame `?__editor=1` requests — derived from the same
|
|
79
|
+
* `EDITOR_CORS_ORIGINS` / `NEXT_PUBLIC_EDITOR_ORIGIN` allowlist the editor
|
|
80
|
+
* API uses, so the two halves cannot disagree. Your own `headers()` entries
|
|
81
|
+
* come first and win, since Next uses the first match per header.
|
|
82
|
+
*/
|
|
83
|
+
framing?: boolean
|
|
73
84
|
/** Environment to read the orchestrator origin from. Defaults to `process.env`. */
|
|
74
85
|
env?: Record<string, string | undefined>
|
|
75
86
|
}
|
|
76
87
|
): T
|
|
77
88
|
|
|
89
|
+
/** Every origin allowed to frame the preview, both loopback spellings included. */
|
|
90
|
+
export function avocadoEditorOrigins(env?: Record<string, string | undefined>): string[]
|
|
91
|
+
|
|
92
|
+
/** Just the framing headers, for a site that wants them without the rest. */
|
|
93
|
+
export function withAvocadoFraming<T extends object>(
|
|
94
|
+
config?: T,
|
|
95
|
+
env?: Record<string, string | undefined>
|
|
96
|
+
): T
|
|
97
|
+
|
|
78
98
|
export default withAvocado
|
package/next-config.mjs
CHANGED
|
@@ -403,6 +403,88 @@ function builtOrchestratorCore(from, linked) {
|
|
|
403
403
|
* as it was. Never throws — a helper that can break `next.config` is worse than
|
|
404
404
|
* the bug it fixes.
|
|
405
405
|
*/
|
|
406
|
+
|
|
407
|
+
/*
|
|
408
|
+
* The framing rule, derived from the one allowlist the site already has.
|
|
409
|
+
*
|
|
410
|
+
* A library-mode site names the editor's origin in two places and they must
|
|
411
|
+
* agree: CORS on the editor API, and `frame-ancestors` on the preview. Every
|
|
412
|
+
* integration so far hand-wrote the second in `next.config`, and every one of
|
|
413
|
+
* them hardcoded `:4100` — which is right until the editor is served anywhere
|
|
414
|
+
* else, and anyone who already runs the standalone stack has to serve it
|
|
415
|
+
* somewhere else. Then the two halves fail separately and silently: the API
|
|
416
|
+
* answers 200 with no `access-control-allow-origin` (the Sites page reports the
|
|
417
|
+
* site as *offline*) and the browser refuses the frame (the preview is a blank
|
|
418
|
+
* rectangle). Neither names a port, and neither says CORS or CSP.
|
|
419
|
+
*
|
|
420
|
+
* Two rules that are easy to get wrong by hand, and are why this is worth
|
|
421
|
+
* generating:
|
|
422
|
+
*
|
|
423
|
+
* - **Both spellings.** The CLI binds `127.0.0.1` and prints it; every doc says
|
|
424
|
+
* `localhost`. `frame-ancestors` compares origins as strings.
|
|
425
|
+
* - **`missing:` on the public rule.** Without it both rules match an editor
|
|
426
|
+
* request, the browser receives two CSP headers, and it enforces their
|
|
427
|
+
* *intersection* — putting `frame-ancestors 'self'` back and blocking the
|
|
428
|
+
* frame it just allowed.
|
|
429
|
+
*/
|
|
430
|
+
function bothLoopbackSpellings(origin) {
|
|
431
|
+
if (origin.includes("localhost")) return [origin, origin.replace("localhost", "127.0.0.1")]
|
|
432
|
+
if (origin.includes("127.0.0.1")) return [origin, origin.replace("127.0.0.1", "localhost")]
|
|
433
|
+
return [origin]
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function avocadoEditorOrigins(env = process.env) {
|
|
437
|
+
const parse = (value) =>
|
|
438
|
+
(value ?? "")
|
|
439
|
+
.split(",")
|
|
440
|
+
.map((entry) => entry.trim().replace(/\/+$/, ""))
|
|
441
|
+
.filter(Boolean)
|
|
442
|
+
|
|
443
|
+
// Mirrors `getEditorCorsOrigins`: a deployment names its editor, development
|
|
444
|
+
// assumes the one the CLI serves by default.
|
|
445
|
+
const defaults = env.NODE_ENV === "production" ? [] : ["http://localhost:4100"]
|
|
446
|
+
const named = [...parse(env.EDITOR_CORS_ORIGINS), ...parse(env.NEXT_PUBLIC_EDITOR_ORIGIN)]
|
|
447
|
+
return [...new Set([...defaults, ...named].flatMap(bothLoopbackSpellings))]
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function withAvocadoFraming(config = {}, env = process.env) {
|
|
451
|
+
const origins = avocadoEditorOrigins(env)
|
|
452
|
+
const existing = config.headers
|
|
453
|
+
const framing = [
|
|
454
|
+
{
|
|
455
|
+
source: "/:path*",
|
|
456
|
+
has: [{ type: "query", key: "__editor" }],
|
|
457
|
+
headers: [
|
|
458
|
+
{
|
|
459
|
+
key: "Content-Security-Policy",
|
|
460
|
+
value: `frame-ancestors 'self'${origins.length > 0 ? ` ${origins.join(" ")}` : ""};`
|
|
461
|
+
}
|
|
462
|
+
]
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
source: "/:path*",
|
|
466
|
+
missing: [{ type: "query", key: "__editor" }],
|
|
467
|
+
headers: [
|
|
468
|
+
{ key: "Content-Security-Policy", value: "frame-ancestors 'self';" },
|
|
469
|
+
{ key: "X-Frame-Options", value: "SAMEORIGIN" }
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
]
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
...config,
|
|
476
|
+
async headers() {
|
|
477
|
+
const own = typeof existing === "function" ? ((await existing()) ?? []) : []
|
|
478
|
+
/*
|
|
479
|
+
* The site's own rules come first: Next uses the first matching entry per
|
|
480
|
+
* header, so a site that already set its own CSP keeps it and this adds
|
|
481
|
+
* nothing. Opt out entirely with `withAvocado(config, { framing: false })`.
|
|
482
|
+
*/
|
|
483
|
+
return [...own, ...framing]
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
406
488
|
export function withAvocado(config = {}, options = {}) {
|
|
407
489
|
const {
|
|
408
490
|
cwd = process.cwd(),
|
|
@@ -410,6 +492,7 @@ export function withAvocado(config = {}, options = {}) {
|
|
|
410
492
|
images = true,
|
|
411
493
|
serverExternals = true,
|
|
412
494
|
trailingSlash = true,
|
|
495
|
+
framing = true,
|
|
413
496
|
env = process.env,
|
|
414
497
|
} = options
|
|
415
498
|
|
|
@@ -434,9 +517,10 @@ export function withAvocado(config = {}, options = {}) {
|
|
|
434
517
|
*/
|
|
435
518
|
const base = trailingSlash ? withAvocadoTrailingSlash(config) : config
|
|
436
519
|
const withImages = images ? withAvocadoImages(base, env) : base
|
|
520
|
+
const framed = framing ? withAvocadoFraming(withImages, env) : withImages
|
|
437
521
|
const result = serverExternals
|
|
438
|
-
? withAvocadoServerExternals(
|
|
439
|
-
:
|
|
522
|
+
? withAvocadoServerExternals(framed, linked === null ? [] : builtOrchestratorCore(cwd, linked))
|
|
523
|
+
: framed
|
|
440
524
|
|
|
441
525
|
if (linked === null) return result
|
|
442
526
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/site-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -21,6 +21,21 @@
|
|
|
21
21
|
"import": "./dist/editor.js",
|
|
22
22
|
"default": "./dist/editor.js"
|
|
23
23
|
},
|
|
24
|
+
"./markers": {
|
|
25
|
+
"types": "./dist/markers.d.ts",
|
|
26
|
+
"import": "./dist/markers.js",
|
|
27
|
+
"default": "./dist/markers.js"
|
|
28
|
+
},
|
|
29
|
+
"./blocks": {
|
|
30
|
+
"types": "./dist/blocks.d.ts",
|
|
31
|
+
"import": "./dist/blocks.js",
|
|
32
|
+
"default": "./dist/blocks.js"
|
|
33
|
+
},
|
|
34
|
+
"./coverage": {
|
|
35
|
+
"types": "./dist/coverage.d.ts",
|
|
36
|
+
"import": "./dist/coverage.js",
|
|
37
|
+
"default": "./dist/coverage.js"
|
|
38
|
+
},
|
|
24
39
|
"./routes": {
|
|
25
40
|
"types": "./dist/routes.d.ts",
|
|
26
41
|
"import": "./dist/routes.js",
|
|
@@ -107,16 +122,16 @@
|
|
|
107
122
|
],
|
|
108
123
|
"dependencies": {
|
|
109
124
|
"zod": "^4.3.6",
|
|
110
|
-
"@avocadostudio-ai/blocks": "^0.
|
|
111
|
-
"@avocadostudio-ai/preview-adapter": "^0.
|
|
112
|
-
"@avocadostudio-ai/shared": "^0.
|
|
125
|
+
"@avocadostudio-ai/blocks": "^0.5.0",
|
|
126
|
+
"@avocadostudio-ai/preview-adapter": "^0.5.0",
|
|
127
|
+
"@avocadostudio-ai/shared": "^0.5.0"
|
|
113
128
|
},
|
|
114
129
|
"peerDependencies": {
|
|
115
130
|
"next": ">=15.0.0",
|
|
116
131
|
"react": ">=19.0.0",
|
|
117
132
|
"react-dom": ">=19.0.0",
|
|
118
133
|
"better-sqlite3": ">=12.0.0",
|
|
119
|
-
"@avocadostudio-ai/orchestrator-core": "^0.
|
|
134
|
+
"@avocadostudio-ai/orchestrator-core": "^0.5.0"
|
|
120
135
|
},
|
|
121
136
|
"peerDependenciesMeta": {
|
|
122
137
|
"@avocadostudio-ai/orchestrator-core": {
|