@avocadostudio-ai/site-sdk 0.5.1 → 0.7.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.
Files changed (41) hide show
  1. package/dist/cli/register-notice.d.ts +20 -0
  2. package/dist/cli/register-notice.js +34 -0
  3. package/dist/cli/register-notice.test.d.ts +1 -0
  4. package/dist/cli/register-notice.test.js +21 -0
  5. package/dist/cli/register.js +7 -2
  6. package/dist/draft.d.ts +1 -0
  7. package/dist/draft.js +3 -0
  8. package/dist/editor-render.d.ts +43 -0
  9. package/dist/editor-render.js +71 -0
  10. package/dist/editor-render.test.d.ts +1 -0
  11. package/dist/editor-render.test.js +44 -0
  12. package/dist/editor.d.ts +1 -1
  13. package/dist/editor.js +1 -1
  14. package/dist/lens/create-lens.d.ts +48 -0
  15. package/dist/lens/create-lens.js +349 -0
  16. package/dist/lens/index.d.ts +7 -0
  17. package/dist/lens/index.js +25 -0
  18. package/dist/lens/lens.test.d.ts +1 -0
  19. package/dist/lens/lens.test.js +221 -0
  20. package/dist/lens/register.d.ts +34 -0
  21. package/dist/lens/register.js +148 -0
  22. package/dist/lens/register.test.d.ts +1 -0
  23. package/dist/lens/register.test.js +81 -0
  24. package/dist/lens/sanity.d.ts +28 -0
  25. package/dist/lens/sanity.js +187 -0
  26. package/dist/lens/scalar-codecs.d.ts +26 -0
  27. package/dist/lens/scalar-codecs.js +92 -0
  28. package/dist/lens/storyblok.d.ts +28 -0
  29. package/dist/lens/storyblok.js +232 -0
  30. package/dist/lens/types.d.ts +243 -0
  31. package/dist/lens/types.js +28 -0
  32. package/dist/markers.d.ts +68 -0
  33. package/dist/markers.js +62 -0
  34. package/dist/markers.test.d.ts +1 -0
  35. package/dist/markers.test.js +35 -0
  36. package/dist/middleware.d.ts +1 -0
  37. package/dist/middleware.js +6 -0
  38. package/dist/proxy.d.ts +26 -0
  39. package/dist/proxy.js +88 -26
  40. package/dist/proxy.test.js +61 -2
  41. package/package.json +21 -5
@@ -1,4 +1,10 @@
1
1
  import { createEditorProxy } from "./proxy.js";
2
+ /*
3
+ * A Next 15 site reaches for this entry point, and a Next 15 site that already
4
+ * has a `middleware.ts` needs the two halves rather than the whole factory.
5
+ * Re-exported here so composing does not mean importing from two subpaths.
6
+ */
7
+ export { trailingSlashRedirect, editorPreviewRewrite } from "./proxy.js";
2
8
  /**
3
9
  * Create a Next.js middleware function that rewrites editor/draft requests
4
10
  * to a dynamic preview route, keeping the main page route fully static.
package/dist/proxy.d.ts CHANGED
@@ -93,3 +93,29 @@ export declare function createEditorProxy(options?: EditorProxyOptions): {
93
93
  matcher: string[];
94
94
  };
95
95
  };
96
+ /**
97
+ * Re-issue the trailing-slash redirect that `skipTrailingSlashRedirect` turned
98
+ * off, or `null` when the URL is already canonical.
99
+ *
100
+ * Exported because a site that already has a `middleware.ts` cannot use
101
+ * `createEditorProxy` as its whole middleware and has to compose — and the one
102
+ * line worth not writing twice is the first one below.
103
+ *
104
+ * ```ts
105
+ * export function middleware(request: NextRequest) {
106
+ * const canonical = trailingSlashRedirect(request)
107
+ * if (canonical) return canonical
108
+ * return editorPreviewRewrite(request) ?? myOwnMiddleware(request)
109
+ * }
110
+ * ```
111
+ */
112
+ export declare function trailingSlashRedirect(request: NextRequest): NextResponse | null;
113
+ /**
114
+ * Rewrite an editor request to the dynamic preview route, or `null` when the
115
+ * request is not one — which is the caller's cue to run its own middleware
116
+ * rather than a decision to pass the request through.
117
+ *
118
+ * The half of `createEditorProxy` a site with its own middleware needs; see
119
+ * {@link trailingSlashRedirect} for the composition.
120
+ */
121
+ export declare function editorPreviewRewrite(request: NextRequest, options?: Pick<EditorProxyOptions, "previewRoute" | "editorParam" | "draftCookie">): NextResponse | null;
package/dist/proxy.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { DEFAULT_PREVIEW_ROUTE, buildEditorMatcher } from "./editor-matcher.js";
3
+ import { EDITOR_RENDER_HEADER } from "./editor-render.js";
3
4
  export { DEFAULT_PREVIEW_ROUTE, buildEditorMatcher } from "./editor-matcher.js";
