@avocadostudio-ai/site-sdk 0.3.2 → 0.4.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 +17 -3
- package/dist/editor-api-blocks-catalogue.test.d.ts +1 -0
- package/dist/editor-api-blocks-catalogue.test.js +73 -0
- package/dist/editor-api-handler.d.ts +19 -1
- package/dist/editor-api-handler.js +6 -2
- package/dist/editor.d.ts +52 -0
- package/dist/editor.js +48 -0
- package/dist/publish/field-diff.js +11 -1
- package/dist/publish/field-diff.test.js +56 -3
- package/dist/server/orchestrator.d.ts +1 -1
- package/dist/server/orchestrator.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -706,12 +706,26 @@ If you render Avocado's blocks (a scaffolded site does), skip this. If your site
|
|
|
706
706
|
brought its own, declare them:
|
|
707
707
|
|
|
708
708
|
```ts
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
709
|
+
const SITE_BLOCK_TYPES = ["acme_hero", "acme_splitSection", "acme_pricing"]
|
|
710
|
+
|
|
711
|
+
// app/api/avocado/[[...path]]/route.ts
|
|
712
|
+
export const { GET, POST } = createOrchestrator({ adapter, blockTypes: SITE_BLOCK_TYPES })
|
|
713
|
+
|
|
714
|
+
// app/api/editor/[...path]/route.ts — the same list, again
|
|
715
|
+
export const { GET, POST, OPTIONS } = createEditorApiHandler({
|
|
716
|
+
getPages,
|
|
717
|
+
blockTypes: SITE_BLOCK_TYPES
|
|
712
718
|
})
|
|
713
719
|
```
|
|
714
720
|
|
|
721
|
+
**Both handlers, every time.** The declaration is shared state, so setting it in
|
|
722
|
+
one place looks sufficient and is not: a Next route module is evaluated on the
|
|
723
|
+
first request *to that route*, and the editor asks `/api/editor/blocks` — served
|
|
724
|
+
by `createEditorApiHandler` — before it has any reason to call the orchestrator.
|
|
725
|
+
Declare it only on the orchestrator and the first manifest the editor sees is
|
|
726
|
+
your types plus all eighteen built-ins. Verify with
|
|
727
|
+
`curl -s localhost:3000/api/editor/blocks | jq '.blocks | length'`.
|
|
728
|
+
|
|
715
729
|
The declaration is exclusive. The manifest narrows to it, the planner is only
|
|
716
730
|
told about those types, and `add_block` refuses anything outside it with a
|
|
717
731
|
message naming what the site does render. Nothing is removed from the registry,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `/api/editor/blocks` is the endpoint the editor actually reads, and it is
|
|
3
|
+
* served by a different route module than the orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* `createOrchestrator({ blockTypes })` declares the catalogue too, and because
|
|
6
|
+
* the declaration lives on `globalThis` that looked like enough for both. It is
|
|
7
|
+
* not: a Next route module is evaluated on the first request *to that route*,
|
|
8
|
+
* so an editor that asks for the manifest before anything has touched
|
|
9
|
+
* `/api/avocado/*` is answered by a process where `createOrchestrator` has never
|
|
10
|
+
* run. What comes back is the site's own blocks plus all of Avocado's built-ins,
|
|
11
|
+
* which the site has no renderer for — and an agent reading that list will
|
|
12
|
+
* happily add a `Hero` that applies cleanly and draws nothing.
|
|
13
|
+
*
|
|
14
|
+
* These tests therefore never construct an orchestrator.
|
|
15
|
+
*/
|
|
16
|
+
import { test } from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { z, registerBlock, declareBlockCatalogue } from "@avocadostudio-ai/shared";
|
|
19
|
+
import { createEditorApiHandler } from "./editor-api-handler.js";
|
|
20
|
+
registerBlock("site_ownHero", {
|
|
21
|
+
schema: z.object({ headline: z.string() }).catchall(z.unknown()),
|
|
22
|
+
meta: { displayName: "Own Hero", fields: { headline: { kind: "text" } } }
|
|
23
|
+
});
|
|
24
|
+
async function manifestTypes(config) {
|
|
25
|
+
const { GET } = createEditorApiHandler(config);
|
|
26
|
+
const res = await GET(new Request("http://site.test/api/editor/blocks"), {
|
|
27
|
+
params: Promise.resolve({ path: ["blocks"] })
|
|
28
|
+
});
|
|
29
|
+
assert.equal(res.status, 200);
|
|
30
|
+
const body = (await res.json());
|
|
31
|
+
return body.blocks.map((b) => b.type);
|
|
32
|
+
}
|
|
33
|
+
test("without blockTypes the manifest still offers Avocado's built-ins", async () => {
|
|
34
|
+
declareBlockCatalogue(null);
|
|
35
|
+
const types = await manifestTypes({ getPages: () => [] });
|
|
36
|
+
assert.ok(types.includes("site_ownHero"));
|
|
37
|
+
assert.ok(types.includes("Hero"), "the built-ins are registered transitively and nothing has narrowed them");
|
|
38
|
+
});
|
|
39
|
+
test("blockTypes narrows /api/editor/blocks with no orchestrator in the process", async () => {
|
|
40
|
+
declareBlockCatalogue(null);
|
|
41
|
+
const types = await manifestTypes({ getPages: () => [], blockTypes: ["site_ownHero"] });
|
|
42
|
+
assert.deepEqual(types, ["site_ownHero"]);
|
|
43
|
+
});
|
|
44
|
+
test("blockTypes composes with registerBlocks", async () => {
|
|
45
|
+
declareBlockCatalogue(null);
|
|
46
|
+
let registered = 0;
|
|
47
|
+
const types = await manifestTypes({
|
|
48
|
+
getPages: () => [],
|
|
49
|
+
blockTypes: ["site_lateBlock"],
|
|
50
|
+
registerBlocks: () => {
|
|
51
|
+
registered += 1;
|
|
52
|
+
registerBlock("site_lateBlock", {
|
|
53
|
+
schema: z.object({ title: z.string() }).catchall(z.unknown()),
|
|
54
|
+
meta: { displayName: "Late Block", fields: { title: { kind: "text" } } }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
assert.equal(registered, 1);
|
|
59
|
+
assert.deepEqual(types, ["site_lateBlock"]);
|
|
60
|
+
});
|
|
61
|
+
test("the declaration is re-made per request, so another copy cannot lift it", async () => {
|
|
62
|
+
declareBlockCatalogue(null);
|
|
63
|
+
const { GET } = createEditorApiHandler({ getPages: () => [], blockTypes: ["site_ownHero"] });
|
|
64
|
+
const ask = async () => {
|
|
65
|
+
const res = await GET(new Request("http://site.test/api/editor/blocks"), {
|
|
66
|
+
params: Promise.resolve({ path: ["blocks"] })
|
|
67
|
+
});
|
|
68
|
+
return (await res.json()).blocks.map((b) => b.type);
|
|
69
|
+
};
|
|
70
|
+
assert.deepEqual(await ask(), ["site_ownHero"]);
|
|
71
|
+
declareBlockCatalogue(null); // as if another module had lifted it
|
|
72
|
+
assert.deepEqual(await ask(), ["site_ownHero"]);
|
|
73
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OnPublishFn } from "./editor-routes.ts";
|
|
2
2
|
import { type BlockManifest } from "./editor-manifest.ts";
|
|
3
3
|
import type { PageDoc } from "./types.ts";
|
|
4
|
-
import type
|
|
4
|
+
import { type SiteConfig } from "@avocadostudio-ai/shared";
|
|
5
5
|
export interface EditorApiHandlerConfig {
|
|
6
6
|
getPages: () => PageDoc[] | Promise<PageDoc[]>;
|
|
7
7
|
/**
|
|
@@ -24,6 +24,24 @@ export interface EditorApiHandlerConfig {
|
|
|
24
24
|
* first, then `getManifest` is invoked.
|
|
25
25
|
*/
|
|
26
26
|
registerBlocks?: () => void;
|
|
27
|
+
/**
|
|
28
|
+
* The block types this site renders, narrowing `/api/editor/blocks` to them.
|
|
29
|
+
*
|
|
30
|
+
* `createOrchestrator` takes the same option and declares the same catalogue,
|
|
31
|
+
* and for a while that was assumed to be enough for both routes: the
|
|
32
|
+
* declaration lives on `globalThis`, so one handler's call is visible to the
|
|
33
|
+
* other. What that reasoning missed is *when* it runs. A Next route module is
|
|
34
|
+
* evaluated on the first request to that route, so a site whose editor asks
|
|
35
|
+
* for `/api/editor/blocks` before anything has touched `/api/avocado/*` gets
|
|
36
|
+
* the manifest built before `createOrchestrator` has ever been called — and
|
|
37
|
+
* the answer is the site's own blocks plus all of Avocado's built-ins, which
|
|
38
|
+
* the site has no renderer for.
|
|
39
|
+
*
|
|
40
|
+
* Declaring it here closes that window, and passing it to both handlers is
|
|
41
|
+
* harmless: the declaration records names and the manifest is rebuilt per
|
|
42
|
+
* request.
|
|
43
|
+
*/
|
|
44
|
+
blockTypes?: readonly string[];
|
|
27
45
|
onPublish?: OnPublishFn;
|
|
28
46
|
/** Secret token required for publish requests. Checked against x-publish-token header. */
|
|
29
47
|
publishSecret?: string;
|
|
@@ -3,6 +3,7 @@ import { createBlocksHandler, createPagesHandler, createPublishHandler } from ".
|
|
|
3
3
|
import { applyEditorCors } from "./editor-cors.js";
|
|
4
4
|
import { checkIntegrationOnce } from "./integration-check.js";
|
|
5
5
|
import { buildBlockManifest } from "./editor-manifest.js";
|
|
6
|
+
import { declareBlockCatalogue } from "@avocadostudio-ai/shared";
|
|
6
7
|
/**
|
|
7
8
|
* Creates a single catch-all route handler that serves all editor API endpoints.
|
|
8
9
|
*
|
|
@@ -24,9 +25,12 @@ import { buildBlockManifest } from "./editor-manifest.js";
|
|
|
24
25
|
export function createEditorApiHandler(config) {
|
|
25
26
|
const draftEnable = createDraftEnableHandler();
|
|
26
27
|
const draftDisable = createDraftDisableHandler();
|
|
27
|
-
const
|
|
28
|
+
const needsPreamble = Boolean(config.registerBlocks || config.blockTypes);
|
|
29
|
+
const manifestBuilder = needsPreamble
|
|
28
30
|
? () => {
|
|
29
|
-
config.registerBlocks();
|
|
31
|
+
config.registerBlocks?.();
|
|
32
|
+
if (config.blockTypes)
|
|
33
|
+
declareBlockCatalogue(config.blockTypes);
|
|
30
34
|
return (config.getManifest ?? buildBlockManifest)();
|
|
31
35
|
}
|
|
32
36
|
: config.getManifest;
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FieldKind } from "@avocadostudio-ai/shared";
|
|
1
2
|
export { EditorOverlay } from "./editor-overlay.tsx";
|
|
2
3
|
export { buildEditorQuerySuffix } from "./editor-query.ts";
|
|
3
4
|
export declare function getPreviewWrapperProps(editorMode: boolean, blockId: string, blockType: string): {
|
|
@@ -13,6 +14,57 @@ export declare function getPreviewWrapperProps(editorMode: boolean, blockId: str
|
|
|
13
14
|
readonly viewTransitionName: `block-${string}`;
|
|
14
15
|
};
|
|
15
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Mark the element that draws one editable field.
|
|
19
|
+
*
|
|
20
|
+
* This is the second half of instrumenting a preview, and until now it was the
|
|
21
|
+
* half with no helper. `getPreviewWrapperProps` covers the block boundary and
|
|
22
|
+
* is named, exported and documented; the per-field attributes were prose in the
|
|
23
|
+
* integration guide with a link to a renderer to copy from. The predictable
|
|
24
|
+
* result is an integration that wraps every block and marks no fields — which
|
|
25
|
+
* frames, renders, selects, scrolls and opens the property panel correctly, and
|
|
26
|
+
* silently has no inline text editing, no field pills and no image buttons in
|
|
27
|
+
* the preview, because every one of those is found by walking
|
|
28
|
+
* `[data-editable-target]`.
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <div className="hero__media" {...editableProps("imageUrl", { kind: "image" })}>
|
|
32
|
+
* <Image src={props.imageUrl} … />
|
|
33
|
+
* </div>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* The path is the same grammar an operation uses — `title`, `cards[0].title`,
|
|
37
|
+
* `links[0].children[1].label` — because it is the same path: what the overlay
|
|
38
|
+
* reads here is what it sends back as the field to patch.
|
|
39
|
+
*
|
|
40
|
+
* **Put it on an element, not on the image.** For an image field the attribute
|
|
41
|
+
* belongs on the wrapper around the `<img>`, never on the image itself: the
|
|
42
|
+
* overlay appends its Change button *into* the marked element, and nothing can
|
|
43
|
+
* be appended into an `<img>`.
|
|
44
|
+
*
|
|
45
|
+
* **`kind` is how a site says what a field is in its own vocabulary.** Without
|
|
46
|
+
* it the overlay has to guess an image from the prop name, against Avocado's
|
|
47
|
+
* own naming (`imageUrl`, `*.src`) — so a site whose field is `photoUrl` or
|
|
48
|
+
* `heroSrc` gets a picker in the property panel and no button in the preview,
|
|
49
|
+
* with no error on either side. Pass the same word the block manifest uses and
|
|
50
|
+
* the guess never runs.
|
|
51
|
+
*
|
|
52
|
+
* There is no `editorMode` argument on purpose. These are inert data attributes
|
|
53
|
+
* that cost a few bytes and do nothing unless the overlay is mounted (all of
|
|
54
|
+
* its styling is scoped under `[data-editor-active]`), and most sites render
|
|
55
|
+
* their blocks through components shared with the public pages, where threading
|
|
56
|
+
* a flag down to every field is the step that does not get done.
|
|
57
|
+
*/
|
|
58
|
+
export declare function editableProps(path: string, options?: {
|
|
59
|
+
/** Text for the hover pill. Defaults to the path. */
|
|
60
|
+
label?: string;
|
|
61
|
+
/** What kind of field this is — the same vocabulary as the block manifest. */
|
|
62
|
+
kind?: FieldKind;
|
|
63
|
+
}): {
|
|
64
|
+
readonly "data-editable-kind"?: FieldKind | undefined;
|
|
65
|
+
readonly "data-editable-target": string;
|
|
66
|
+
readonly "data-editable-target-label": string;
|
|
67
|
+
};
|
|
16
68
|
export { renderBlocks } from "./render-blocks.tsx";
|
|
17
69
|
export { RenderedBlocks, PreviewBlock } from "./live-preview-blocks.tsx";
|
|
18
70
|
export { LivePreviewProvider } from "@avocadostudio-ai/preview-adapter";
|
package/dist/editor.js
CHANGED
|
@@ -13,6 +13,54 @@ export function getPreviewWrapperProps(editorMode, blockId, blockType) {
|
|
|
13
13
|
style: { viewTransitionName: `block-${blockId}` }
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Mark the element that draws one editable field.
|
|
18
|
+
*
|
|
19
|
+
* This is the second half of instrumenting a preview, and until now it was the
|
|
20
|
+
* half with no helper. `getPreviewWrapperProps` covers the block boundary and
|
|
21
|
+
* is named, exported and documented; the per-field attributes were prose in the
|
|
22
|
+
* integration guide with a link to a renderer to copy from. The predictable
|
|
23
|
+
* result is an integration that wraps every block and marks no fields — which
|
|
24
|
+
* frames, renders, selects, scrolls and opens the property panel correctly, and
|
|
25
|
+
* silently has no inline text editing, no field pills and no image buttons in
|
|
26
|
+
* the preview, because every one of those is found by walking
|
|
27
|
+
* `[data-editable-target]`.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <div className="hero__media" {...editableProps("imageUrl", { kind: "image" })}>
|
|
31
|
+
* <Image src={props.imageUrl} … />
|
|
32
|
+
* </div>
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* The path is the same grammar an operation uses — `title`, `cards[0].title`,
|
|
36
|
+
* `links[0].children[1].label` — because it is the same path: what the overlay
|
|
37
|
+
* reads here is what it sends back as the field to patch.
|
|
38
|
+
*
|
|
39
|
+
* **Put it on an element, not on the image.** For an image field the attribute
|
|
40
|
+
* belongs on the wrapper around the `<img>`, never on the image itself: the
|
|
41
|
+
* overlay appends its Change button *into* the marked element, and nothing can
|
|
42
|
+
* be appended into an `<img>`.
|
|
43
|
+
*
|
|
44
|
+
* **`kind` is how a site says what a field is in its own vocabulary.** Without
|
|
45
|
+
* it the overlay has to guess an image from the prop name, against Avocado's
|
|
46
|
+
* own naming (`imageUrl`, `*.src`) — so a site whose field is `photoUrl` or
|
|
47
|
+
* `heroSrc` gets a picker in the property panel and no button in the preview,
|
|
48
|
+
* with no error on either side. Pass the same word the block manifest uses and
|
|
49
|
+
* the guess never runs.
|
|
50
|
+
*
|
|
51
|
+
* There is no `editorMode` argument on purpose. These are inert data attributes
|
|
52
|
+
* that cost a few bytes and do nothing unless the overlay is mounted (all of
|
|
53
|
+
* its styling is scoped under `[data-editor-active]`), and most sites render
|
|
54
|
+
* their blocks through components shared with the public pages, where threading
|
|
55
|
+
* a flag down to every field is the step that does not get done.
|
|
56
|
+
*/
|
|
57
|
+
export function editableProps(path, options) {
|
|
58
|
+
return {
|
|
59
|
+
"data-editable-target": path,
|
|
60
|
+
"data-editable-target-label": options?.label ?? path,
|
|
61
|
+
...(options?.kind ? { "data-editable-kind": options.kind } : {})
|
|
62
|
+
};
|
|
63
|
+
}
|
|
16
64
|
// Block rendering helper
|
|
17
65
|
export { renderBlocks } from "./render-blocks.js";
|
|
18
66
|
// Live-preview store renderer (streams field drafts through React)
|
|
@@ -83,7 +83,17 @@ export function diffFields(args) {
|
|
|
83
83
|
const reject = (where) => (change, remedy) => unsupported.push({ where, change, ...(remedy ? { remedy } : {}) });
|
|
84
84
|
for (const [name, spec] of Object.entries(args.specs)) {
|
|
85
85
|
const field = spec.cmsKey ?? name;
|
|
86
|
-
|
|
86
|
+
/*
|
|
87
|
+
* Both of these read the CMS key, not the props key. They used to disagree:
|
|
88
|
+
* the baseline came from `source[name]` while the patch path was built from
|
|
89
|
+
* `cmsKey ?? name`. A spec that actually used `cmsKey` therefore compared
|
|
90
|
+
* against `undefined`, never matched, and emitted a patch on every publish
|
|
91
|
+
* whether or not anything had been edited — the precise overwrite this
|
|
92
|
+
* module exists to prevent. Worse, `rehydrate` was handed that `undefined`
|
|
93
|
+
* as the stored value, so a partial inversion had nothing to merge into and
|
|
94
|
+
* dropped everything the projection had not carried.
|
|
95
|
+
*/
|
|
96
|
+
const before = args.source?.[field];
|
|
87
97
|
const after = spec.rehydrate(args.props, before, args.ctx);
|
|
88
98
|
if (deepEqual(before, after))
|
|
89
99
|
continue;
|
|
@@ -39,13 +39,66 @@ test("cmsKey writes to the field the CMS actually has", () => {
|
|
|
39
39
|
const diff = diffFields({
|
|
40
40
|
specs: { heading: { ...plain("heading"), cmsKey: "headingOverride" } },
|
|
41
41
|
props: { heading: "After" },
|
|
42
|
-
|
|
42
|
+
// Keyed the way the CMS keys it. The version of this test that wrote
|
|
43
|
+
// `{ heading: "Before" }` passed against the bug below, because a baseline
|
|
44
|
+
// of `undefined` also produces a patch at the right path.
|
|
45
|
+
source: { headingOverride: "Before" },
|
|
46
|
+
ctx,
|
|
47
|
+
documentId: "doc1",
|
|
48
|
+
where: "/ → hero",
|
|
49
|
+
paths: sanityPaths
|
|
50
|
+
});
|
|
51
|
+
assert.deepEqual(diff.patches, [
|
|
52
|
+
{ documentId: "doc1", path: "headingOverride", value: "After" }
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
test("an unchanged cmsKey field emits nothing", () => {
|
|
56
|
+
// The regression this file exists for. `diffFields` read the baseline from
|
|
57
|
+
// the *props* key while pathing the patch from `cmsKey`, so every spec that
|
|
58
|
+
// used the option compared its value against `undefined` and republished it
|
|
59
|
+
// on every publish, edited or not.
|
|
60
|
+
const diff = diffFields({
|
|
61
|
+
specs: { heading: { ...plain("heading"), cmsKey: "headingOverride" } },
|
|
62
|
+
props: { heading: "Same" },
|
|
63
|
+
source: { headingOverride: "Same" },
|
|
43
64
|
ctx,
|
|
44
65
|
documentId: "doc1",
|
|
45
66
|
where: "/ → hero",
|
|
46
67
|
paths: sanityPaths
|
|
47
68
|
});
|
|
48
|
-
assert.
|
|
69
|
+
assert.deepEqual(diff.patches, []);
|
|
70
|
+
});
|
|
71
|
+
test("a cmsKey spec's rehydrate receives the value the CMS holds", () => {
|
|
72
|
+
// Partial inversion is the whole point of passing `before`: return the stored
|
|
73
|
+
// object with one key replaced and everything the projection dropped
|
|
74
|
+
// survives. Hand `rehydrate` an `undefined` and it has nothing to merge into,
|
|
75
|
+
// so the patch overwrites instead of editing.
|
|
76
|
+
const seen = [];
|
|
77
|
+
const diff = diffFields({
|
|
78
|
+
specs: {
|
|
79
|
+
alt: {
|
|
80
|
+
cmsKey: "image",
|
|
81
|
+
rehydrate: (props, before) => {
|
|
82
|
+
seen.push(before);
|
|
83
|
+
return { ...before, alt: props.alt };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
props: { alt: "A team in action" },
|
|
88
|
+
source: { image: { _type: "image", asset: { _ref: "image-abc" }, alt: "Old" } },
|
|
89
|
+
ctx,
|
|
90
|
+
documentId: "doc1",
|
|
91
|
+
where: "/ → hero",
|
|
92
|
+
paths: sanityPaths
|
|
93
|
+
});
|
|
94
|
+
assert.deepEqual(seen, [{ _type: "image", asset: { _ref: "image-abc" }, alt: "Old" }]);
|
|
95
|
+
assert.deepEqual(diff.patches, [
|
|
96
|
+
{
|
|
97
|
+
documentId: "doc1",
|
|
98
|
+
path: "image",
|
|
99
|
+
value: { _type: "image", asset: { _ref: "image-abc" }, alt: "A team in action" }
|
|
100
|
+
}
|
|
101
|
+
]);
|
|
49
102
|
});
|
|
50
103
|
test("rehydrate receives the stored value, so an inversion can be partial", () => {
|
|
51
104
|
// The whole reason a snapshot contract cannot work: the editor saw
|
|
@@ -241,7 +294,7 @@ test("one block can write to two documents", () => {
|
|
|
241
294
|
documentId: "page-1",
|
|
242
295
|
prefix: 'pageBuilder[_key=="b1"].',
|
|
243
296
|
specs: { heading: { ...plain("heading"), cmsKey: "headingOverride" } },
|
|
244
|
-
source: {
|
|
297
|
+
source: { headingOverride: "Old page heading" }
|
|
245
298
|
},
|
|
246
299
|
{
|
|
247
300
|
documentId: "section-9",
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/site-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -107,16 +107,16 @@
|
|
|
107
107
|
],
|
|
108
108
|
"dependencies": {
|
|
109
109
|
"zod": "^4.3.6",
|
|
110
|
-
"@avocadostudio-ai/blocks": "^0.
|
|
111
|
-
"@avocadostudio-ai/preview-adapter": "^0.
|
|
112
|
-
"@avocadostudio-ai/shared": "^0.
|
|
110
|
+
"@avocadostudio-ai/blocks": "^0.4.0",
|
|
111
|
+
"@avocadostudio-ai/preview-adapter": "^0.4.0",
|
|
112
|
+
"@avocadostudio-ai/shared": "^0.4.0"
|
|
113
113
|
},
|
|
114
114
|
"peerDependencies": {
|
|
115
115
|
"next": ">=15.0.0",
|
|
116
116
|
"react": ">=19.0.0",
|
|
117
117
|
"react-dom": ">=19.0.0",
|
|
118
118
|
"better-sqlite3": ">=12.0.0",
|
|
119
|
-
"@avocadostudio-ai/orchestrator-core": "^0.
|
|
119
|
+
"@avocadostudio-ai/orchestrator-core": "^0.4.0"
|
|
120
120
|
},
|
|
121
121
|
"peerDependenciesMeta": {
|
|
122
122
|
"@avocadostudio-ai/orchestrator-core": {
|