@avocadostudio-ai/site-sdk 0.7.0 → 0.9.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 +1 -1
- package/dist/create-site-page.js +47 -84
- package/dist/draft-common.d.ts +1 -0
- package/dist/draft-common.js +13 -0
- package/dist/draft-context-core.d.ts +1 -1
- package/dist/draft-context-core.js +2 -3
- package/dist/draft-context.d.ts +1 -1
- package/dist/draft-context.js +1 -3
- package/dist/draft-routes.d.ts +7 -0
- package/dist/draft-routes.js +8 -3
- package/dist/editor-api-handler-core.d.ts +85 -0
- package/dist/editor-api-handler-core.js +101 -0
- package/dist/editor-api-handler.d.ts +7 -49
- package/dist/editor-api-handler.js +12 -66
- package/dist/editor-rewrite-core.d.ts +71 -0
- package/dist/editor-rewrite-core.js +37 -0
- package/dist/lens/register.d.ts +6 -8
- package/dist/lens/register.js +203 -29
- package/dist/lens/types.d.ts +36 -0
- package/dist/page-metadata.d.ts +21 -0
- package/dist/page-metadata.js +64 -0
- package/dist/proxy.d.ts +9 -30
- package/dist/proxy.js +16 -22
- package/dist/resolve-page-render.d.ts +94 -0
- package/dist/resolve-page-render.js +80 -0
- package/dist/routes-core.d.ts +12 -0
- package/dist/routes-core.js +24 -0
- package/package.json +25 -15
- package/dist/cli/register-notice.test.d.ts +0 -1
- package/dist/cli/register-notice.test.js +0 -21
- package/dist/draft-context-core.test.d.ts +0 -10
- package/dist/draft-context-core.test.js +0 -146
- package/dist/draft-fetch.test.d.ts +0 -1
- package/dist/draft-fetch.test.js +0 -153
- package/dist/editor-api-blocks-catalogue.test.d.ts +0 -1
- package/dist/editor-api-blocks-catalogue.test.js +0 -73
- package/dist/editor-cors.test.d.ts +0 -1
- package/dist/editor-cors.test.js +0 -85
- package/dist/editor-render.test.d.ts +0 -1
- package/dist/editor-render.test.js +0 -44
- package/dist/lens/lens.test.d.ts +0 -1
- package/dist/lens/lens.test.js +0 -221
- package/dist/lens/register.test.d.ts +0 -1
- package/dist/lens/register.test.js +0 -81
- package/dist/manifest-utils.test.d.ts +0 -1
- package/dist/manifest-utils.test.js +0 -72
- package/dist/markers.test.d.ts +0 -1
- package/dist/markers.test.js +0 -35
- package/dist/next-config.test.d.ts +0 -1
- package/dist/next-config.test.js +0 -393
- package/dist/page-metadata.test.d.ts +0 -1
- package/dist/page-metadata.test.js +0 -105
- package/dist/proxy.test.d.ts +0 -1
- package/dist/proxy.test.js +0 -182
- package/dist/publish/field-diff.test.d.ts +0 -1
- package/dist/publish/field-diff.test.js +0 -350
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from "node:assert";
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
import { getBlockMeta, blockSchemas, catalogueBlockTypes } from "@avocadostudio-ai/shared";
|
|
4
|
-
import { registerFieldTable } from "./register.js";
|
|
5
|
-
import { storyblokPrimitives } from "./storyblok.js";
|
|
6
|
-
import { sanityPrimitives } from "./sanity.js";
|
|
7
|
-
const TABLE = {
|
|
8
|
-
lens_hero: {
|
|
9
|
-
displayName: "Hero",
|
|
10
|
-
topLevel: true,
|
|
11
|
-
category: "content",
|
|
12
|
-
fields: {
|
|
13
|
-
title: { kind: "text", multiline: true },
|
|
14
|
-
body: { kind: "richtext" },
|
|
15
|
-
background: { kind: "image", label: "Background" },
|
|
16
|
-
alignment: { kind: "enum", options: ["left", "center"] },
|
|
17
|
-
uid: { kind: "text", internal: true },
|
|
18
|
-
cards: { kind: "list", itemFields: { heading: { kind: "text" }, photo: { kind: "image" } } }
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
lens_card: { displayName: "Card", topLevel: false, fields: { heading: { kind: "text" } } }
|
|
22
|
-
};
|
|
23
|
-
registerFieldTable(TABLE, { primitives: storyblokPrimitives(), narrowCatalogue: false });
|
|
24
|
-
test("every type in the table becomes a registered block type", () => {
|
|
25
|
-
assert.ok(blockSchemas.lens_hero, "without a schema every AI edit is Unknown block type");
|
|
26
|
-
assert.equal(getBlockMeta("lens_hero")?.displayName, "Hero");
|
|
27
|
-
assert.equal(getBlockMeta("lens_hero")?.category, "content");
|
|
28
|
-
});
|
|
29
|
-
/*
|
|
30
|
-
* `kind` has no JSON Schema spelling, so a table that produced only a schema
|
|
31
|
-
* would give a rich-text body the same one-line input as a headline.
|
|
32
|
-
*/
|
|
33
|
-
test("the panel metadata carries what the schema cannot say", () => {
|
|
34
|
-
const fields = getBlockMeta("lens_hero").fields;
|
|
35
|
-
assert.equal(fields.title.kind, "text");
|
|
36
|
-
assert.equal(fields.title.multiline, true);
|
|
37
|
-
assert.equal(fields.body.kind, "richtext");
|
|
38
|
-
assert.deepEqual(fields.alignment.options, ["left", "center"]);
|
|
39
|
-
});
|
|
40
|
-
test("an internal prop is declared internal rather than left undeclared", () => {
|
|
41
|
-
assert.equal(getBlockMeta("lens_hero").fields.uid.internal, true);
|
|
42
|
-
});
|
|
43
|
-
/*
|
|
44
|
-
* The projection and the panel have to agree about what an image field's two
|
|
45
|
-
* props are called, and the agreement is `imageNaming` on the pack rather than
|
|
46
|
-
* two constants in two files.
|
|
47
|
-
*/
|
|
48
|
-
test("an image registers the two props its pack projects", () => {
|
|
49
|
-
const fields = getBlockMeta("lens_hero").fields;
|
|
50
|
-
assert.equal(fields.background.kind, "image");
|
|
51
|
-
assert.equal(fields.background_alt.kind, "imageAlt");
|
|
52
|
-
assert.ok(blockSchemas.lens_hero.shape.background, "the URL prop must be in the schema");
|
|
53
|
-
assert.ok(blockSchemas.lens_hero.shape.background_alt, "and so must the alt");
|
|
54
|
-
});
|
|
55
|
-
test("a list becomes a list field, with metadata for its rows", () => {
|
|
56
|
-
const listFields = getBlockMeta("lens_hero").listFields;
|
|
57
|
-
assert.equal(listFields.cards.itemFields.heading.kind, "text");
|
|
58
|
-
assert.equal(listFields.cards.itemFields.photo.kind, "image");
|
|
59
|
-
assert.equal(listFields.cards.itemFields.photo_alt.kind, "imageAlt");
|
|
60
|
-
assert.equal("cards" in getBlockMeta("lens_hero").fields, false, "a list is not a scalar field");
|
|
61
|
-
});
|
|
62
|
-
test("the other pack names the same image field differently, and the panel follows", () => {
|
|
63
|
-
registerFieldTable({ sanity_hero: TABLE.lens_hero }, {
|
|
64
|
-
primitives: sanityPrimitives(),
|
|
65
|
-
narrowCatalogue: false
|
|
66
|
-
});
|
|
67
|
-
const fields = getBlockMeta("sanity_hero").fields;
|
|
68
|
-
assert.equal(fields.backgroundUrl.kind, "image");
|
|
69
|
-
assert.equal(fields.backgroundAlt.kind, "imageAlt");
|
|
70
|
-
assert.equal("background_alt" in fields, false);
|
|
71
|
-
});
|
|
72
|
-
/*
|
|
73
|
-
* A site that cannot render `FeatureGrid` should not be offered it: an editor
|
|
74
|
-
* who adds one gets a block that draws as nothing, with a warning in a log as
|
|
75
|
-
* the only trace.
|
|
76
|
-
*/
|
|
77
|
-
test("the catalogue narrows to the table's top-level types", () => {
|
|
78
|
-
registerFieldTable(TABLE);
|
|
79
|
-
const types = catalogueBlockTypes();
|
|
80
|
-
assert.deepEqual([...types].sort(), ["lens_hero"]);
|
|
81
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { test } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { getManifestImageFields } from "./manifest-utils.js";
|
|
4
|
-
/**
|
|
5
|
-
* A rich-text prop can be stored two ways, and an integration has to tell them
|
|
6
|
-
* apart at both boundaries — convert on read, convert back on publish. The
|
|
7
|
-
* editor's `FieldKind` cannot help: a markdown string and a ProseMirror
|
|
8
|
-
* document are both `richtext` to it. The schema can, because a document pins
|
|
9
|
-
* its `type` to the literal "doc".
|
|
10
|
-
*/
|
|
11
|
-
const MANIFEST = {
|
|
12
|
-
version: 1,
|
|
13
|
-
blocks: [
|
|
14
|
-
{
|
|
15
|
-
type: "RichText",
|
|
16
|
-
propsSchema: {
|
|
17
|
-
type: "object",
|
|
18
|
-
properties: {
|
|
19
|
-
title: { type: "string" },
|
|
20
|
-
body: { type: "string", contentMediaType: "text/markdown" }
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
type: "ArticleBody",
|
|
26
|
-
propsSchema: {
|
|
27
|
-
type: "object",
|
|
28
|
-
properties: {
|
|
29
|
-
title: { type: "string" },
|
|
30
|
-
body: { type: "object", properties: { type: { const: "doc" }, content: { type: "array" } } }
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
type: "FAQ",
|
|
36
|
-
propsSchema: {
|
|
37
|
-
type: "object",
|
|
38
|
-
properties: {
|
|
39
|
-
items: {
|
|
40
|
-
type: "array",
|
|
41
|
-
items: {
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: {
|
|
44
|
-
q: { type: "string" },
|
|
45
|
-
answer: { type: "object", properties: { type: { const: "doc" }, content: { type: "array" } } }
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
};
|
|
54
|
-
const info = getManifestImageFields(MANIFEST);
|
|
55
|
-
test("a document-valued rich-text prop is reported under documentFields", () => {
|
|
56
|
-
assert.deepEqual([...(info.documentFields.get("ArticleBody") ?? [])], ["body"]);
|
|
57
|
-
});
|
|
58
|
-
test("a markdown-string rich-text prop is not", () => {
|
|
59
|
-
assert.equal(info.documentFields.has("RichText"), false);
|
|
60
|
-
});
|
|
61
|
-
test("a document-valued field inside a list item is reported too", () => {
|
|
62
|
-
assert.deepEqual([...(info.listDocumentFields.get("FAQ")?.get("items") ?? [])], ["answer"]);
|
|
63
|
-
});
|
|
64
|
-
test("a block with no document fields is absent rather than present-and-empty", () => {
|
|
65
|
-
// Callers do `documentFields.get(type) ?? new Set()`, so an empty entry would
|
|
66
|
-
// work — but keeping the map to blocks that actually have one makes a
|
|
67
|
-
// debugging dump of it readable.
|
|
68
|
-
assert.equal(info.documentFields.has("FAQ"), false);
|
|
69
|
-
});
|
|
70
|
-
test("the derivation is cached by manifest reference", () => {
|
|
71
|
-
assert.equal(getManifestImageFields(MANIFEST), info);
|
|
72
|
-
});
|
package/dist/markers.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/markers.test.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from "node:assert";
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
import { editableProps, editableScopeProps, getPreviewWrapperProps } from "./markers.js";
|
|
4
|
-
test("editableProps defaults the label to the path and omits kind when unset", () => {
|
|
5
|
-
assert.deepEqual(editableProps("cards[0].title"), {
|
|
6
|
-
"data-editable-target": "cards[0].title",
|
|
7
|
-
"data-editable-target-label": "cards[0].title"
|
|
8
|
-
});
|
|
9
|
-
});
|
|
10
|
-
test("editableProps carries an explicit label and kind", () => {
|
|
11
|
-
assert.deepEqual(editableProps("photoUrl", { label: "Photo", kind: "image" }), {
|
|
12
|
-
"data-editable-target": "photoUrl",
|
|
13
|
-
"data-editable-target-label": "Photo",
|
|
14
|
-
"data-editable-kind": "image"
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
test("editableScopeProps writes only the scope by default", () => {
|
|
18
|
-
assert.deepEqual(editableScopeProps("items[3]"), { "data-editable-scope": "items[3]" });
|
|
19
|
-
});
|
|
20
|
-
/*
|
|
21
|
-
* A scope wants a DOM ancestor, and a list that maps its rows straight into a
|
|
22
|
-
* flex container has none — so the wrapper you add to hold it becomes the flex
|
|
23
|
-
* item and the layout moves. `display: contents` keeps the element in the tree
|
|
24
|
-
* the overlay walks and out of the box tree that lays the rows out.
|
|
25
|
-
*/
|
|
26
|
-
test("editableScopeProps can make its wrapper vanish from layout", () => {
|
|
27
|
-
assert.deepEqual(editableScopeProps("items[3]", { display: "contents" }), {
|
|
28
|
-
"data-editable-scope": "items[3]",
|
|
29
|
-
style: { display: "contents" }
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
test("getPreviewWrapperProps renders nothing outside editor mode", () => {
|
|
33
|
-
assert.deepEqual(getPreviewWrapperProps(false, "b1", "Hero"), {});
|
|
34
|
-
assert.equal(getPreviewWrapperProps(true, "b1", "Hero")["data-block-id"], "b1");
|
|
35
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/next-config.test.js
DELETED
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
import { test } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
// @ts-expect-error — plain ESM, deliberately not TypeScript (see the module's own note)
|
|
7
|
-
import { withAvocado, linkedAvocadoPackages, AVOCADO_SERVER_EXTERNALS } from "../next-config.mjs";
|
|
8
|
-
/**
|
|
9
|
-
* The list of packages to transpile is derived, not written down, because the
|
|
10
|
-
* hand-written version breaks retroactively: adding a package to Avocado, or
|
|
11
|
-
* re-exporting one from another, silently breaks every consumer that pinned the
|
|
12
|
-
* old list. That is not hypothetical — it took out every route of the Sanity
|
|
13
|
-
* example the day `@avocadostudio-ai/richtext` was added.
|
|
14
|
-
*/
|
|
15
|
-
function fixture(packages) {
|
|
16
|
-
const root = mkdtempSync(join(tmpdir(), "avocado-next-config-"));
|
|
17
|
-
for (const [name, pkg] of Object.entries(packages)) {
|
|
18
|
-
const [scope, bare] = name.split("/");
|
|
19
|
-
const dir = join(root, "node_modules", scope, bare);
|
|
20
|
-
mkdirSync(dir, { recursive: true });
|
|
21
|
-
writeFileSync(join(dir, "package.json"), JSON.stringify({ name, ...pkg }));
|
|
22
|
-
}
|
|
23
|
-
return root;
|
|
24
|
-
}
|
|
25
|
-
test("a package whose entry point is TypeScript needs transpiling", () => {
|
|
26
|
-
const root = fixture({ "@avocadostudio-ai/shared": { main: "src/index.ts" } });
|
|
27
|
-
assert.deepEqual(linkedAvocadoPackages(root), ["@avocadostudio-ai/shared"]);
|
|
28
|
-
});
|
|
29
|
-
test("a package installed from a registry does not", () => {
|
|
30
|
-
/*
|
|
31
|
-
* The shape every `@avocadostudio-ai` package actually has on npm, and the
|
|
32
|
-
* `types` field is the whole point of the fixture: `dist/index.d.ts` ends in
|
|
33
|
-
* `.ts`, so a naive TypeScript test matches it and transpiles the published
|
|
34
|
-
* package — which drags `orchestrator-core` into the bundle and fails the
|
|
35
|
-
* build on an optional peer.
|
|
36
|
-
*
|
|
37
|
-
* This test existed before that was found, with a fixture carrying `main`
|
|
38
|
-
* alone. It passed, on a package shape that does not occur on a registry.
|
|
39
|
-
*/
|
|
40
|
-
const root = fixture({
|
|
41
|
-
"@avocadostudio-ai/shared": { main: "dist/index.js", types: "dist/index.d.ts" }
|
|
42
|
-
});
|
|
43
|
-
assert.deepEqual(linkedAvocadoPackages(root), []);
|
|
44
|
-
});
|
|
45
|
-
test("a declaration file in an exports map is not a reason to transpile either", () => {
|
|
46
|
-
// The same trap one level down: an `exports` map carries its own `types`.
|
|
47
|
-
const root = fixture({
|
|
48
|
-
"@avocadostudio-ai/richtext": {
|
|
49
|
-
main: "dist/index.js",
|
|
50
|
-
exports: {
|
|
51
|
-
".": { types: "./dist/index.d.ts", import: "./dist/index.js" },
|
|
52
|
-
"./package.json": "./package.json"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
assert.deepEqual(linkedAvocadoPackages(root), []);
|
|
57
|
-
});
|
|
58
|
-
test("a linked package is still caught when its types point at source", () => {
|
|
59
|
-
// A workspace checkout points both fields at `src/`; that must still count.
|
|
60
|
-
const root = fixture({
|
|
61
|
-
"@avocadostudio-ai/shared": { main: "src/index.ts", types: "src/index.ts" }
|
|
62
|
-
});
|
|
63
|
-
assert.deepEqual(linkedAvocadoPackages(root), ["@avocadostudio-ai/shared"]);
|
|
64
|
-
});
|
|
65
|
-
test("a TypeScript entry hidden in an exports map still counts", () => {
|
|
66
|
-
const root = fixture({
|
|
67
|
-
"@avocadostudio-ai/blocks": {
|
|
68
|
-
main: "dist/index.js",
|
|
69
|
-
exports: { ".": { import: "./src/index.ts" }, "./styles.css": "./src/styles.css" }
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
assert.deepEqual(linkedAvocadoPackages(root), ["@avocadostudio-ai/blocks"]);
|
|
73
|
-
});
|
|
74
|
-
test("both scopes are scanned", () => {
|
|
75
|
-
const root = fixture({
|
|
76
|
-
"@avocadostudio-ai/richtext": { main: "src/index.ts" },
|
|
77
|
-
"@ai-site-editor/immersive-widget": { main: "src/index.ts" },
|
|
78
|
-
"@unrelated/thing": { main: "src/index.ts" }
|
|
79
|
-
});
|
|
80
|
-
assert.deepEqual(linkedAvocadoPackages(root), [
|
|
81
|
-
"@ai-site-editor/immersive-widget",
|
|
82
|
-
"@avocadostudio-ai/richtext"
|
|
83
|
-
]);
|
|
84
|
-
});
|
|
85
|
-
test("withAvocado adds what is missing and keeps what was declared", () => {
|
|
86
|
-
const root = fixture({
|
|
87
|
-
"@avocadostudio-ai/shared": { main: "src/index.ts" },
|
|
88
|
-
"@avocadostudio-ai/richtext": { main: "src/index.ts" }
|
|
89
|
-
});
|
|
90
|
-
const config = withAvocado({ transpilePackages: ["@avocadostudio-ai/shared", "some-other-package"], reactStrictMode: true }, { cwd: root, silent: true });
|
|
91
|
-
assert.deepEqual(config.transpilePackages, [
|
|
92
|
-
"@avocadostudio-ai/shared",
|
|
93
|
-
"some-other-package",
|
|
94
|
-
"@avocadostudio-ai/richtext"
|
|
95
|
-
]);
|
|
96
|
-
assert.equal(config.reactStrictMode, true, "the rest of the config is untouched");
|
|
97
|
-
});
|
|
98
|
-
test("a config that already declares everything is returned unchanged", () => {
|
|
99
|
-
/*
|
|
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
|
-
*/
|
|
105
|
-
const root = fixture({ "@avocadostudio-ai/shared": { main: "src/index.ts" } });
|
|
106
|
-
const input = { transpilePackages: ["@avocadostudio-ai/shared"] };
|
|
107
|
-
assert.equal(withAvocado(input, {
|
|
108
|
-
cwd: root,
|
|
109
|
-
silent: true,
|
|
110
|
-
images: false,
|
|
111
|
-
serverExternals: false,
|
|
112
|
-
framing: false
|
|
113
|
-
}), input);
|
|
114
|
-
});
|
|
115
|
-
test("merging image hosts does not disturb the transpile list", () => {
|
|
116
|
-
const root = fixture({ "@avocadostudio-ai/shared": { main: "src/index.ts" } });
|
|
117
|
-
const config = withAvocado({ transpilePackages: ["some-other-package"] }, { cwd: root, silent: true });
|
|
118
|
-
assert.deepEqual(config.transpilePackages, ["some-other-package", "@avocadostudio-ai/shared"]);
|
|
119
|
-
assert.ok(config.images.remotePatterns.length > 0);
|
|
120
|
-
});
|
|
121
|
-
test("a config with no transpilePackages at all gets the full list", () => {
|
|
122
|
-
const root = fixture({ "@avocadostudio-ai/shared": { main: "src/index.ts" } });
|
|
123
|
-
assert.deepEqual(withAvocado({}, { cwd: root, silent: true }).transpilePackages, ["@avocadostudio-ai/shared"]);
|
|
124
|
-
});
|
|
125
|
-
test("an unreadable manifest is skipped rather than thrown on", () => {
|
|
126
|
-
const root = mkdtempSync(join(tmpdir(), "avocado-next-config-"));
|
|
127
|
-
const dir = join(root, "node_modules", "@avocadostudio-ai", "broken");
|
|
128
|
-
mkdirSync(dir, { recursive: true });
|
|
129
|
-
writeFileSync(join(dir, "package.json"), "{ not json");
|
|
130
|
-
assert.deepEqual(linkedAvocadoPackages(root), []);
|
|
131
|
-
});
|
|
132
|
-
test("a directory that does not exist returns nothing instead of failing the build", () => {
|
|
133
|
-
// The whole point of the helper is that it cannot break `next.config`.
|
|
134
|
-
assert.deepEqual(linkedAvocadoPackages(join(tmpdir(), "definitely-not-here-9f3a")), []);
|
|
135
|
-
});
|
|
136
|
-
test("this workspace's own linked packages are found", () => {
|
|
137
|
-
const found = linkedAvocadoPackages(new URL("..", import.meta.url).pathname);
|
|
138
|
-
assert.ok(found.includes("@avocadostudio-ai/shared"), `shared missing from ${found.join(", ")}`);
|
|
139
|
-
assert.ok(found.includes("@avocadostudio-ai/blocks"), `blocks missing from ${found.join(", ")}`);
|
|
140
|
-
});
|
|
141
|
-
test("a real consumer picks up the package that broke it", () => {
|
|
142
|
-
/*
|
|
143
|
-
* pnpm links only declared dependencies, so this has to be asked from a real
|
|
144
|
-
* app rather than from this package: `site-sdk` does not depend on
|
|
145
|
-
* `richtext`, but the Sanity example does — transitively, through `shared`,
|
|
146
|
-
* which is exactly the shape that caught everyone out.
|
|
147
|
-
*/
|
|
148
|
-
const found = linkedAvocadoPackages(new URL("../../../examples/sanity-site", import.meta.url).pathname);
|
|
149
|
-
assert.ok(found.includes("@avocadostudio-ai/richtext"), `richtext missing from ${found.join(", ")}`);
|
|
150
|
-
});
|
|
151
|
-
// ── images.remotePatterns ────────────────────────────────────────────────
|
|
152
|
-
//
|
|
153
|
-
// Four apps in this repo carried the same hand-copied `remotePatterns` block,
|
|
154
|
-
// and the integration docs never mentioned it — so an integrator who followed
|
|
155
|
-
// them got a 500 the first time a user generated an image.
|
|
156
|
-
const NO_ENV = { NODE_ENV: "production" };
|
|
157
|
-
/** A tree with no linked Avocado packages, so these tests see only the image merge. */
|
|
158
|
-
const EMPTY_DIR = mkdtempSync(join(tmpdir(), "avocado-next-config-images-"));
|
|
159
|
-
test("Avocado's own image hosts are added to a config that declares none", () => {
|
|
160
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
161
|
-
const hosts = config.images.remotePatterns.map((p) => p.hostname);
|
|
162
|
-
assert.ok(hosts.includes("images.unsplash.com"), hosts.join(", "));
|
|
163
|
-
assert.ok(hosts.includes("oaidalleapiprodscus.blob.core.windows.net"), hosts.join(", "));
|
|
164
|
-
assert.ok(hosts.includes("placehold.co"), hosts.join(", "));
|
|
165
|
-
});
|
|
166
|
-
test("the app's own hosts are kept, first and unduplicated", () => {
|
|
167
|
-
const config = withAvocado({
|
|
168
|
-
images: {
|
|
169
|
-
formats: ["image/avif"],
|
|
170
|
-
remotePatterns: [
|
|
171
|
-
{ protocol: "https", hostname: "cdn.sanity.io" },
|
|
172
|
-
{ protocol: "https", hostname: "images.unsplash.com" },
|
|
173
|
-
],
|
|
174
|
-
},
|
|
175
|
-
}, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
176
|
-
const hosts = config.images.remotePatterns.map((p) => p.hostname);
|
|
177
|
-
assert.equal(hosts[0], "cdn.sanity.io", "the app's own list must keep its order");
|
|
178
|
-
assert.equal(hosts.filter((h) => h === "images.unsplash.com").length, 1, "duplicated a host the app declared");
|
|
179
|
-
// Unrelated image settings survive.
|
|
180
|
-
assert.deepEqual(config.images.formats, ["image/avif"]);
|
|
181
|
-
});
|
|
182
|
-
test("the orchestrator's own origin is allowed when it is configured", () => {
|
|
183
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, env: { ORCHESTRATOR_URL: "https://orch.example.com" } });
|
|
184
|
-
const match = config.images.remotePatterns.find((p) => p.hostname === "orch.example.com");
|
|
185
|
-
assert.ok(match, "orchestrator host missing");
|
|
186
|
-
assert.equal(match.protocol, "https");
|
|
187
|
-
});
|
|
188
|
-
test("a port on the orchestrator origin is carried through", () => {
|
|
189
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, env: { ORCHESTRATOR_URL: "http://localhost:4200" } });
|
|
190
|
-
const match = config.images.remotePatterns.find((p) => p.hostname === "localhost");
|
|
191
|
-
assert.ok(match, "orchestrator host missing");
|
|
192
|
-
assert.equal(match.port, "4200");
|
|
193
|
-
});
|
|
194
|
-
test("localhost is assumed in development but never in production", () => {
|
|
195
|
-
const dev = withAvocado({}, { cwd: EMPTY_DIR, env: { NODE_ENV: "development" } });
|
|
196
|
-
assert.ok(dev.images.remotePatterns.some((p) => p.hostname === "localhost"), "a local build should reach a local orchestrator without extra config");
|
|
197
|
-
const prod = withAvocado({}, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
198
|
-
assert.equal(prod.images.remotePatterns.some((p) => p.hostname === "localhost"), false, "a deployed site must not open its image optimizer to localhost");
|
|
199
|
-
});
|
|
200
|
-
test("a garbage orchestrator URL is ignored rather than thrown on", () => {
|
|
201
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, env: { ...NO_ENV, ORCHESTRATOR_URL: "not a url" } });
|
|
202
|
-
assert.ok(Array.isArray(config.images.remotePatterns));
|
|
203
|
-
});
|
|
204
|
-
test("images: false leaves the config's image settings untouched", () => {
|
|
205
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, images: false, env: NO_ENV });
|
|
206
|
-
assert.equal(config.images, undefined);
|
|
207
|
-
});
|
|
208
|
-
/*
|
|
209
|
-
* Server externals.
|
|
210
|
-
*
|
|
211
|
-
* `orchestrator-core` reaches native binaries and provider SDKs, and a bundler
|
|
212
|
-
* that swallows either produces a build that succeeds: `sharp` bundled crashes
|
|
213
|
-
* loading its `.node` file on the first request, and Turbopack — which resolves
|
|
214
|
-
* `await import(...)` statically — fails `Module not found` over an optional
|
|
215
|
-
* peer that was deliberately never installed.
|
|
216
|
-
*
|
|
217
|
-
* `serverExternalPackages` is the documented knob and is not sufficient alone:
|
|
218
|
-
* `transpilePackages`, which this same helper fills in, wins for a transitive
|
|
219
|
-
* dependency. Both halves are asserted here because shipping one is shipping a
|
|
220
|
-
* fix that does not hold.
|
|
221
|
-
*/
|
|
222
|
-
/** Run the merged `webpack` hook and read back what it externalised. */
|
|
223
|
-
function serverExternals(config) {
|
|
224
|
-
const result = config.webpack({}, { isServer: true });
|
|
225
|
-
const fns = (result.externals ?? []).filter((e) => typeof e === "function");
|
|
226
|
-
return (request) => {
|
|
227
|
-
for (const fn of fns) {
|
|
228
|
-
let answer;
|
|
229
|
-
fn({ request }, (_e, r) => { answer = r; });
|
|
230
|
-
if (answer)
|
|
231
|
-
return answer;
|
|
232
|
-
}
|
|
233
|
-
return undefined;
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
test("the native and provider dependencies are declared external", () => {
|
|
237
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
238
|
-
for (const name of ["better-sqlite3", "sharp", "@google/genai", "googleapis"]) {
|
|
239
|
-
assert.ok(config.serverExternalPackages.includes(name), `${name} must not be bundled into the server build`);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
test("an app's own server externals are kept, and not duplicated", () => {
|
|
243
|
-
const config = withAvocado({ serverExternalPackages: ["@sanity/client", "sharp"] }, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
244
|
-
assert.equal(config.serverExternalPackages[0], "@sanity/client", "the app's own list keeps its order");
|
|
245
|
-
assert.equal(config.serverExternalPackages.filter((n) => n === "sharp").length, 1, "a package the app already listed must not appear twice");
|
|
246
|
-
});
|
|
247
|
-
test("the webpack hook externalises the same packages, because the array alone does not", () => {
|
|
248
|
-
// `transpilePackages` overrides `serverExternalPackages` for a transitive
|
|
249
|
-
// dependency — which is exactly how orchestrator-core reaches sharp.
|
|
250
|
-
const external = serverExternals(withAvocado({}, { cwd: EMPTY_DIR, env: NO_ENV }));
|
|
251
|
-
assert.equal(external("better-sqlite3"), "commonjs better-sqlite3");
|
|
252
|
-
// A deep import has to travel with the package root, or half of it is bundled.
|
|
253
|
-
assert.equal(external("sharp/lib/libvips.js"), "commonjs sharp/lib/libvips.js");
|
|
254
|
-
// A package that merely starts with the same letters is not a match.
|
|
255
|
-
assert.equal(external("sharpen-image"), undefined);
|
|
256
|
-
assert.equal(external("react"), undefined);
|
|
257
|
-
});
|
|
258
|
-
test("the client bundle is left alone", () => {
|
|
259
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
260
|
-
assert.equal(config.webpack({}, { isServer: false }).externals, undefined);
|
|
261
|
-
});
|
|
262
|
-
test("an app's own webpack hook still runs, and still wins", () => {
|
|
263
|
-
const calls = [];
|
|
264
|
-
const config = withAvocado({
|
|
265
|
-
webpack(webpackConfig, context) {
|
|
266
|
-
calls.push(`app:${context.isServer}`);
|
|
267
|
-
return { ...webpackConfig, resolve: { extensionAlias: { ".js": [".ts"] } } };
|
|
268
|
-
},
|
|
269
|
-
}, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
270
|
-
const result = config.webpack({}, { isServer: true });
|
|
271
|
-
assert.deepEqual(calls, ["app:true"], "the app's hook must be called exactly once");
|
|
272
|
-
assert.deepEqual(result.resolve.extensionAlias, { ".js": [".ts"] }, "whatever the app's hook did survives");
|
|
273
|
-
assert.equal(serverExternals(config)("sharp"), "commonjs sharp");
|
|
274
|
-
});
|
|
275
|
-
test("serverExternals: false leaves both halves to the app", () => {
|
|
276
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, serverExternals: false, env: NO_ENV });
|
|
277
|
-
assert.equal(config.serverExternalPackages, undefined);
|
|
278
|
-
assert.equal(config.webpack, undefined, "no hook is attached at all");
|
|
279
|
-
assert.equal(config.turbopack, undefined, "and nothing is declared on its behalf");
|
|
280
|
-
});
|
|
281
|
-
/*
|
|
282
|
-
* On Next 16 Turbopack is the default, and a config with a `webpack` key and no
|
|
283
|
-
* `turbopack` key fails the build outright — measured, on 16.3.4, against a
|
|
284
|
-
* project that installed the SDK from a tarball:
|
|
285
|
-
*
|
|
286
|
-
* ERROR: This build is using Turbopack, with a `webpack` config and no
|
|
287
|
-
* `turbopack` config.
|
|
288
|
-
*
|
|
289
|
-
* Since the hook above is attached whether or not the app asked for one, the
|
|
290
|
-
* empty Turbopack config has to travel with it.
|
|
291
|
-
*/
|
|
292
|
-
test("attaching a webpack hook also declares the turbopack config Next 16 demands", () => {
|
|
293
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
294
|
-
assert.equal(typeof config.webpack, "function", "the hook is still attached");
|
|
295
|
-
assert.deepEqual(config.turbopack, {}, "an empty turbopack config must travel with it");
|
|
296
|
-
});
|
|
297
|
-
test("an app's own turbopack config is never overwritten", () => {
|
|
298
|
-
const config = withAvocado({ turbopack: { resolveAlias: { underscore: "lodash" } } }, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
299
|
-
assert.deepEqual(config.turbopack, { resolveAlias: { underscore: "lodash" } });
|
|
300
|
-
});
|
|
301
|
-
test("externals are applied even when there is nothing to transpile", () => {
|
|
302
|
-
// The transpile derivation returns early twice — on a config that already
|
|
303
|
-
// lists every linked package, and on any filesystem error. Neither may skip
|
|
304
|
-
// the externals, which is why they are applied first.
|
|
305
|
-
const config = withAvocado({}, { cwd: "/nonexistent-path-for-this-test", env: NO_ENV });
|
|
306
|
-
assert.ok(config.serverExternalPackages.includes("better-sqlite3"));
|
|
307
|
-
});
|
|
308
|
-
/*
|
|
309
|
-
* Naming orchestrator-core's *dependencies* external does not stop Turbopack
|
|
310
|
-
* walking into orchestrator-core and resolving its `await import("googleapis")`
|
|
311
|
-
* statically. Measured on Next 16.3.4 against a real tarball install: the build
|
|
312
|
-
* fails on an optional peer the project never installed, with every provider
|
|
313
|
-
* SDK already in `serverExternalPackages`. Externalising the package itself is
|
|
314
|
-
* the fix, and it is only correct when the package arrived built.
|
|
315
|
-
*/
|
|
316
|
-
test("a registry-installed orchestrator-core is externalised, not walked", () => {
|
|
317
|
-
const root = fixture({
|
|
318
|
-
"@avocadostudio-ai/orchestrator-core": { main: "dist/index.js", types: "dist/index.d.ts" }
|
|
319
|
-
});
|
|
320
|
-
const config = withAvocado({}, { cwd: root, env: NO_ENV });
|
|
321
|
-
assert.ok(config.serverExternalPackages.includes("@avocadostudio-ai/orchestrator-core"), "a built orchestrator-core must be external, or Turbopack resolves its optional peers");
|
|
322
|
-
});
|
|
323
|
-
test("a linked orchestrator-core is transpiled instead, never externalised", () => {
|
|
324
|
-
// Externalising a package whose `main` is `src/index.ts` hands Node a
|
|
325
|
-
// TypeScript file to require — a build error traded for a runtime crash.
|
|
326
|
-
const root = fixture({ "@avocadostudio-ai/orchestrator-core": { main: "src/index.ts" } });
|
|
327
|
-
const config = withAvocado({}, { cwd: root, env: NO_ENV });
|
|
328
|
-
assert.deepEqual(config.transpilePackages, ["@avocadostudio-ai/orchestrator-core"]);
|
|
329
|
-
assert.equal(config.serverExternalPackages.includes("@avocadostudio-ai/orchestrator-core"), false, "a linked checkout must be compiled with the app, not handed to Node raw");
|
|
330
|
-
});
|
|
331
|
-
test("the exported list is what the helper actually applies", () => {
|
|
332
|
-
const config = withAvocado({}, { cwd: EMPTY_DIR, env: NO_ENV });
|
|
333
|
-
assert.deepEqual(config.serverExternalPackages, AVOCADO_SERVER_EXTERNALS);
|
|
334
|
-
});
|
|
335
|
-
/*
|
|
336
|
-
* `trailingSlash: true` is the setting that made the editor unreachable. Next
|
|
337
|
-
* applies its 308 to `/api/*` too, and a browser will not follow a redirect on
|
|
338
|
-
* a CORS preflight, so every editor API call failed before it was sent — with
|
|
339
|
-
* no error naming the config line responsible.
|
|
340
|
-
*/
|
|
341
|
-
test("a trailing-slash site stops redirecting, or the editor cannot reach it", () => {
|
|
342
|
-
const root = fixture({});
|
|
343
|
-
const config = withAvocado({ trailingSlash: true }, { cwd: root, silent: true });
|
|
344
|
-
assert.equal(config.skipTrailingSlashRedirect, true);
|
|
345
|
-
assert.equal(config.trailingSlash, true, "the app's own setting is not touched — only the redirect is");
|
|
346
|
-
});
|
|
347
|
-
test("a site without trailing slashes is left exactly as it was", () => {
|
|
348
|
-
const root = fixture({});
|
|
349
|
-
const config = withAvocado({}, { cwd: root, silent: true });
|
|
350
|
-
assert.equal(config.skipTrailingSlashRedirect, undefined, "the vast majority of sites never hit this, and must not inherit the workaround");
|
|
351
|
-
});
|
|
352
|
-
test("an app that already decided about the redirect keeps its decision", () => {
|
|
353
|
-
const root = fixture({});
|
|
354
|
-
const kept = withAvocado({ trailingSlash: true, skipTrailingSlashRedirect: false }, { cwd: root, silent: true });
|
|
355
|
-
assert.equal(kept.skipTrailingSlashRedirect, false, "a site that stated this has thought about it harder than a helper can");
|
|
356
|
-
});
|
|
357
|
-
test("trailingSlash: false leaves the whole thing to the app", () => {
|
|
358
|
-
const root = fixture({});
|
|
359
|
-
const config = withAvocado({ trailingSlash: true }, { cwd: root, silent: true, trailingSlash: false });
|
|
360
|
-
assert.equal(config.skipTrailingSlashRedirect, undefined);
|
|
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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|