4
5
  /**
5
6
  * Create a Next.js proxy function that rewrites editor/draft requests
@@ -33,8 +34,6 @@ export { DEFAULT_PREVIEW_ROUTE, buildEditorMatcher } from "./editor-matcher.js";
33
34
  */
34
35
  export function createEditorProxy(options) {
35
36
  const previewRoute = options?.previewRoute ?? DEFAULT_PREVIEW_ROUTE;
36
- const editorParam = options?.editorParam ?? "__editor";
37
- const draftCookie = options?.draftCookie === undefined ? "__prerender_bypass" : options.draftCookie;
38
37
  const trailingSlash = options?.trailingSlash ?? false;
39
38
  function proxy(request) {
40
39
  /*
@@ -45,32 +44,95 @@ export function createEditorProxy(options) {
45
44
  * publishes only at `/about/`.
46
45
  */
47
46
  if (trailingSlash) {
48
- /*
49
- * Built from `request.url`, not from `request.nextUrl.clone()`. NextURL
50
- * normalises a trailing slash back *off* when it stringifies, so a
51
- * redirect built from a clone points at the URL it is trying to leave —
52
- * a redirect loop, and one that only a browser would ever have shown us.
53
- * A plain URL does no normalising. It also keeps `basePath`, which
54
- * `nextUrl.pathname` has already stripped.
55
- */
56
- const url = new URL(request.url);
57
- if (url.pathname.length > 1 && !url.pathname.endsWith("/")) {
58
- url.pathname = `${url.pathname}/`;
59
- // 308, not 307: the method is preserved *and* the redirect is
60
- // permanent, which is what Next's own trailing-slash redirect sends
61
- // and what the site's existing search rankings were built on.
62
- return NextResponse.redirect(url, 308);
63
- }
64
- }
65
- const isEditor = request.nextUrl.searchParams.get(editorParam) === "1";
66
- const hasDraftCookie = draftCookie !== false && request.cookies.has(draftCookie);
67
- if (isEditor || hasDraftCookie) {
68
- const url = request.nextUrl.clone();
69
- url.pathname = `${previewRoute}${url.pathname}`;
70
- return NextResponse.rewrite(url);
47
+ const redirect = trailingSlashRedirect(request);
48
+ if (redirect)
49
+ return redirect;
71
50
  }
72
- return NextResponse.next();
51
+ return editorPreviewRewrite(request, options) ?? NextResponse.next();
73
52
  }
74
53
  const config = { matcher: [buildEditorMatcher(previewRoute)] };
75
54
  return { proxy, config };
76
55
  }
