canopycms-next 0.0.67 → 0.0.68-int.101
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/dist/adapter.js +6 -1
- package/dist/config.cjs +3 -1
- package/dist/config.mjs +3 -1
- package/dist/context-wrapper.d.ts +21 -2
- package/dist/context-wrapper.js +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/preview-page.d.ts +94 -0
- package/dist/preview-page.js +68 -0
- package/dist/preview.d.ts +32 -0
- package/dist/preview.js +24 -0
- package/dist/with-canopy.d.ts +1 -0
- package/dist/with-canopy.js +7 -0
- package/package.json +8 -3
package/dist/adapter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import { createCanopyRequestHandler, isCanopyBinaryResponse, } from 'canopycms/http';
|
|
2
|
+
import { createCanopyRequestHandler, isCanopyBinaryResponse, workerNotReadyResponse, } from 'canopycms/http';
|
|
3
3
|
import { getErrorMessage, redactCredentials, sanitizeErrorMessage } from 'canopycms/utils/error';
|
|
4
4
|
/**
|
|
5
5
|
* Only uses standard Request methods, so any Request subclass works.
|
|
@@ -39,6 +39,8 @@ function toBinaryHeaders(headers) {
|
|
|
39
39
|
result['Cache-Control'] = headers.cacheControl;
|
|
40
40
|
if (headers.etag)
|
|
41
41
|
result['ETag'] = headers.etag;
|
|
42
|
+
if (headers.location)
|
|
43
|
+
result['Location'] = headers.location;
|
|
42
44
|
return result;
|
|
43
45
|
}
|
|
44
46
|
function toNextResponse(response) {
|
|
@@ -108,6 +110,9 @@ export const createCanopyCatchAllHandler = (options) => {
|
|
|
108
110
|
// but this line reaches the server log verbatim, and a git failure message can embed a
|
|
109
111
|
// token-bearing clone URL — the one thing that must not reach a log aggregator.
|
|
110
112
|
console.error('CanopyCMS: Unhandled error in Next.js catch-all handler:', redactCredentials(message));
|
|
113
|
+
const notReady = workerNotReadyResponse(err);
|
|
114
|
+
if (notReady)
|
|
115
|
+
return toNextResponse(notReady);
|
|
111
116
|
return toNextResponse({
|
|
112
117
|
status: 500,
|
|
113
118
|
body: { ok: false, status: 500, error: sanitizeErrorMessage(message) },
|
package/dist/config.cjs
CHANGED
|
@@ -399,6 +399,7 @@ function withCanopy(nextConfig = {}, options = {}) {
|
|
|
399
399
|
])
|
|
400
400
|
];
|
|
401
401
|
const generateBuildId = nextConfig.generateBuildId ?? (options.staticBuild ? resolveStaticBuildId : void 0);
|
|
402
|
+
const env = nextConfig.trailingSlash ? { CANOPY_TRAILING_SLASH: "true", ...nextConfig.env } : void 0;
|
|
402
403
|
return {
|
|
403
404
|
...nextConfig,
|
|
404
405
|
transpilePackages: allPackages,
|
|
@@ -409,7 +410,8 @@ function withCanopy(nextConfig = {}, options = {}) {
|
|
|
409
410
|
// Spread conditionally: emitting `generateBuildId: undefined` would be a key Next has to
|
|
410
411
|
// reason about, where absence is unambiguous.
|
|
411
412
|
...generateBuildId ? { generateBuildId } : {},
|
|
412
|
-
...answerTurbopackGuard ? { turbopack: {} } : {}
|
|
413
|
+
...answerTurbopackGuard ? { turbopack: {} } : {},
|
|
414
|
+
...env ? { env } : {}
|
|
413
415
|
};
|
|
414
416
|
}
|
|
415
417
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/config.mjs
CHANGED
|
@@ -363,6 +363,7 @@ function withCanopy(nextConfig = {}, options = {}) {
|
|
|
363
363
|
])
|
|
364
364
|
];
|
|
365
365
|
const generateBuildId = nextConfig.generateBuildId ?? (options.staticBuild ? resolveStaticBuildId : void 0);
|
|
366
|
+
const env = nextConfig.trailingSlash ? { CANOPY_TRAILING_SLASH: "true", ...nextConfig.env } : void 0;
|
|
366
367
|
return {
|
|
367
368
|
...nextConfig,
|
|
368
369
|
transpilePackages: allPackages,
|
|
@@ -373,7 +374,8 @@ function withCanopy(nextConfig = {}, options = {}) {
|
|
|
373
374
|
// Spread conditionally: emitting `generateBuildId: undefined` would be a key Next has to
|
|
374
375
|
// reason about, where absence is unambiguous.
|
|
375
376
|
...generateBuildId ? { generateBuildId } : {},
|
|
376
|
-
...answerTurbopackGuard ? { turbopack: {} } : {}
|
|
377
|
+
...answerTurbopackGuard ? { turbopack: {} } : {},
|
|
378
|
+
...env ? { env } : {}
|
|
377
379
|
};
|
|
378
380
|
}
|
|
379
381
|
export {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type CanopyContext, type CanopyBuildContext, type CanopyServices, type SeoFieldLocation } from 'canopycms/server';
|
|
2
2
|
import type { CanopyConfig, AuthPlugin, FieldConfig } from 'canopycms';
|
|
3
3
|
import type { Metadata, MetadataRoute } from 'next';
|
|
4
|
+
import type { ReactElement } from 'react';
|
|
4
5
|
import { createCanopyCatchAllHandler } from './adapter.js';
|
|
6
|
+
import { type CreatePreviewPageOptions, type PreviewPageProps } from './preview-page.js';
|
|
5
7
|
import { type EntryToMetadataOptions, type GenerateContentSitemapOptions, type GenerateContentStaticParamsOptions } from './static.js';
|
|
6
8
|
/**
|
|
7
9
|
* Merge a context-wide default SEO field location (`createNextCanopyContext({ seo })`) with a
|
|
@@ -101,8 +103,8 @@ export interface NextCanopyContextResult {
|
|
|
101
103
|
*
|
|
102
104
|
* At build time this reads filesystem-direct; at request time it uses the branch-aware,
|
|
103
105
|
* **ACL-enforced** runtime context, so entries the current user cannot read are omitted.
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
+
* Pass the preview iframe's `?branch=` as `branch` on an index page so a content
|
|
107
|
+
* branch's entries show in preview — see `CanopyContext['listEntries']`.
|
|
106
108
|
*/
|
|
107
109
|
listEntries: CanopyContext['listEntries'];
|
|
108
110
|
/**
|
|
@@ -132,6 +134,23 @@ export interface NextCanopyContextResult {
|
|
|
132
134
|
* (see that function's doc); pass `fields`/`group` on a given call to override just that call.
|
|
133
135
|
*/
|
|
134
136
|
entryToMetadata: (entryData: unknown, options?: EntryToMetadataOptions) => Metadata;
|
|
137
|
+
/**
|
|
138
|
+
* The page for the preview route the editor's `previewPrefix` names, for a site whose public
|
|
139
|
+
* pages are a static export. Mount it as `app/<prefix>/[[...path]]/page.server.tsx`, so only the
|
|
140
|
+
* CMS build has it:
|
|
141
|
+
*
|
|
142
|
+
* ```tsx
|
|
143
|
+
* export default createPreviewPage({ views: { post: PostPreview, doc: DocPreview } })
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* Each view is made by `withCanopyPreview` (`canopycms-next/preview`) in a `'use client'` module.
|
|
147
|
+
* It renders each entry from the `?branch=` the editor names, through `views[entryType]` with
|
|
148
|
+
* the live draft. Reads are request-scoped and ACL-checked, and an anonymous request or anything
|
|
149
|
+
* not readable is a 404.
|
|
150
|
+
* A view that needs more server reads is `previewView({ view, load })`: `load` runs after the
|
|
151
|
+
* entry is read and its result arrives as the view's `extras` prop.
|
|
152
|
+
*/
|
|
153
|
+
createPreviewPage: (options: CreatePreviewPageOptions) => (props: PreviewPageProps) => Promise<ReactElement>;
|
|
135
154
|
/** API catch-all route handler */
|
|
136
155
|
handler: ReturnType<typeof createCanopyCatchAllHandler>;
|
|
137
156
|
/** Underlying services (rarely needed directly) */
|
package/dist/context-wrapper.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createCanopyContext, isBuildMode, startDevContentWatcher, createCanopyS
|
|
|
5
5
|
import { assertAuthPluginAllowedForMode } from 'canopycms/auth';
|
|
6
6
|
import { CachingAuthPlugin, FileBasedAuthCache } from 'canopycms/auth/cache';
|
|
7
7
|
import { createCanopyCatchAllHandler } from './adapter.js';
|
|
8
|
+
import { createPreviewPageFor, } from './preview-page.js';
|
|
8
9
|
import { collectStaticParams, entryToMetadata as entryToMetadataCore, generateContentSitemap, } from './static.js';
|
|
9
10
|
/**
|
|
10
11
|
* Merge a context-wide default SEO field location (`createNextCanopyContext({ seo })`) with a
|
|
@@ -175,8 +176,12 @@ export async function createNextCanopyContext(options) {
|
|
|
175
176
|
//
|
|
176
177
|
// No base-branch context resolution needed here: request-time content reads
|
|
177
178
|
// (buildContentTree/listEntries/read) already provision the base/active branch via
|
|
178
|
-
// loadOrCreateBranchContext (see context.ts's
|
|
179
|
+
// loadOrCreateBranchContext (see context.ts's resolveListingSource); build-time reads never
|
|
179
180
|
// provision, they read the checkout.
|
|
181
|
+
//
|
|
182
|
+
// Failures propagate to the page's error boundary and never degrade to a user: a
|
|
183
|
+
// RemoteNotReadyError (the worker has not created the remote yet) names the cause in the
|
|
184
|
+
// server log, and only an HTTP route can answer it with a 503.
|
|
180
185
|
const extractUser = async () => {
|
|
181
186
|
const headersList = await headers();
|
|
182
187
|
const authResult = await authPlugin.authenticate(headersList);
|
|
@@ -267,6 +272,7 @@ export async function createNextCanopyContext(options) {
|
|
|
267
272
|
generateContentStaticParams,
|
|
268
273
|
generateContentSitemap: boundGenerateContentSitemap,
|
|
269
274
|
entryToMetadata: boundEntryToMetadata,
|
|
275
|
+
createPreviewPage: (previewOptions) => createPreviewPageFor(getCanopy, previewOptions, options.config.deployedAs),
|
|
270
276
|
handler,
|
|
271
277
|
services,
|
|
272
278
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createCanopyCatchAllHandler, wrapNextRequest, type CanopyNextOptions } from './adapter.js';
|
|
2
2
|
export { createNextCanopyContext, type NextCanopyOptions, type NextCanopyContextResult, } from './context-wrapper.js';
|
|
3
|
+
export { previewView, type CreatePreviewPageOptions, type PreviewEntry, type PreviewLoadContext, type PreviewPageProps, type PreviewViewWithLoader, } from './preview-page.js';
|
|
3
4
|
export { collectStaticParams, generateContentSitemap, entryToMetadata, type GenerateContentStaticParamsOptions, type GenerateContentSitemapOptions, type EntryToMetadataOptions, type SitemapExtraUrl, } from './static.js';
|
|
4
5
|
export { createMockAuthPlugin, createRejectingAuthPlugin } from './test-utils.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createCanopyCatchAllHandler, wrapNextRequest } from './adapter.js';
|
|
2
2
|
export { createNextCanopyContext, } from './context-wrapper.js';
|
|
3
|
+
export { previewView, } from './preview-page.js';
|
|
3
4
|
export { collectStaticParams, generateContentSitemap, entryToMetadata, } from './static.js';
|
|
4
5
|
export { createMockAuthPlugin, createRejectingAuthPlugin } from './test-utils.js';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
import type { CanopyConfig } from 'canopycms';
|
|
3
|
+
import type { CanopyContext } from 'canopycms/server';
|
|
4
|
+
import type { CanopyPreviewProps } from './preview.js';
|
|
5
|
+
type ReadEntry = NonNullable<Awaited<ReturnType<CanopyContext['readByUrlPath']>>>;
|
|
6
|
+
/**
|
|
7
|
+
* The entry a `load` receives. It carries no `meta.physicalPath`, so a loader that returns its
|
|
8
|
+
* whole `entry` cannot send the server's filesystem layout to the browser.
|
|
9
|
+
*/
|
|
10
|
+
export interface PreviewEntry {
|
|
11
|
+
data: unknown;
|
|
12
|
+
path: string;
|
|
13
|
+
entryType: string;
|
|
14
|
+
entryId?: ReadEntry['meta']['entryId'];
|
|
15
|
+
}
|
|
16
|
+
/** What a `load` receives. */
|
|
17
|
+
export interface PreviewLoadContext {
|
|
18
|
+
/** The entry the route is rendering, as read from the requested branch. */
|
|
19
|
+
entry: PreviewEntry;
|
|
20
|
+
/** The request-scoped context the entry was read through: authenticated and ACL-checked. */
|
|
21
|
+
canopy: CanopyContext;
|
|
22
|
+
/** The `?branch=` the editor named, to pass on to further reads. */
|
|
23
|
+
branch: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A view and the server loader that feeds its `extras` prop. Make one with `previewView`.
|
|
27
|
+
*
|
|
28
|
+
* `load` runs on the server, once per request, after the entry is read and its view found, so an
|
|
29
|
+
* entry type with no view is a 404 without running it. It may throw, or call `notFound()`: either
|
|
30
|
+
* rejects the page rather than rendering.
|
|
31
|
+
*
|
|
32
|
+
* Its result reaches the browser like any RSC prop, so it must be RSC-serializable: plain data
|
|
33
|
+
* or React elements. A server-rendered element such as `<RelatedPosts />` is fine, which lets a
|
|
34
|
+
* view reuse a server component for a section that need not update live. The result is a
|
|
35
|
+
* snapshot of the request and does not follow the editor's live draft; only `data` does.
|
|
36
|
+
*/
|
|
37
|
+
export interface PreviewViewWithLoader<X> {
|
|
38
|
+
view: (props: CanopyPreviewProps<never, X>) => ReactNode;
|
|
39
|
+
load: (ctx: PreviewLoadContext) => X | Promise<X>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The form `views` holds, with `X` erased. `view` takes `extras?: never`, so a view of any `X`
|
|
43
|
+
* is assignable (its parameter is only wider), and `load` returns `unknown`, so any loader is
|
|
44
|
+
* too. That drops the check that `load`'s result is what `view` expects, which `previewView`
|
|
45
|
+
* restores where each pair is written.
|
|
46
|
+
*/
|
|
47
|
+
interface ErasedPreviewViewWithLoader {
|
|
48
|
+
view: (props: CanopyPreviewProps<never, never>) => ReactNode;
|
|
49
|
+
load: (ctx: PreviewLoadContext) => unknown;
|
|
50
|
+
}
|
|
51
|
+
type PreviewView = (props: CanopyPreviewProps<never>) => ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* Pairs a view with its loader, so TypeScript checks that `load`'s result is what the view's
|
|
54
|
+
* `extras` prop expects. A record of views cannot infer a distinct `X` per key, so this identity
|
|
55
|
+
* function infers it for each entry on its own.
|
|
56
|
+
*/
|
|
57
|
+
export declare function previewView<X>(entry: PreviewViewWithLoader<X>): PreviewViewWithLoader<X>;
|
|
58
|
+
export interface CreatePreviewPageOptions {
|
|
59
|
+
/**
|
|
60
|
+
* The view for each entry type the preview route serves, keyed by entry type name, each made by
|
|
61
|
+
* `withCanopyPreview` (`canopycms-next/preview`) in a `'use client'` module, or a
|
|
62
|
+
* `previewView({ view, load })` pair when the view needs more than the entry. An entry whose
|
|
63
|
+
* type has no view is a 404.
|
|
64
|
+
*/
|
|
65
|
+
views: Record<string, PreviewView | ErasedPreviewViewWithLoader>;
|
|
66
|
+
/**
|
|
67
|
+
* Editor origin to trust, for an editor on another origin. Defaults to the page's own. A
|
|
68
|
+
* request without the viewer's session is a 404, and a `SameSite=Lax` session cookie reaches a
|
|
69
|
+
* framed page only from an editor on the same site, so a cross-site editor previews nothing.
|
|
70
|
+
*/
|
|
71
|
+
editorOrigin?: string;
|
|
72
|
+
}
|
|
73
|
+
/** The props Next passes a `[[...path]]` page. */
|
|
74
|
+
export interface PreviewPageProps {
|
|
75
|
+
params: Promise<{
|
|
76
|
+
path?: string[];
|
|
77
|
+
}>;
|
|
78
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The page for a `[[...path]]` route mounted at `editor.previewPrefix`: it reads the entry at
|
|
82
|
+
* the route's path from the `?branch=` the editor names and renders its view.
|
|
83
|
+
*
|
|
84
|
+
* The read goes through the request-scoped `getCanopy()`, never the build context: it is
|
|
85
|
+
* authenticated and ACL-checked, and a branch other than the active one is only loaded, never
|
|
86
|
+
* created. An anonymous request, a missing, unreadable or malformed branch, and a path no entry
|
|
87
|
+
* publishes, are all a 404, so a hidden branch looks the same as a missing one.
|
|
88
|
+
*
|
|
89
|
+
* On a `deployedAs: 'static'` deployment every request is a 404: reads there skip access checks,
|
|
90
|
+
* so the route would show any branch to anyone.
|
|
91
|
+
* @internal Exported for tests.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createPreviewPageFor(getCanopy: () => Promise<CanopyContext>, options: CreatePreviewPageOptions, deployedAs?: CanopyConfig['deployedAs']): (props: PreviewPageProps) => Promise<ReactElement>;
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { notFound } from 'next/navigation';
|
|
3
|
+
/**
|
|
4
|
+
* Pairs a view with its loader, so TypeScript checks that `load`'s result is what the view's
|
|
5
|
+
* `extras` prop expects. A record of views cannot infer a distinct `X` per key, so this identity
|
|
6
|
+
* function infers it for each entry on its own.
|
|
7
|
+
*/
|
|
8
|
+
export function previewView(entry) {
|
|
9
|
+
return entry;
|
|
10
|
+
}
|
|
11
|
+
function hasLoader(entry) {
|
|
12
|
+
return (typeof entry === 'object' &&
|
|
13
|
+
entry !== null &&
|
|
14
|
+
Object.prototype.hasOwnProperty.call(entry, 'load'));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The page for a `[[...path]]` route mounted at `editor.previewPrefix`: it reads the entry at
|
|
18
|
+
* the route's path from the `?branch=` the editor names and renders its view.
|
|
19
|
+
*
|
|
20
|
+
* The read goes through the request-scoped `getCanopy()`, never the build context: it is
|
|
21
|
+
* authenticated and ACL-checked, and a branch other than the active one is only loaded, never
|
|
22
|
+
* created. An anonymous request, a missing, unreadable or malformed branch, and a path no entry
|
|
23
|
+
* publishes, are all a 404, so a hidden branch looks the same as a missing one.
|
|
24
|
+
*
|
|
25
|
+
* On a `deployedAs: 'static'` deployment every request is a 404: reads there skip access checks,
|
|
26
|
+
* so the route would show any branch to anyone.
|
|
27
|
+
* @internal Exported for tests.
|
|
28
|
+
*/
|
|
29
|
+
export function createPreviewPageFor(getCanopy, options, deployedAs = 'server') {
|
|
30
|
+
return async function CanopyPreviewPage({ params, searchParams }) {
|
|
31
|
+
if (deployedAs === 'static')
|
|
32
|
+
notFound();
|
|
33
|
+
const [{ path = [] }, query] = await Promise.all([params, searchParams]);
|
|
34
|
+
const branch = query.branch;
|
|
35
|
+
if (Array.isArray(branch))
|
|
36
|
+
notFound();
|
|
37
|
+
const canopy = await getCanopy();
|
|
38
|
+
// Branch ACLs can grant an anonymous user the base branch, and this route is deployed like the
|
|
39
|
+
// editor's routes (canopycms-cdk's `attachTo` `previewPrefix`), outside any site-wide gate.
|
|
40
|
+
if (canopy.user.type === 'anonymous')
|
|
41
|
+
notFound();
|
|
42
|
+
const result = await canopy.readByUrlPath(`/${path.join('/')}`, { branch });
|
|
43
|
+
if (!result)
|
|
44
|
+
notFound();
|
|
45
|
+
const { entryType } = result.meta;
|
|
46
|
+
const viewEntry = Object.prototype.hasOwnProperty.call(options.views, entryType)
|
|
47
|
+
? options.views[entryType]
|
|
48
|
+
: undefined;
|
|
49
|
+
if (!viewEntry)
|
|
50
|
+
notFound();
|
|
51
|
+
if (!hasLoader(viewEntry)) {
|
|
52
|
+
const View = viewEntry;
|
|
53
|
+
return _jsx(View, { initialData: result.data, editorOrigin: options.editorOrigin });
|
|
54
|
+
}
|
|
55
|
+
const { meta } = result;
|
|
56
|
+
const entry = {
|
|
57
|
+
data: result.data,
|
|
58
|
+
path: result.path,
|
|
59
|
+
entryType: meta.entryType,
|
|
60
|
+
entryId: meta.entryId,
|
|
61
|
+
};
|
|
62
|
+
const extras = await viewEntry.load({ entry, canopy, branch });
|
|
63
|
+
// The record erases `view` to `extras?: never`, so it cannot check `load`'s result against the
|
|
64
|
+
// view; `previewView` does, where a pair is written.
|
|
65
|
+
const View = viewEntry.view;
|
|
66
|
+
return _jsx(View, { initialData: result.data, editorOrigin: options.editorOrigin, extras: extras });
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
import { useCanopyPreview } from 'canopycms/preview';
|
|
3
|
+
/** What a preview view renders from: the editor's live draft, plus `useCanopyPreview`'s helpers. */
|
|
4
|
+
export type CanopyPreviewViewProps<T> = ReturnType<typeof useCanopyPreview<T>>;
|
|
5
|
+
/** The props of a view wrapped by `withCanopyPreview`. */
|
|
6
|
+
export interface CanopyPreviewProps<T, X = undefined> {
|
|
7
|
+
/** The saved entry data, rendered until the editor sends a draft. */
|
|
8
|
+
initialData: T;
|
|
9
|
+
/** Editor origin to trust, for an editor on another origin. Defaults to this page's own. */
|
|
10
|
+
editorOrigin?: string;
|
|
11
|
+
/**
|
|
12
|
+
* What the route's `load` returned (see `previewView`), handed to the view untouched as its
|
|
13
|
+
* `extras` prop. Absent on a public page and for a view with no loader.
|
|
14
|
+
*/
|
|
15
|
+
extras?: X;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a view so it renders the editor's live draft of `initialData`, through `useCanopyPreview`;
|
|
19
|
+
* outside an editor frame it renders `initialData` unchanged.
|
|
20
|
+
*
|
|
21
|
+
* Call it in your own `'use client'` module and pass the result to `createPreviewPage`'s `views`,
|
|
22
|
+
* or render it on a public page. The wrapping lives in your module, not in `createPreviewPage`,
|
|
23
|
+
* because Next ships every client module a page's server code imports: one imported by the
|
|
24
|
+
* context would put the preview bridge in every page that reads content.
|
|
25
|
+
*
|
|
26
|
+
* The returned type is a call signature rather than `ComponentType`, whose `propTypes` would make
|
|
27
|
+
* a view for one content type unassignable to a map of views for many. It returns `ReactElement`,
|
|
28
|
+
* which JSX accepts on every supported TypeScript, where `ReactNode` needs `JSX.ElementType`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withCanopyPreview<T, X = undefined>(View: (props: CanopyPreviewViewProps<T> & {
|
|
31
|
+
extras: X | undefined;
|
|
32
|
+
}) => ReactNode): (props: CanopyPreviewProps<T, X>) => ReactElement;
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCanopyPreview } from 'canopycms/preview';
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a view so it renders the editor's live draft of `initialData`, through `useCanopyPreview`;
|
|
6
|
+
* outside an editor frame it renders `initialData` unchanged.
|
|
7
|
+
*
|
|
8
|
+
* Call it in your own `'use client'` module and pass the result to `createPreviewPage`'s `views`,
|
|
9
|
+
* or render it on a public page. The wrapping lives in your module, not in `createPreviewPage`,
|
|
10
|
+
* because Next ships every client module a page's server code imports: one imported by the
|
|
11
|
+
* context would put the preview bridge in every page that reads content.
|
|
12
|
+
*
|
|
13
|
+
* The returned type is a call signature rather than `ComponentType`, whose `propTypes` would make
|
|
14
|
+
* a view for one content type unassignable to a map of views for many. It returns `ReactElement`,
|
|
15
|
+
* which JSX accepts on every supported TypeScript, where `ReactNode` needs `JSX.ElementType`.
|
|
16
|
+
*/
|
|
17
|
+
export function withCanopyPreview(View) {
|
|
18
|
+
return function CanopyPreview({ initialData, editorOrigin, extras }) {
|
|
19
|
+
const preview = useCanopyPreview({ initialData, editorOrigin });
|
|
20
|
+
// `extras` is a prop of its own, never spread into the hook's result, so it cannot shadow
|
|
21
|
+
// `data` or `fieldProps`.
|
|
22
|
+
return _jsx(View, { ...preview, extras: extras });
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/with-canopy.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface WithCanopyOptions {
|
|
|
40
40
|
* (kept under `experimental` on Next 13/14 or legacy spellings; your own includes stay), so a
|
|
41
41
|
* Turbopack `output: 'standalone'` server can load sharp — Next's tracing misses that library
|
|
42
42
|
* for sharp 0.35 (doesn't fix webpack builds); warns if the directory or version can't be found.
|
|
43
|
+
* - `trailingSlash: true` sets `env.CANOPY_TRAILING_SLASH`, so the editor's requests skip Next's 308.
|
|
43
44
|
* - On Next 16+, sets `turbopack: {}` when your config has neither `turbopack` nor `webpack` and
|
|
44
45
|
* the Next version can be read — Next 16 defaults to Turbopack and exits when a config exports
|
|
45
46
|
* `webpack` (the React aliases above add one) with no `turbopack` key; a `turbopack` you set is
|
package/dist/with-canopy.js
CHANGED
|
@@ -337,6 +337,7 @@ function sharpTracingConfig(nextConfig, options) {
|
|
|
337
337
|
* (kept under `experimental` on Next 13/14 or legacy spellings; your own includes stay), so a
|
|
338
338
|
* Turbopack `output: 'standalone'` server can load sharp — Next's tracing misses that library
|
|
339
339
|
* for sharp 0.35 (doesn't fix webpack builds); warns if the directory or version can't be found.
|
|
340
|
+
* - `trailingSlash: true` sets `env.CANOPY_TRAILING_SLASH`, so the editor's requests skip Next's 308.
|
|
340
341
|
* - On Next 16+, sets `turbopack: {}` when your config has neither `turbopack` nor `webpack` and
|
|
341
342
|
* the Next version can be read — Next 16 defaults to Turbopack and exits when a config exports
|
|
342
343
|
* `webpack` (the React aliases above add one) with no `turbopack` key; a `turbopack` you set is
|
|
@@ -448,6 +449,11 @@ export function withCanopy(nextConfig = {}, options = {}) {
|
|
|
448
449
|
// verbatim (`next/dist/build/generate-build-id.js`) — which is what makes a hex tree hash usable
|
|
449
450
|
// as a build id at all. Do not "helpfully" route this through the fallback.
|
|
450
451
|
const generateBuildId = nextConfig.generateBuildId ?? (options.staticBuild ? resolveStaticBuildId : undefined);
|
|
452
|
+
// Next substitutes `env` into every bundle at build time, where the API client and the editor's
|
|
453
|
+
// preview URLs read it (`readTrailingSlashEnv` in canopycms's `utils/url-prefix.ts`).
|
|
454
|
+
const env = nextConfig.trailingSlash
|
|
455
|
+
? { CANOPY_TRAILING_SLASH: 'true', ...nextConfig.env }
|
|
456
|
+
: undefined;
|
|
451
457
|
return {
|
|
452
458
|
...nextConfig,
|
|
453
459
|
transpilePackages: allPackages,
|
|
@@ -459,5 +465,6 @@ export function withCanopy(nextConfig = {}, options = {}) {
|
|
|
459
465
|
// reason about, where absence is unambiguous.
|
|
460
466
|
...(generateBuildId ? { generateBuildId } : {}),
|
|
461
467
|
...(answerTurbopackGuard ? { turbopack: {} } : {}),
|
|
468
|
+
...(env ? { env } : {}),
|
|
462
469
|
};
|
|
463
470
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopycms-next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.68-int.101",
|
|
4
4
|
"description": "Next.js adapter for CanopyCMS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"import": "./dist/client.js",
|
|
24
24
|
"require": "./dist/client.js"
|
|
25
25
|
},
|
|
26
|
+
"./preview": {
|
|
27
|
+
"types": "./dist/preview.d.ts",
|
|
28
|
+
"import": "./dist/preview.js",
|
|
29
|
+
"require": "./dist/preview.js"
|
|
30
|
+
},
|
|
26
31
|
"./config": {
|
|
27
32
|
"types": "./dist/config.d.ts",
|
|
28
33
|
"import": "./dist/config.mjs",
|
|
@@ -38,7 +43,7 @@
|
|
|
38
43
|
"peerDependencies": {
|
|
39
44
|
"next": "^13.5.7 || ^14.2.25 || ^15.5.21 || >=16.0.0 <16.2.0 || >=16.3.0 <17.0.0",
|
|
40
45
|
"react": "^18.0.0 || ^19.0.0",
|
|
41
|
-
"canopycms": "0.0.
|
|
46
|
+
"canopycms": "0.0.68-int.101"
|
|
42
47
|
},
|
|
43
48
|
"devDependencies": {
|
|
44
49
|
"@types/node": "^22.9.0",
|
|
@@ -48,7 +53,7 @@
|
|
|
48
53
|
"react": "^18.3.1",
|
|
49
54
|
"typescript": "^5.6.3",
|
|
50
55
|
"vitest": "^4.1.0",
|
|
51
|
-
"canopycms": "0.0.
|
|
56
|
+
"canopycms": "0.0.68-int.101"
|
|
52
57
|
},
|
|
53
58
|
"scripts": {
|
|
54
59
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/add-js-extensions.mjs dist && esbuild src/config.ts --bundle --platform=node --packages=external --external:next --format=cjs --outfile=dist/config.cjs && esbuild src/config.ts --bundle --platform=node --packages=external --external:next --format=esm --outfile=dist/config.mjs",
|