56
+ /**
57
+ * Re-issue the trailing-slash redirect that `skipTrailingSlashRedirect` turned
58
+ * off, or `null` when the URL is already canonical.
59
+ *
60
+ * Exported because a site that already has a `middleware.ts` cannot use
61
+ * `createEditorProxy` as its whole middleware and has to compose — and the one
62
+ * line worth not writing twice is the first one below.
63
+ *
64
+ * ```ts
65
+ * export function middleware(request: NextRequest) {
66
+ * const canonical = trailingSlashRedirect(request)
67
+ * if (canonical) return canonical
68
+ * return editorPreviewRewrite(request) ?? myOwnMiddleware(request)
69
+ * }
70
+ * ```
71
+ */
72
+ export function trailingSlashRedirect(request) {
73
+ /*
74
+ * Built from `request.url`, not from `request.nextUrl.clone()`. NextURL
75
+ * normalises a trailing slash back *off* when it stringifies, so a redirect
76
+ * built from a clone points at the URL it is trying to leave — every page on
77
+ * the site enters an infinite redirect, and only in a browser: `curl`
78
+ * without `-L` sees one perfectly ordinary 308. A plain URL does no
79
+ * normalising. It also keeps `basePath`, which `nextUrl.pathname` has
80
+ * already stripped.
81
+ */
82
+ const url = new URL(request.url);
83
+ const { pathname } = url;
84
+ if (pathname.length <= 1 || pathname.endsWith("/"))
85
+ return null;
86
+ /*
87
+ * The set that must never gain a trailing slash. `createEditorProxy`'s own
88
+ * matcher already excludes all of it, so these are no-ops there — they are
89
+ * for the hand-composed case, where the site's matcher is its own and is
90
+ * usually wider. Redirecting `/api/editor/blocks` to `/api/editor/blocks/`
91
+ * here would re-create, by hand, the exact 308 that
92
+ * `skipTrailingSlashRedirect` was turned on to stop.
93
+ */
94
+ if (pathname.startsWith("/api/") || pathname.startsWith("/_next/"))
95
+ return null;
96
+ if (pathname.slice(pathname.lastIndexOf("/") + 1).includes("."))
97
+ return null;
98
+ url.pathname = `${pathname}/`;
99
+ // 308, not 307: the method is preserved *and* the redirect is permanent,
100
+ // which is what Next's own trailing-slash redirect sends and what the site's
101
+ // existing search rankings were built on.
102
+ return NextResponse.redirect(url, 308);
103
+ }
104
+ /**
105
+ * Rewrite an editor request to the dynamic preview route, or `null` when the
106
+ * request is not one — which is the caller's cue to run its own middleware
107
+ * rather than a decision to pass the request through.
108
+ *
109
+ * The half of `createEditorProxy` a site with its own middleware needs; see
110
+ * {@link trailingSlashRedirect} for the composition.
111
+ */
112
+ export function editorPreviewRewrite(request, options) {
113
+ const previewRoute = options?.previewRoute ?? DEFAULT_PREVIEW_ROUTE;
114
+ const editorParam = options?.editorParam ?? "__editor";
115
+ const draftCookie = options?.draftCookie === undefined ? "__prerender_bypass" : options.draftCookie;
116
+ const isEditor = request.nextUrl.searchParams.get(editorParam) === "1";
117
+ const hasDraftCookie = draftCookie !== false && request.cookies.has(draftCookie);
118
+ if (!isEditor && !hasDraftCookie)
119
+ return null;
120
+ const url = request.nextUrl.clone();
121
+ url.pathname = `${previewRoute}${url.pathname}`;
122
+ /*
123
+ * The one fact a layout can get at.
124
+ *
125
+ * A page learns it is being previewed from `resolveEditorContext(searchParams)`.
126
+ * A layout receives no `searchParams`, and the root layout is exactly where a
127
+ * site mounts its consent banner, analytics and tag manager — so every
128
+ * integration loads all three into the editor iframe until somebody notices
129
+ * the pageviews. Stamping the rewritten request lets `isEditorRender()` answer
130
+ * in a layout without the site threading anything down to it.
131
+ *
132
+ * Set, not appended: a client-supplied header of the same name is replaced, so
133
+ * what a layout reads here is the proxy's own answer.
134
+ */
135
+ const headers = new Headers(request.headers);
136
+ headers.set(EDITOR_RENDER_HEADER, "1");
137
+ return NextResponse.rewrite(url, { request: { headers } });
138
+ }
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { NextRequest } from "next/server";
4
- import { buildEditorMatcher, createEditorProxy, DEFAULT_PREVIEW_ROUTE } from "./proxy.js";
3
+ import { NextRequest, NextResponse } from "next/server";
4
+ import { buildEditorMatcher, createEditorProxy, editorPreviewRewrite, trailingSlashRedirect, DEFAULT_PREVIEW_ROUTE } from "./proxy.js";
5
5
  import { createEditorMiddleware } from "./middleware.js";
6
6
  /*
7
7
  * The rewrite that keeps the published route static and sends editor traffic to
@@ -121,3 +121,62 @@ test("draftCookie: false leaves Next's draft cookie to whoever else is using it"
121
121
  assert.equal(rewriteOf(proxy(request("https://site.test/about", "__prerender_bypass=abc"))), null, "a Sanity or Contentful preview must not be hijacked into Avocado's route");
122
122
  assert.equal(rewriteOf(proxy(request("https://site.test/about?__editor=1"))), "https://site.test/preview-draft/about?__editor=1", "the explicit editor parameter still works — that is the whole point of the opt-out");
123
123
  });
124
+ /*
125
+ * The two halves, on their own.
126
+ *
127
+ * A site that already has a `middleware.ts` cannot hand the whole file to
128
+ * `createEditorProxy` and has to compose — which until now meant re-deriving
129
+ * the trailing-slash redirect from the docs. One integration did, and wrote
130
+ * `request.nextUrl.clone()`, because that is what every Next example uses for
131
+ * a URL you intend to modify. A `NextURL` re-applies trailing-slash
132
+ * normalisation when it serialises, so the redirect stripped the slash it had
133
+ * just added and pointed at the URL it came from: every page on the site in an
134
+ * infinite redirect, reproducing only in a browser. `curl` without `-L` sees
135
+ * one perfectly ordinary 308, which is how it survived a session and a half.
136
+ */
137
+ test("the redirect is built from request.url, not from a NextURL that would undo it", () => {
138
+ const response = trailingSlashRedirect(request("https://site.test/bern"));
139
+ assert.equal(response?.status, 308);
140
+ assert.equal(response?.headers.get("location"), "https://site.test/bern/", "a location without the slash is the loop: the browser asks again and gets the same answer");
141
+ });
142
+ test("an already-canonical URL, the root, and a file are left alone", () => {
143
+ assert.equal(trailingSlashRedirect(request("https://site.test/bern/")), null);
144
+ assert.equal(trailingSlashRedirect(request("https://site.test/")), null, "redirecting / never terminates");
145
+ assert.equal(trailingSlashRedirect(request("https://site.test/logo.svg")), null);
146
+ });
147
+ test("the API routes are never redirected, whatever the caller's matcher says", () => {
148
+ /*
149
+ * The proxy's own matcher excludes these, but a hand-composed middleware
150
+ * brings its own matcher and it is usually wider. A slash added here
151
+ * re-creates by hand the exact 308 on `/api/editor/*` that
152
+ * `skipTrailingSlashRedirect` was turned on to stop — and the symptom is a
153
+ * CORS error that names nothing.
154
+ */
155
+ assert.equal(trailingSlashRedirect(request("https://site.test/api/editor/blocks")), null);
156
+ assert.equal(trailingSlashRedirect(request("https://site.test/_next/static/chunk")), null);
157
+ });
158
+ test("the redirect keeps the query string when called directly too", () => {
159
+ const response = trailingSlashRedirect(request("https://site.test/bern?__editor=1"));
160
+ assert.equal(response?.headers.get("location"), "https://site.test/bern/?__editor=1");
161
+ });
162
+ test("editorPreviewRewrite answers null for a visitor, so the caller runs its own middleware", () => {
163
+ assert.equal(editorPreviewRewrite(request("https://site.test/about")), null, "null is 'not mine', not 'pass this through' — a composing caller has its own work to do");
164
+ assert.equal(rewriteOf(editorPreviewRewrite(request("https://site.test/about?__editor=1"))), "https://site.test/preview-draft/about?__editor=1");
165
+ });
166
+ test("the composed form behaves like the factory it was extracted from", () => {
167
+ const { proxy } = createEditorProxy({ trailingSlash: true });
168
+ const compose = (r) => trailingSlashRedirect(r) ?? editorPreviewRewrite(r) ?? NextResponse.next();
169
+ for (const url of [
170
+ "https://site.test/bern",
171
+ "https://site.test/bern/",
172
+ "https://site.test/bern?__editor=1",
173
+ "https://site.test/bern/?__editor=1",
174
+ "https://site.test/"
175
+ ]) {
176
+ const fromFactory = proxy(request(url));
177
+ const fromParts = compose(request(url));
178
+ assert.equal(fromParts.status, fromFactory.status, url);
179
+ assert.equal(fromParts.headers.get("location"), fromFactory.headers.get("location"), url);
180
+ assert.equal(rewriteOf(fromParts), rewriteOf(fromFactory), url);
181
+ }
182
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/site-sdk",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,6 +26,21 @@
26
26
  "import": "./dist/markers.js",
27
27
  "default": "./dist/markers.js"
28
28
  },
29
+ "./lens": {
30
+ "types": "./dist/lens/index.d.ts",
31
+ "import": "./dist/lens/index.js",
32
+ "default": "./dist/lens/index.js"
33
+ },
34
+ "./lens/storyblok": {
35
+ "types": "./dist/lens/storyblok.d.ts",
36
+ "import": "./dist/lens/storyblok.js",
37
+ "default": "./dist/lens/storyblok.js"
38
+ },
39
+ "./lens/sanity": {
40
+ "types": "./dist/lens/sanity.d.ts",
41
+ "import": "./dist/lens/sanity.js",
42
+ "default": "./dist/lens/sanity.js"
43
+ },
29
44
  "./blocks": {
30
45
  "types": "./dist/blocks.d.ts",
31
46
  "import": "./dist/blocks.js",
@@ -122,16 +137,17 @@
122
137
  ],
123
138
  "dependencies": {
124
139
  "zod": "^4.3.6",
125
- "@avocadostudio-ai/blocks": "^0.5.1",
126
- "@avocadostudio-ai/preview-adapter": "^0.5.1",
127
- "@avocadostudio-ai/shared": "^0.5.1"
140
+ "@avocadostudio-ai/blocks": "^0.7.0",
141
+ "@avocadostudio-ai/richtext": "^0.7.0",
142
+ "@avocadostudio-ai/shared": "^0.7.0",
143
+ "@avocadostudio-ai/preview-adapter": "^0.7.0"
128
144
  },
129
145
  "peerDependencies": {
130
146
  "next": ">=15.0.0",
131
147
  "react": ">=19.0.0",
132
148
  "react-dom": ">=19.0.0",
133
149
  "better-sqlite3": ">=12.0.0",
134
- "@avocadostudio-ai/orchestrator-core": "^0.5.1"
150
+ "@avocadostudio-ai/orchestrator-core": "^0.7.0"
135
151
  },
136
152
  "peerDependenciesMeta": {
137
153
  "@avocadostudio-ai/orchestrator-core": {