canopycms-next 0.0.67-int.90 → 0.0.67-int.91
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.d.ts +0 -8
- package/dist/adapter.js +11 -31
- package/dist/client.d.ts +5 -7
- package/dist/client.js +8 -11
- package/dist/context-wrapper.d.ts +14 -20
- package/dist/context-wrapper.js +22 -38
- package/dist/sharp-tracing.d.ts +8 -1
- package/dist/sharp-tracing.js +25 -52
- package/dist/static.d.ts +30 -56
- package/dist/static.js +2 -7
- package/dist/test-utils.d.ts +0 -4
- package/dist/test-utils.js +0 -4
- package/dist/with-canopy.d.ts +20 -46
- package/dist/with-canopy.js +20 -51
- package/package.json +3 -3
package/dist/adapter.d.ts
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import { type CanopyHandlerOptions, type CanopyRequest } from 'canopycms/http';
|
|
2
2
|
/**
|
|
3
|
-
* Options for creating a Canopy Next.js handler.
|
|
4
3
|
* Same as core CanopyHandlerOptions - re-exported for convenience.
|
|
5
4
|
*/
|
|
6
5
|
export type CanopyNextOptions = CanopyHandlerOptions;
|
|
7
6
|
/**
|
|
8
|
-
* Wrap a standard Request (or NextRequest) to implement the CanopyRequest interface.
|
|
9
7
|
* Only uses standard Request methods, so any Request subclass works.
|
|
10
8
|
*/
|
|
11
9
|
export declare function wrapNextRequest(req: Request): CanopyRequest;
|
|
12
10
|
/**
|
|
13
11
|
* Catch-all Next.js handler for a single API route (e.g., /api/canopycms/[...canopycms]).
|
|
14
12
|
*
|
|
15
|
-
* This is a thin adapter that:
|
|
16
|
-
* 1. Converts NextRequest to CanopyRequest
|
|
17
|
-
* 2. Extracts path segments from Next.js params
|
|
18
|
-
* 3. Delegates to the core handler
|
|
19
|
-
* 4. Converts CanopyResponse to NextResponse
|
|
20
|
-
*
|
|
21
13
|
* @example
|
|
22
14
|
* ```ts
|
|
23
15
|
* // app/api/canopycms/[...canopycms]/route.ts
|
package/dist/adapter.js
CHANGED
|
@@ -2,7 +2,6 @@ import { NextResponse } from 'next/server';
|
|
|
2
2
|
import { createCanopyRequestHandler, isCanopyBinaryResponse, } from 'canopycms/http';
|
|
3
3
|
import { getErrorMessage, redactCredentials, sanitizeErrorMessage } from 'canopycms/utils/error';
|
|
4
4
|
/**
|
|
5
|
-
* Wrap a standard Request (or NextRequest) to implement the CanopyRequest interface.
|
|
6
5
|
* Only uses standard Request methods, so any Request subclass works.
|
|
7
6
|
*/
|
|
8
7
|
export function wrapNextRequest(req) {
|
|
@@ -30,10 +29,6 @@ export function wrapNextRequest(req) {
|
|
|
30
29
|
},
|
|
31
30
|
};
|
|
32
31
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Map a CanopyBinaryResponse's framework-agnostic header fields onto the
|
|
35
|
-
* real HTTP header names, omitting any that weren't set.
|
|
36
|
-
*/
|
|
37
32
|
function toBinaryHeaders(headers) {
|
|
38
33
|
const result = {};
|
|
39
34
|
if (headers.contentType)
|
|
@@ -46,17 +41,12 @@ function toBinaryHeaders(headers) {
|
|
|
46
41
|
result['ETag'] = headers.etag;
|
|
47
42
|
return result;
|
|
48
43
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Convert a CanopyResponse (or CanopyBinaryResponse) to a NextResponse.
|
|
51
|
-
*/
|
|
52
44
|
function toNextResponse(response) {
|
|
53
45
|
if (isCanopyBinaryResponse(response)) {
|
|
54
|
-
// A Uint8Array can be backed by an arbitrary ArrayBufferLike (e.g. a
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// view so this type-checks without an unsafe cast; ReadableStream
|
|
59
|
-
// bodies pass through unchanged.
|
|
46
|
+
// A Uint8Array can be backed by an arbitrary ArrayBufferLike (e.g. a Node Buffer), which
|
|
47
|
+
// doesn't structurally satisfy the DOM lib's BodyInit/ArrayBufferView. Copying through the
|
|
48
|
+
// typed-array constructor yields a plain ArrayBuffer-backed view that type-checks without an
|
|
49
|
+
// unsafe cast; ReadableStream bodies pass through unchanged.
|
|
60
50
|
const body = response.body instanceof Uint8Array ? new Uint8Array(response.body) : response.body;
|
|
61
51
|
return new NextResponse(body, {
|
|
62
52
|
status: response.status,
|
|
@@ -69,7 +59,6 @@ function toNextResponse(response) {
|
|
|
69
59
|
});
|
|
70
60
|
}
|
|
71
61
|
/**
|
|
72
|
-
* Extract path segments from Next.js catch-all route params.
|
|
73
62
|
* Handles both Next.js 14 (direct object) and Next.js 15 (Promise) params.
|
|
74
63
|
*/
|
|
75
64
|
async function extractPathSegments(ctx) {
|
|
@@ -81,12 +70,6 @@ async function extractPathSegments(ctx) {
|
|
|
81
70
|
/**
|
|
82
71
|
* Catch-all Next.js handler for a single API route (e.g., /api/canopycms/[...canopycms]).
|
|
83
72
|
*
|
|
84
|
-
* This is a thin adapter that:
|
|
85
|
-
* 1. Converts NextRequest to CanopyRequest
|
|
86
|
-
* 2. Extracts path segments from Next.js params
|
|
87
|
-
* 3. Delegates to the core handler
|
|
88
|
-
* 4. Converts CanopyResponse to NextResponse
|
|
89
|
-
*
|
|
90
73
|
* @example
|
|
91
74
|
* ```ts
|
|
92
75
|
* // app/api/canopycms/[...canopycms]/route.ts
|
|
@@ -116,17 +99,14 @@ export const createCanopyCatchAllHandler = (options) => {
|
|
|
116
99
|
return toNextResponse(response);
|
|
117
100
|
}
|
|
118
101
|
catch (err) {
|
|
119
|
-
// Defense-in-depth (API-C1): coreHandler already guards itself with a
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// break the uniform { ok, status, error } envelope the editor expects.
|
|
102
|
+
// Defense-in-depth (API-C1): coreHandler already guards itself with a top-level try/catch,
|
|
103
|
+
// but this adapter also wraps request conversion, so a failure there can never escape as
|
|
104
|
+
// Next's generic unhandled-error 500, which would break the uniform { ok, status, error }
|
|
105
|
+
// envelope the editor expects.
|
|
124
106
|
const message = getErrorMessage(err);
|
|
125
|
-
// Redacted before logging, not just before responding
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// one thing that must not reach a log aggregator. Every failure surface
|
|
129
|
-
// in the worker already redacts; this was the outlier.
|
|
107
|
+
// Redacted before logging, not just before responding: the HTTP body is already sanitized,
|
|
108
|
+
// but this line reaches the server log verbatim, and a git failure message can embed a
|
|
109
|
+
// token-bearing clone URL — the one thing that must not reach a log aggregator.
|
|
130
110
|
console.error('CanopyCMS: Unhandled error in Next.js catch-all handler:', redactCredentials(message));
|
|
131
111
|
return toNextResponse({
|
|
132
112
|
status: 500,
|
package/dist/client.d.ts
CHANGED
|
@@ -3,13 +3,11 @@ import type { CanopyClientConfig, CustomFieldRenderers } from 'canopycms/client'
|
|
|
3
3
|
* Next.js-specific wrapper for CanopyEditorPage that automatically reads
|
|
4
4
|
* URL search params (branch, entry) using Next.js's useSearchParams hook.
|
|
5
5
|
*
|
|
6
|
-
* @param customRenderers Optional per-field-type render overrides, forwarded
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
11
|
-
* unreachable from the path every adopter uses, which is an extension point
|
|
12
|
-
* in name only.
|
|
6
|
+
* @param customRenderers Optional per-field-type render overrides, forwarded to
|
|
7
|
+
* `CanopyEditorPage`. Forwarded (not dropped) because Next is the primary target: this wrapper
|
|
8
|
+
* is the entrypoint every adopter imports (the README's Quick Start scaffolds it), so accepting
|
|
9
|
+
* the argument only on core `CanopyEditorPage` would make the extension point unreachable from
|
|
10
|
+
* the path adopters actually use.
|
|
13
11
|
*
|
|
14
12
|
* @example
|
|
15
13
|
* ```tsx
|
package/dist/client.js
CHANGED
|
@@ -7,13 +7,11 @@ import { CanopyEditorPage } from 'canopycms/client';
|
|
|
7
7
|
* Next.js-specific wrapper for CanopyEditorPage that automatically reads
|
|
8
8
|
* URL search params (branch, entry) using Next.js's useSearchParams hook.
|
|
9
9
|
*
|
|
10
|
-
* @param customRenderers Optional per-field-type render overrides, forwarded
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
15
|
-
* unreachable from the path every adopter uses, which is an extension point
|
|
16
|
-
* in name only.
|
|
10
|
+
* @param customRenderers Optional per-field-type render overrides, forwarded to
|
|
11
|
+
* `CanopyEditorPage`. Forwarded (not dropped) because Next is the primary target: this wrapper
|
|
12
|
+
* is the entrypoint every adopter imports (the README's Quick Start scaffolds it), so accepting
|
|
13
|
+
* the argument only on core `CanopyEditorPage` would make the extension point unreachable from
|
|
14
|
+
* the path adopters actually use.
|
|
17
15
|
*
|
|
18
16
|
* @example
|
|
19
17
|
* ```tsx
|
|
@@ -31,10 +29,9 @@ import { CanopyEditorPage } from 'canopycms/client';
|
|
|
31
29
|
*/
|
|
32
30
|
export const NextCanopyEditorPage = (config, customRenderers) => {
|
|
33
31
|
const CorePage = CanopyEditorPage(config, customRenderers);
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// boundary here so adopters don't have to wrap their /edit page themselves.
|
|
32
|
+
// Next.js 15 requires useSearchParams() consumers to sit under a <Suspense> boundary, or static
|
|
33
|
+
// rendering errors with "useSearchParams() should be wrapped in a suspense boundary" — provided
|
|
34
|
+
// here so adopters don't have to wrap their /edit page themselves.
|
|
38
35
|
function EditorWithSearchParams() {
|
|
39
36
|
const urlSearchParams = useSearchParams();
|
|
40
37
|
const searchParams = {
|
|
@@ -21,6 +21,7 @@ import { type EntryToMetadataOptions, type GenerateContentSitemapOptions, type G
|
|
|
21
21
|
* keys, so a caller who omits them still produces an object whose `fields`/`group` are present but
|
|
22
22
|
* `undefined`, and spreading that would silently clobber the default. Checking each key explicitly
|
|
23
23
|
* avoids that trap. Exported for direct unit testing.
|
|
24
|
+
* @internal Exported for tests.
|
|
24
25
|
*/
|
|
25
26
|
export declare function mergeSeoFieldLocation(contextDefault: SeoFieldLocation | undefined, callOverride: SeoFieldLocation | undefined): SeoFieldLocation;
|
|
26
27
|
export interface NextCanopyOptions {
|
|
@@ -29,13 +30,11 @@ export interface NextCanopyOptions {
|
|
|
29
30
|
authPlugin?: AuthPlugin;
|
|
30
31
|
entrySchemaRegistry: Record<string, readonly FieldConfig[]>;
|
|
31
32
|
/**
|
|
32
|
-
* Where the SEO fields live (`fields` name overrides, and/or a nested `group`)
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* A per-call `seo`/`fields`/`group` option on either helper still overrides this default for
|
|
38
|
-
* that one call.
|
|
33
|
+
* Where the SEO fields live (`fields` name overrides, and/or a nested `group`) — shared by both
|
|
34
|
+
* `generateContentSitemap`'s `noindex` exclusion and `entryToMetadata`'s field extraction; see
|
|
35
|
+
* `mergeSeoFieldLocation` for why one shared default matters. Omit for the flat (inline-group)
|
|
36
|
+
* convention, the default on both the schema side (`defineSeoFieldGroup()`) and here. A per-call
|
|
37
|
+
* `seo`/`fields`/`group` option on either helper overrides this default for that one call.
|
|
39
38
|
*/
|
|
40
39
|
seo?: SeoFieldLocation;
|
|
41
40
|
}
|
|
@@ -55,6 +54,7 @@ export interface NextCanopyOptions {
|
|
|
55
54
|
* that ambiguity is gone (generateStaticParams is build-only; request-time build-context use is the
|
|
56
55
|
* footgun). Why not on `static` deployments: they skip ACLs everywhere by design, so there's nothing
|
|
57
56
|
* to leak. `CANOPY_BUILD_MODE=true` marks non-Next static generation as build phase.
|
|
57
|
+
* @internal Exported for tests.
|
|
58
58
|
*/
|
|
59
59
|
export declare function guardBuildContext(buildCtx: CanopyBuildContext, config: CanopyConfig): CanopyBuildContext;
|
|
60
60
|
export interface NextCanopyContextResult {
|
|
@@ -125,13 +125,11 @@ export interface NextCanopyContextResult {
|
|
|
125
125
|
generateContentSitemap: (options: GenerateContentSitemapOptions) => Promise<MetadataRoute.Sitemap>;
|
|
126
126
|
/**
|
|
127
127
|
* Map an entry's SEO fields onto a Next `Metadata` for `generateMetadata`. Re-exposed here so a
|
|
128
|
-
* page module has one CanopyCMS import (your `lib/canopy.ts`) rather than two.
|
|
128
|
+
* page module has one CanopyCMS import (your `lib/canopy.ts`) rather than two. Pure mapping —
|
|
129
|
+
* touches no context.
|
|
129
130
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* they cannot silently disagree about where `noindex` (or a renamed field) lives. Pass
|
|
133
|
-
* `fields`/`group` on a given call to override just that call. This is a pure
|
|
134
|
-
* mapping and touches no context.
|
|
131
|
+
* Shares its SEO field location with `generateContentSitemap` above via `mergeSeoFieldLocation`
|
|
132
|
+
* (see that function's doc); pass `fields`/`group` on a given call to override just that call.
|
|
135
133
|
*/
|
|
136
134
|
entryToMetadata: (entryData: unknown, options?: EntryToMetadataOptions) => Metadata;
|
|
137
135
|
/** API catch-all route handler */
|
|
@@ -140,12 +138,8 @@ export interface NextCanopyContextResult {
|
|
|
140
138
|
services: CanopyServices;
|
|
141
139
|
}
|
|
142
140
|
/**
|
|
143
|
-
* Create Next.js-specific wrapper around core context
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* In prod/dev mode, if the provided authPlugin implements verifyTokenOnly(),
|
|
148
|
-
* it is automatically wrapped with CachingAuthPlugin + FileBasedAuthCache so that
|
|
149
|
-
* auth works without network access (Lambda in prod, local in dev). The cache is populated by the worker daemon.
|
|
141
|
+
* Create the Next.js-specific wrapper around the core context: React `cache()` per-request
|
|
142
|
+
* memoization plus the API handler. Async because it loads .collection.json meta files (see the
|
|
143
|
+
* authPlugin auto-wrap below for how prod/dev sets up networkless auth).
|
|
150
144
|
*/
|
|
151
145
|
export declare function createNextCanopyContext(options: NextCanopyOptions): Promise<NextCanopyContextResult>;
|
package/dist/context-wrapper.js
CHANGED
|
@@ -24,6 +24,7 @@ import { collectStaticParams, entryToMetadata as entryToMetadataCore, generateCo
|
|
|
24
24
|
* keys, so a caller who omits them still produces an object whose `fields`/`group` are present but
|
|
25
25
|
* `undefined`, and spreading that would silently clobber the default. Checking each key explicitly
|
|
26
26
|
* avoids that trap. Exported for direct unit testing.
|
|
27
|
+
* @internal Exported for tests.
|
|
27
28
|
*/
|
|
28
29
|
export function mergeSeoFieldLocation(contextDefault, callOverride) {
|
|
29
30
|
const merged = { ...contextDefault };
|
|
@@ -35,7 +36,6 @@ export function mergeSeoFieldLocation(contextDefault, callOverride) {
|
|
|
35
36
|
}
|
|
36
37
|
let warnedStaticMode = false;
|
|
37
38
|
/**
|
|
38
|
-
* Stub auth plugin for static deployments where no real auth is needed.
|
|
39
39
|
* Returns unauthenticated for all requests — API routes will return 401.
|
|
40
40
|
*
|
|
41
41
|
* `verifiesCredentials: true` is set here even though this plugin verifies nothing: it is an
|
|
@@ -78,6 +78,7 @@ const staticDeployAuthPlugin = {
|
|
|
78
78
|
* that ambiguity is gone (generateStaticParams is build-only; request-time build-context use is the
|
|
79
79
|
* footgun). Why not on `static` deployments: they skip ACLs everywhere by design, so there's nothing
|
|
80
80
|
* to leak. `CANOPY_BUILD_MODE=true` marks non-Next static generation as build phase.
|
|
81
|
+
* @internal Exported for tests.
|
|
81
82
|
*/
|
|
82
83
|
export function guardBuildContext(buildCtx, config) {
|
|
83
84
|
const assertBuildPhase = (method) => {
|
|
@@ -98,8 +99,8 @@ export function guardBuildContext(buildCtx, config) {
|
|
|
98
99
|
assertBuildPhase('listEntries');
|
|
99
100
|
return buildCtx.listEntries(options);
|
|
100
101
|
},
|
|
101
|
-
// Return types inferred from the core build context
|
|
102
|
-
//
|
|
102
|
+
// Return types inferred from the core build context, which carries meta.physicalPath;
|
|
103
|
+
// the returned object is validated against CanopyBuildContext.
|
|
103
104
|
read: (input) => {
|
|
104
105
|
assertBuildPhase('read');
|
|
105
106
|
return buildCtx.read(input);
|
|
@@ -111,16 +112,11 @@ export function guardBuildContext(buildCtx, config) {
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
/**
|
|
114
|
-
* Create Next.js-specific wrapper around core context
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* In prod/dev mode, if the provided authPlugin implements verifyTokenOnly(),
|
|
119
|
-
* it is automatically wrapped with CachingAuthPlugin + FileBasedAuthCache so that
|
|
120
|
-
* auth works without network access (Lambda in prod, local in dev). The cache is populated by the worker daemon.
|
|
115
|
+
* Create the Next.js-specific wrapper around the core context: React `cache()` per-request
|
|
116
|
+
* memoization plus the API handler. Async because it loads .collection.json meta files (see the
|
|
117
|
+
* authPlugin auto-wrap below for how prod/dev sets up networkless auth).
|
|
121
118
|
*/
|
|
122
119
|
export async function createNextCanopyContext(options) {
|
|
123
|
-
// Fail fast: authPlugin is required for server deployments
|
|
124
120
|
if (options.config.deployedAs !== 'static' && !options.authPlugin) {
|
|
125
121
|
throw new Error('CanopyCMS: authPlugin is required when deployedAs is "server". ' +
|
|
126
122
|
'Set deployedAs: "static" in your canopy config, or provide an authPlugin.');
|
|
@@ -136,10 +132,10 @@ export async function createNextCanopyContext(options) {
|
|
|
136
132
|
'Do not set deployedAs: "static" in a server deployment.');
|
|
137
133
|
warnedStaticMode = true;
|
|
138
134
|
}
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
135
|
+
// Auto-wrap with CachingAuthPlugin + FileBasedAuthCache in prod/dev when the plugin implements
|
|
136
|
+
// verifyTokenOnly(), so auth stays networkless (required for Lambda in prod, consistent in dev)
|
|
137
|
+
// without exposing caching internals to adopters; the cache is populated by the worker daemon.
|
|
138
|
+
// Static deployments use the stub above, which always returns 401.
|
|
143
139
|
const { mode } = options.config;
|
|
144
140
|
const authPlugin = (() => {
|
|
145
141
|
if (!options.authPlugin)
|
|
@@ -156,7 +152,6 @@ export async function createNextCanopyContext(options) {
|
|
|
156
152
|
}
|
|
157
153
|
return options.authPlugin;
|
|
158
154
|
})();
|
|
159
|
-
// Create services ONCE at initialization
|
|
160
155
|
const services = await createCanopyServices(options.config, {
|
|
161
156
|
entrySchemaRegistry: options.entrySchemaRegistry,
|
|
162
157
|
});
|
|
@@ -172,22 +167,16 @@ export async function createNextCanopyContext(options) {
|
|
|
172
167
|
if (options.config.mode === 'dev' && !isBuildMode()) {
|
|
173
168
|
startDevContentWatcher(services, { mode: options.config.dev?.contentSync });
|
|
174
169
|
}
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// a previous copy here loaded groups from the base branch content clone
|
|
181
|
-
// (via loadBranchContext), which nothing in the product ever writes
|
|
182
|
-
// groups.json into, so group-based privileges never took effect.
|
|
170
|
+
// Passes Next.js headers to the auth plugin, then resolves internal groups and authorization
|
|
171
|
+
// through the shared "authenticate -> load internal groups -> merge" pipeline (resolveCanopyUser,
|
|
172
|
+
// in canopycms core), keeping this in lockstep with http/handler.ts's API-layer equivalent rather
|
|
173
|
+
// than re-implementing it. Must not source groups from a base-branch content clone — nothing in
|
|
174
|
+
// the product writes groups.json there, so group-based privileges depend on this shared pipeline.
|
|
183
175
|
//
|
|
184
|
-
// No base-branch context resolution here
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// loadOrCreateBranchContext (see context.ts's resolveSchemaContext; build-time
|
|
189
|
-
// reads never provision, they read the checkout), so dropping it here removes
|
|
190
|
-
// a redundant per-request EFS round-trip rather than losing provisioning.
|
|
176
|
+
// No base-branch context resolution needed here: request-time content reads
|
|
177
|
+
// (buildContentTree/listEntries/read) already provision the base/active branch via
|
|
178
|
+
// loadOrCreateBranchContext (see context.ts's resolveSchemaContext); build-time reads never
|
|
179
|
+
// provision, they read the checkout.
|
|
191
180
|
const extractUser = async () => {
|
|
192
181
|
const headersList = await headers();
|
|
193
182
|
const authResult = await authPlugin.authenticate(headersList);
|
|
@@ -197,12 +186,10 @@ export async function createNextCanopyContext(options) {
|
|
|
197
186
|
bootstrapAdminIds: services.bootstrapAdminIds,
|
|
198
187
|
});
|
|
199
188
|
};
|
|
200
|
-
// Create core context with pre-created services (framework-agnostic)
|
|
201
189
|
const coreContext = createCanopyContext({
|
|
202
190
|
services,
|
|
203
191
|
extractUser,
|
|
204
192
|
});
|
|
205
|
-
// Wrap with React cache() for per-request caching
|
|
206
193
|
const getCanopy = cache(() => {
|
|
207
194
|
return coreContext.getContext();
|
|
208
195
|
});
|
|
@@ -228,7 +215,7 @@ export async function createNextCanopyContext(options) {
|
|
|
228
215
|
return buildContextPromise;
|
|
229
216
|
};
|
|
230
217
|
// Phase-selecting helpers: build context during static generation, branch-aware runtime at request
|
|
231
|
-
// time. Lets page code resolve content without hand-picking the admin build context
|
|
218
|
+
// time. Lets page code resolve content without hand-picking the admin build context.
|
|
232
219
|
const readByUrlPath = async (urlPath, opts) => {
|
|
233
220
|
const ctx = isBuildMode() ? await getCanopyForBuild() : await getCanopy();
|
|
234
221
|
return ctx.readByUrlPath(urlPath, opts);
|
|
@@ -251,9 +238,7 @@ export async function createNextCanopyContext(options) {
|
|
|
251
238
|
// static export, and on a prod `server` deployment guardBuildContext throws if it is reached at
|
|
252
239
|
// request time. Binding it here keeps the admin context out of the route module either way.
|
|
253
240
|
//
|
|
254
|
-
//
|
|
255
|
-
// createNextCanopyContext) through the SAME mergeSeoFieldLocation call, so the noindex exclusion
|
|
256
|
-
// here and entryToMetadata's noindex/field read cannot independently drift out of sync.
|
|
241
|
+
// SEO field location is shared with boundEntryToMetadata via mergeSeoFieldLocation — see its doc.
|
|
257
242
|
const boundGenerateContentSitemap = async (callOptions) => {
|
|
258
243
|
return generateContentSitemap(await getCanopyForBuild(), {
|
|
259
244
|
...callOptions,
|
|
@@ -267,7 +252,6 @@ export async function createNextCanopyContext(options) {
|
|
|
267
252
|
});
|
|
268
253
|
return entryToMetadataCore(entryData, { ...callOptions, ...seoLocation });
|
|
269
254
|
};
|
|
270
|
-
// Create API handler using same services
|
|
271
255
|
const handler = createCanopyCatchAllHandler({
|
|
272
256
|
...options,
|
|
273
257
|
authPlugin,
|
package/dist/sharp-tracing.d.ts
CHANGED
|
@@ -16,7 +16,12 @@ export interface SharpTracingInput {
|
|
|
16
16
|
lockfileRoot?: 'outermost' | 'closest';
|
|
17
17
|
}
|
|
18
18
|
export interface SharpTracingResult {
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Project-relative POSIX globs, one per native-library `lib/` directory found. Turbopack matches
|
|
21
|
+
* includes in unanchored "contains" mode and follows symlinked directories rather than confining
|
|
22
|
+
* its walk to the include's own directory, so this costs roughly 5 s of compile time on one
|
|
23
|
+
* measured build.
|
|
24
|
+
*/
|
|
20
25
|
includes: string[];
|
|
21
26
|
/** Why nothing was found. Set exactly when `includes` is empty. */
|
|
22
27
|
problem?: string;
|
|
@@ -28,6 +33,7 @@ export interface SharpTracingResult {
|
|
|
28
33
|
* of the packages this module needs defeat it:
|
|
29
34
|
* - a `canopycms` published without a `require` condition throws ERR_PACKAGE_PATH_NOT_EXPORTED;
|
|
30
35
|
* - sharp exports no `./package.json` at all.
|
|
36
|
+
* @internal Exported for tests.
|
|
31
37
|
*/
|
|
32
38
|
export declare function findInstalledPackage(fromDir: string, name: string): string | null;
|
|
33
39
|
/** The major version of the `next` installed for `projectDir`, or null if it cannot be read. */
|
|
@@ -46,6 +52,7 @@ export declare function hasNextConfig(dir: string): boolean;
|
|
|
46
52
|
*
|
|
47
53
|
* Next resolves a relative configured root against the working directory; `withCanopy` only calls
|
|
48
54
|
* this when that directory is the project dir.
|
|
55
|
+
* @internal Exported for tests.
|
|
49
56
|
*/
|
|
50
57
|
export declare function resolveTracingRoot(input: SharpTracingInput): string;
|
|
51
58
|
/**
|
package/dist/sharp-tracing.js
CHANGED
|
@@ -1,57 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Finds sharp's libvips shared library so `withCanopy` can add it to Next.js output file tracing.
|
|
3
3
|
*
|
|
4
|
-
* **The gap.** sharp loads libvips with `dlopen
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* **The gap.** sharp loads libvips with `dlopen` through its native binding's rpath, so a tracer
|
|
5
|
+
* that follows imports never sees it. Next's JS tracer has a sharp-specific handler, but it only
|
|
6
|
+
* fires on sharp 0.34's entry point `sharp/lib/index.js`; sharp 0.35 ships `dist/index.{cjs,mjs}`
|
|
7
|
+
* instead, so it's missed — on a Next 16.1.7 Turbopack standalone build this leaves
|
|
8
|
+
* `lib/libvips-cpp.so.*` untraced, and the built server fails to load sharp with ERR_DLOPEN_FAILED
|
|
9
|
+
* (`.claude/future-tasks/resolved/cms-image-build-epic.md`, "The tracer misses the `.so`"). A
|
|
10
|
+
* webpack build under pnpm fails differently: Next 15.5.21's webpack bundles sharp's JS into a
|
|
11
|
+
* server chunk, so the bundled copy cannot reach its native binding regardless of this include
|
|
12
|
+
* (root cause in `.claude/future-tasks/webpack-standalone-sharp-bundled.md`). An npm install and
|
|
13
|
+
* Next 16's `next build --webpack` have not been checked. Where the JS tracer does trace the
|
|
14
|
+
* library itself, this include merges into one Set rather than duplicating it (upstream:
|
|
15
|
+
* next.js#97973, sharp #4567/#4543 — fixed there, this module goes).
|
|
9
16
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* sharp
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* (`next/dist/build/collect-build-traces.js:505` in 16.1.7).
|
|
22
|
-
*
|
|
23
|
-
* Upstream: https://github.com/vercel/next.js/issues/97973 (open). On sharp's side, see
|
|
24
|
-
* https://github.com/lovell/sharp/issues/4567 and https://github.com/lovell/sharp/issues/4543.
|
|
25
|
-
* Once a Next release traces the library itself, this module and its call in `withCanopy` should go.
|
|
26
|
-
*
|
|
27
|
-
* **Finding the directory.** Packages are located by walking up the `node_modules` hierarchy, the
|
|
28
|
-
* way a bundler resolves a bare specifier, never by globbing one package manager's layout. The walk:
|
|
29
|
-
* - start from the `canopycms` the project resolves;
|
|
30
|
-
* - find that copy's `sharp`, and also any `sharp` the project resolves directly;
|
|
31
|
-
* - find each `@img/sharp-libvips-*` optional dependency, from sharp and from each installed native
|
|
32
|
-
* binding (the binding's rpath looks for libvips beside the binding, so that is the copy it loads);
|
|
33
|
-
* - an installed binding that lists no libvips package has to carry its native library itself, so
|
|
34
|
-
* take the binding instead (sharp 0.35.3's win32-x64 and win32-arm64 bindings list none, and ship
|
|
35
|
-
* `libvips-42.dll` and `libvips-cpp-8.18.3.dll` in their own `lib/`);
|
|
36
|
-
* - take each package's real `lib/` directory.
|
|
37
|
-
*
|
|
38
|
-
* **Real paths, not symlinks.** The includes name real directories. Under pnpm, the binding's rpath
|
|
39
|
-
* reaches libvips through a sibling symlink whose target is that real directory, and the images
|
|
40
|
-
* this was verified on load the library from there.
|
|
41
|
-
*
|
|
42
|
-
* **What the include does NOT cover.** Under pnpm, the binding reaches the library only through its
|
|
43
|
-
* sibling symlink `.pnpm/@img+sharp-<platform>@<version>/node_modules/@img/sharp-libvips-<platform>`.
|
|
44
|
-
* (Checked with `otool` on sharp 0.35.3's darwin-arm64 binding: none of its other rpath entries
|
|
45
|
-
* resolves in a pnpm layout.) Next 16.1.7's Turbopack traces that symlink already, and the
|
|
46
|
-
* standalone copy recreates it. If a Next upgrade stops tracing it, the library is present but
|
|
47
|
-
* unreachable. Only a smoke test that loads sharp inside the built image would notice.
|
|
48
|
-
*
|
|
49
|
-
* **Cost.** Turbopack matches includes in "contains" mode (`crates/next-api/src/nft_json.rs:312` at
|
|
50
|
-
* v16.1.7), which leaves the pattern unanchored (`turbo-tasks-fs/src/globset.rs:104-114`). Its
|
|
51
|
-
* directory walk is therefore not confined to the directory an include names, and it follows
|
|
52
|
-
* symlinked directories (`read_glob.rs:87-99`). Measured on one Next 16.1.7 app's standalone build,
|
|
53
|
-
* the include added about 5 s of compile time (12.8 s to 17.9 s, mean of three runs; recorded in
|
|
54
|
-
* `.claude/future-tasks/upstream-next-sharp-tracing-recheck.md`).
|
|
17
|
+
* **Locating the directory.** Walk up the `node_modules` hierarchy the way a bundler resolves a
|
|
18
|
+
* bare specifier, never glob one package manager's layout: from the `canopycms` the project
|
|
19
|
+
* resolves (and any `sharp` it resolves directly), find each `@img/sharp-libvips-*` dependency of
|
|
20
|
+
* sharp and of each installed native binding — a binding's rpath looks for libvips beside itself,
|
|
21
|
+
* so that sibling copy is the one it loads. A binding with no libvips dependency carries its own
|
|
22
|
+
* native library instead (true of sharp 0.35.3's win32 bindings). Each package's real `lib/`
|
|
23
|
+
* directory becomes one include — never the `.pnpm` symlink beside it, which is the only path a
|
|
24
|
+
* binding's rpath actually resolves in a pnpm layout (checked with `otool`), and which Next
|
|
25
|
+
* 16.1.7's Turbopack already traces (the standalone build recreates it). If a future Next stops
|
|
26
|
+
* tracing that symlink, the library stays present but unreachable — only a smoke test that loads
|
|
27
|
+
* sharp inside the built image would notice.
|
|
55
28
|
*/
|
|
56
29
|
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
57
30
|
import path from 'node:path';
|
|
@@ -102,8 +75,6 @@ function messageOf(err) {
|
|
|
102
75
|
return String(err);
|
|
103
76
|
}
|
|
104
77
|
/**
|
|
105
|
-
* Whether `candidate` is a regular file.
|
|
106
|
-
*
|
|
107
78
|
* Any failed stat answers false instead of throwing: a missing path, a race with a delete, or a
|
|
108
79
|
* permission error. `hasNextConfig` calls this outside any `try`, and it runs inside the adopter's
|
|
109
80
|
* `next.config`.
|
|
@@ -172,6 +143,7 @@ function nodeModulesLookupDirs(fromDir) {
|
|
|
172
143
|
* of the packages this module needs defeat it:
|
|
173
144
|
* - a `canopycms` published without a `require` condition throws ERR_PACKAGE_PATH_NOT_EXPORTED;
|
|
174
145
|
* - sharp exports no `./package.json` at all.
|
|
146
|
+
* @internal Exported for tests.
|
|
175
147
|
*/
|
|
176
148
|
export function findInstalledPackage(fromDir, name) {
|
|
177
149
|
for (const lookupDir of nodeModulesLookupDirs(fromDir)) {
|
|
@@ -221,6 +193,7 @@ function findLockFileUpwards(startDir, names) {
|
|
|
221
193
|
*
|
|
222
194
|
* Next resolves a relative configured root against the working directory; `withCanopy` only calls
|
|
223
195
|
* this when that directory is the project dir.
|
|
196
|
+
* @internal Exported for tests.
|
|
224
197
|
*/
|
|
225
198
|
export function resolveTracingRoot(input) {
|
|
226
199
|
const configured = input.outputFileTracingRoot || input.turbopackRoot;
|
package/dist/static.d.ts
CHANGED
|
@@ -55,28 +55,19 @@ type SitemapItem = MetadataRoute.Sitemap[number];
|
|
|
55
55
|
* `generateContentSitemap` applies `isNoindexEntry` and a default `lastModified` only while
|
|
56
56
|
* walking real entries; this list is appended afterwards and passes through both:
|
|
57
57
|
*
|
|
58
|
-
* - **No `noindex` gate.**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* you are responsible for not listing it when that entry is marked `noindex`. Marking the
|
|
62
|
-
* entry does not remove the extra URL.
|
|
58
|
+
* - **No `noindex` gate.** There is no entry data to check it against — if you use `extraUrls`
|
|
59
|
+
* to re-advertise a real entry under a different path, you are responsible for not listing it
|
|
60
|
+
* when that entry is `noindex`; marking the entry does not remove the extra URL.
|
|
63
61
|
* - **No `lastModified` fallback.** The per-entry branch defaults to `entry.updatedAt`; here,
|
|
64
|
-
* omitting `lastModified` means the URL
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* `
|
|
68
|
-
* use
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* 1. **Model the entry so its natural `urlPath` IS the URL you serve.** An `index` entry collapses
|
|
74
|
-
* onto its collection's path, and a root `index` entry onto `/`. Nothing to keep in sync. This
|
|
75
|
-
* is what the reference app does now, which is why its sitemap passes neither option.
|
|
76
|
-
* 2. **Failing that, use `pathFor`** (see `GenerateContentSitemapOptions`), which overrides the
|
|
77
|
-
* URL while keeping the entry inside the walk — so the `noindex` gate, the `lastModified`
|
|
78
|
-
* default and `priority` all still apply.
|
|
79
|
-
* 3. **Keep `extraUrls` for what its name says:** URLs with no entry behind them at all.
|
|
62
|
+
* omitting `lastModified` means the URL ships with no date at all.
|
|
63
|
+
*
|
|
64
|
+
* Prefer, in order: (1) model the entry so its natural `urlPath` IS the URL you serve — an
|
|
65
|
+
* `index` entry collapses onto its collection's path, a root `index` onto `/`, nothing to keep
|
|
66
|
+
* in sync; (2) failing that, use `pathFor` (see `GenerateContentSitemapOptions`), which overrides
|
|
67
|
+
* the URL while keeping the entry inside the walk, so the `noindex` gate, `lastModified` default
|
|
68
|
+
* and `priority` all still apply; (3) reach for `extraUrls` only for URLs with no entry behind
|
|
69
|
+
* them at all — re-deriving `noindex`/`lastModified` by hand for a REAL entry is easy to get
|
|
70
|
+
* wrong in both places at once.
|
|
80
71
|
*/
|
|
81
72
|
export interface SitemapExtraUrl {
|
|
82
73
|
/** Site-relative path ('/blog') or an absolute URL. Trailing-slash rules apply to the former. */
|
|
@@ -126,44 +117,27 @@ export interface GenerateContentSitemapOptions {
|
|
|
126
117
|
/**
|
|
127
118
|
* Override the URL an entry is advertised at, keeping it INSIDE the entry walk.
|
|
128
119
|
*
|
|
129
|
-
* Return a path to advertise the entry at instead of its structural `urlPath`;
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* type you care about, return `null` for the rest — advertises every other entry at its own
|
|
134
|
-
* path rather than silently emitting a one-URL sitemap. Dropping an entry stays `exclude`'s
|
|
135
|
-
* job, and keeping the two separate is the whole reason for this choice: a `pathFor` whose
|
|
136
|
-
* natural-looking callback silently omitted everything else would re-introduce exactly the
|
|
137
|
-
* quietly-short sitemap this module exists to prevent.
|
|
138
|
-
*
|
|
139
|
-
* This is the principled alternative to reaching for `extraUrls` to reroute a REAL entry.
|
|
140
|
-
* Because the entry stays in the walk, it keeps the `isNoindexEntry` gate, the `updatedAt`
|
|
141
|
-
* `lastModified` default and its `priority` automatically — all three of which an `extraUrls`
|
|
142
|
-
* entry makes you re-derive by hand, and get wrong independently (see {@link SitemapExtraUrl}).
|
|
143
|
-
*
|
|
144
|
-
* **It changes what is ADVERTISED, not what is BUILT.** `generateContentStaticParams` still
|
|
145
|
-
* enumerates the entry at its structural path, so the URL returned here must be one your app
|
|
146
|
-
* actually routes — otherwise you have advertised a 404, the same hazard `extraUrls` carries.
|
|
147
|
-
*
|
|
148
|
-
* Return a **site-relative path**. The value is trimmed, and an empty or off-site return (an
|
|
149
|
-
* absolute `https://…` or a protocol-relative `//host/…`) throws rather than being emitted:
|
|
150
|
-
* the first silently claims `/`, and the second lands in the sitemap as a non-absolute `<loc>`,
|
|
151
|
-
* which invalidates the whole file. `extraUrls` is where an off-origin URL is legitimate.
|
|
120
|
+
* Return a path to advertise the entry at instead of its structural `urlPath`; `null`/`undefined`
|
|
121
|
+
* means "no opinion", not "drop it" — a callback that returns `null` for entries it doesn't
|
|
122
|
+
* handle must still advertise them at their own path, or it silently reintroduces the
|
|
123
|
+
* quietly-short sitemap this module exists to prevent. Dropping stays `exclude`'s job.
|
|
152
124
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* reference app in this repo does, which is why its sitemap passes neither this nor `extraUrls`.
|
|
125
|
+
* Because the entry stays in the walk, it keeps the `isNoindexEntry` gate and the `updatedAt`
|
|
126
|
+
* `lastModified`/`priority` defaults that `extraUrls` makes you re-derive by hand (see
|
|
127
|
+
* {@link SitemapExtraUrl}). It changes what is ADVERTISED, not what is BUILT:
|
|
128
|
+
* `generateContentStaticParams` still enumerates the entry at its structural path, so the URL
|
|
129
|
+
* returned here must be one your app actually routes, or you have advertised a 404.
|
|
159
130
|
*
|
|
160
|
-
*
|
|
161
|
-
* `
|
|
162
|
-
*
|
|
131
|
+
* Return a **site-relative path**; it is trimmed. An empty or off-site return (absolute
|
|
132
|
+
* `https://…` or protocol-relative `//host/…`) throws: an empty string would silently claim `/`,
|
|
133
|
+
* and a protocol-relative value lands as a non-absolute `<loc>` that invalidates the whole file
|
|
134
|
+
* (`extraUrls` is where an off-origin URL belongs). Use this only when the URL is fixed by
|
|
135
|
+
* something OUTSIDE the content tree; otherwise model the entry so its `urlPath` is that URL.
|
|
163
136
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
137
|
+
* Runs AFTER the `noindex`/`exclude` gates; `exclude`, `lastModified` and `priority` see the
|
|
138
|
+
* entry as ENUMERATED, so branch on the entry, not on `entry.urlPath` (always structural there).
|
|
139
|
+
* A collision this creates is NOT caught by the build's `assertNoDuplicateUrlPaths` guard, which
|
|
140
|
+
* runs before this rewrite; `dedupeSitemapItems` warns and keeps the first instead.
|
|
167
141
|
*
|
|
168
142
|
* @example
|
|
169
143
|
* // Content lives under content/articles/*, but this site has always published /blog/*.
|
package/dist/static.js
CHANGED
|
@@ -109,8 +109,6 @@ export async function generateContentSitemap(buildCtx, options) {
|
|
|
109
109
|
return dedupeSitemapItems(items).sort((a, b) => a.url === root ? -1 : b.url === root ? 1 : a.url.localeCompare(b.url));
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
|
-
* Resolve the path an entry is advertised at, applying the caller's `pathFor` override.
|
|
113
|
-
*
|
|
114
112
|
* `null`/`undefined` mean "no opinion" and keep the entry's own `urlPath` — see the `pathFor`
|
|
115
113
|
* doc for why that is not "drop it".
|
|
116
114
|
*
|
|
@@ -128,9 +126,8 @@ export async function generateContentSitemap(buildCtx, options) {
|
|
|
128
126
|
* reject the whole file for it. Refused whatever the origin — including one matching `siteUrl` —
|
|
129
127
|
* so the rule is "return a site-relative path" with no host comparison to get subtly wrong.
|
|
130
128
|
*
|
|
131
|
-
* The returned path is TRIMMED
|
|
132
|
-
* URL
|
|
133
|
-
* while checking for emptiness and then hand back untrimmed anyway.
|
|
129
|
+
* The returned path is TRIMMED before use: surrounding whitespace is never intended and would
|
|
130
|
+
* otherwise survive into the URL (`' /blog'` → `<siteUrl>/ /blog`).
|
|
134
131
|
*/
|
|
135
132
|
function resolveEntrySitemapPath(entry, pathFor) {
|
|
136
133
|
if (!pathFor)
|
|
@@ -157,8 +154,6 @@ function resolveEntrySitemapPath(entry, pathFor) {
|
|
|
157
154
|
return trimmed;
|
|
158
155
|
}
|
|
159
156
|
/**
|
|
160
|
-
* Drop duplicate `<loc>` entries, keeping the first occurrence and warning about each collision.
|
|
161
|
-
*
|
|
162
157
|
* A duplicate URL isn't fatal to a crawler, but it usually means two URLs are unintentionally
|
|
163
158
|
* sharing one `<loc>` — an index entry collapsing onto a sibling's path, two `urlPath`s that only
|
|
164
159
|
* differ by case (`urlPath` is lowercased — see `content-listing.ts`), an `extraUrls` path
|
package/dist/test-utils.d.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { AuthPlugin } from 'canopycms/auth';
|
|
2
2
|
import type { AuthenticatedUser } from 'canopycms';
|
|
3
3
|
/**
|
|
4
|
-
* Create a mock AuthPlugin for testing.
|
|
5
4
|
* Returns a valid user by default (as Admin), or can be configured to return specific users.
|
|
6
5
|
*/
|
|
7
6
|
export declare const createMockAuthPlugin: (user?: AuthenticatedUser) => AuthPlugin;
|
|
8
|
-
/**
|
|
9
|
-
* Create a mock AuthPlugin that rejects all authentication.
|
|
10
|
-
*/
|
|
11
7
|
export declare const createRejectingAuthPlugin: (error?: string) => AuthPlugin;
|
package/dist/test-utils.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const ADMINS = 'Admins';
|
|
2
2
|
/**
|
|
3
|
-
* Create a mock AuthPlugin for testing.
|
|
4
3
|
* Returns a valid user by default (as Admin), or can be configured to return specific users.
|
|
5
4
|
*/
|
|
6
5
|
export const createMockAuthPlugin = (user = {
|
|
@@ -23,9 +22,6 @@ export const createMockAuthPlugin = (user = {
|
|
|
23
22
|
getGroupMetadata: async () => null,
|
|
24
23
|
listGroups: async () => [],
|
|
25
24
|
});
|
|
26
|
-
/**
|
|
27
|
-
* Create a mock AuthPlugin that rejects all authentication.
|
|
28
|
-
*/
|
|
29
25
|
export const createRejectingAuthPlugin = (error = 'Unauthorized') => ({
|
|
30
26
|
authenticate: async () => ({ success: false, error }),
|
|
31
27
|
searchUsers: async () => [],
|
package/dist/with-canopy.d.ts
CHANGED
|
@@ -23,58 +23,32 @@ export interface WithCanopyOptions {
|
|
|
23
23
|
staticBuild?: boolean;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Wrap your Next.js config
|
|
27
|
-
*
|
|
26
|
+
* Wrap your Next.js config for CanopyCMS: module transpilation and React resolution. Always
|
|
27
|
+
* recommended — replaces manual `transpilePackages` config and is harmless without aliases.
|
|
28
28
|
*
|
|
29
29
|
* **What it does:**
|
|
30
|
-
* -
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* -
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* -
|
|
38
|
-
*
|
|
39
|
-
* -
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `experimental` spelling you already use.
|
|
48
|
-
* - Your own includes are kept.
|
|
49
|
-
* - A standalone build warns if the directory cannot be found, or if the Next version cannot be
|
|
50
|
-
* read and the entry went under the top-level key.
|
|
51
|
-
* - On Next 16 and later, sets `turbopack: {}` when your config has neither `turbopack` nor
|
|
52
|
-
* `webpack` and the installed Next version can be read (an unreadable version gets no key).
|
|
53
|
-
* Next 16 builds and runs `next dev` with Turbopack by default, and exits when the
|
|
54
|
-
* config has a `webpack` function (the React aliases above add one) but no `turbopack` config.
|
|
55
|
-
* A `turbopack` you already set, `turbopack: {}` included, is kept as it is.
|
|
56
|
-
*
|
|
57
|
-
* **When you need this:**
|
|
58
|
-
* - Always recommended — it replaces manual `transpilePackages` configuration
|
|
59
|
-
* and is harmless when React aliases aren't strictly needed.
|
|
60
|
-
*
|
|
61
|
-
* **When React aliases matter:**
|
|
62
|
-
* - When consuming canopycms packages via `file:` references or `npm link`
|
|
63
|
-
* during local development. Without the aliases, the bundler follows
|
|
64
|
-
* symlinks and may resolve a second copy of React from the linked
|
|
65
|
-
* package's node_modules, causing "Invalid hook call" crashes.
|
|
66
|
-
* - When installing from npm (not symlinked), the aliases are still safe
|
|
67
|
-
* — they simply resolve to the same React your project already uses.
|
|
30
|
+
* - Adds installed Canopy packages to `transpilePackages` (they export raw TypeScript), auto-detected
|
|
31
|
+
* so an optional package you haven't installed is never added.
|
|
32
|
+
* - Adds `server.ts`/`server.tsx` (or, with `staticBuild: true`, `static.ts`/`static.tsx`) to
|
|
33
|
+
* `pageExtensions`, so CMS-only files build in dev/CMS and static-only variants build instead.
|
|
34
|
+
* - Resolves React to one copy from your project root, avoiding "Invalid hook call" crashes when
|
|
35
|
+
* canopycms packages are linked via `file:`/`npm link` (the bundler would otherwise follow the
|
|
36
|
+
* symlink to a second React copy); a no-op otherwise, since it resolves to the React you already use.
|
|
37
|
+
* - With `staticBuild: true`, honors `CANOPY_BUILD_ID` as a reproducible build id (Next
|
|
38
|
+
* defaults to a random one); unset, or off a static build, Next's default stands.
|
|
39
|
+
* - Outside a static export, adds sharp's libvips directory to `outputFileTracingIncludes['/**']`
|
|
40
|
+
* (kept under `experimental` on Next 13/14 or legacy spellings; your own includes stay), so a
|
|
41
|
+
* Turbopack `output: 'standalone'` server can load sharp — Next's tracing misses that library
|
|
42
|
+
* for sharp 0.35 (doesn't fix webpack builds); warns if the directory or version can't be found.
|
|
43
|
+
* - On Next 16+, sets `turbopack: {}` when your config has neither `turbopack` nor `webpack` and
|
|
44
|
+
* the Next version can be read — Next 16 defaults to Turbopack and exits when a config exports
|
|
45
|
+
* `webpack` (the React aliases above add one) with no `turbopack` key; a `turbopack` you set is
|
|
46
|
+
* left alone.
|
|
68
47
|
*
|
|
69
48
|
* @example
|
|
70
49
|
* ```ts
|
|
71
|
-
* // next.config.ts
|
|
72
50
|
* import { withCanopy } from 'canopycms-next/config'
|
|
73
|
-
*
|
|
74
|
-
* export default withCanopy({
|
|
75
|
-
* reactStrictMode: true,
|
|
76
|
-
* // ...your config
|
|
77
|
-
* })
|
|
51
|
+
* export default withCanopy({ reactStrictMode: true })
|
|
78
52
|
* ```
|
|
79
53
|
*/
|
|
80
54
|
export declare function withCanopy(nextConfig?: NextConfig, options?: WithCanopyOptions): NextConfig;
|
package/dist/with-canopy.js
CHANGED
|
@@ -226,8 +226,6 @@ function unknownNextVersionWarning(projectDir) {
|
|
|
226
226
|
'experimental.outputFileTracingIncludes.');
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
|
-
* Whether the warning has fired in this module instance.
|
|
230
|
-
*
|
|
231
229
|
* A Turbopack build evaluates the config in the main process and again in a worker thread with its
|
|
232
230
|
* own module registry (`next/dist/build/turbopack-build/index.js:26` and `impl.js:209` in 16.1.7).
|
|
233
231
|
* So "once" means once per process or thread, not once per build.
|
|
@@ -322,58 +320,32 @@ function sharpTracingConfig(nextConfig, options) {
|
|
|
322
320
|
return { experimental: legacyExperimental };
|
|
323
321
|
}
|
|
324
322
|
/**
|
|
325
|
-
* Wrap your Next.js config
|
|
326
|
-
*
|
|
323
|
+
* Wrap your Next.js config for CanopyCMS: module transpilation and React resolution. Always
|
|
324
|
+
* recommended — replaces manual `transpilePackages` config and is harmless without aliases.
|
|
327
325
|
*
|
|
328
326
|
* **What it does:**
|
|
329
|
-
* -
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* -
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
* -
|
|
337
|
-
*
|
|
338
|
-
* -
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* `experimental` spelling you already use.
|
|
347
|
-
* - Your own includes are kept.
|
|
348
|
-
* - A standalone build warns if the directory cannot be found, or if the Next version cannot be
|
|
349
|
-
* read and the entry went under the top-level key.
|
|
350
|
-
* - On Next 16 and later, sets `turbopack: {}` when your config has neither `turbopack` nor
|
|
351
|
-
* `webpack` and the installed Next version can be read (an unreadable version gets no key).
|
|
352
|
-
* Next 16 builds and runs `next dev` with Turbopack by default, and exits when the
|
|
353
|
-
* config has a `webpack` function (the React aliases above add one) but no `turbopack` config.
|
|
354
|
-
* A `turbopack` you already set, `turbopack: {}` included, is kept as it is.
|
|
355
|
-
*
|
|
356
|
-
* **When you need this:**
|
|
357
|
-
* - Always recommended — it replaces manual `transpilePackages` configuration
|
|
358
|
-
* and is harmless when React aliases aren't strictly needed.
|
|
359
|
-
*
|
|
360
|
-
* **When React aliases matter:**
|
|
361
|
-
* - When consuming canopycms packages via `file:` references or `npm link`
|
|
362
|
-
* during local development. Without the aliases, the bundler follows
|
|
363
|
-
* symlinks and may resolve a second copy of React from the linked
|
|
364
|
-
* package's node_modules, causing "Invalid hook call" crashes.
|
|
365
|
-
* - When installing from npm (not symlinked), the aliases are still safe
|
|
366
|
-
* — they simply resolve to the same React your project already uses.
|
|
327
|
+
* - Adds installed Canopy packages to `transpilePackages` (they export raw TypeScript), auto-detected
|
|
328
|
+
* so an optional package you haven't installed is never added.
|
|
329
|
+
* - Adds `server.ts`/`server.tsx` (or, with `staticBuild: true`, `static.ts`/`static.tsx`) to
|
|
330
|
+
* `pageExtensions`, so CMS-only files build in dev/CMS and static-only variants build instead.
|
|
331
|
+
* - Resolves React to one copy from your project root, avoiding "Invalid hook call" crashes when
|
|
332
|
+
* canopycms packages are linked via `file:`/`npm link` (the bundler would otherwise follow the
|
|
333
|
+
* symlink to a second React copy); a no-op otherwise, since it resolves to the React you already use.
|
|
334
|
+
* - With `staticBuild: true`, honors `CANOPY_BUILD_ID` as a reproducible build id (Next
|
|
335
|
+
* defaults to a random one); unset, or off a static build, Next's default stands.
|
|
336
|
+
* - Outside a static export, adds sharp's libvips directory to `outputFileTracingIncludes['/**']`
|
|
337
|
+
* (kept under `experimental` on Next 13/14 or legacy spellings; your own includes stay), so a
|
|
338
|
+
* Turbopack `output: 'standalone'` server can load sharp — Next's tracing misses that library
|
|
339
|
+
* for sharp 0.35 (doesn't fix webpack builds); warns if the directory or version can't be found.
|
|
340
|
+
* - On Next 16+, sets `turbopack: {}` when your config has neither `turbopack` nor `webpack` and
|
|
341
|
+
* the Next version can be read — Next 16 defaults to Turbopack and exits when a config exports
|
|
342
|
+
* `webpack` (the React aliases above add one) with no `turbopack` key; a `turbopack` you set is
|
|
343
|
+
* left alone.
|
|
367
344
|
*
|
|
368
345
|
* @example
|
|
369
346
|
* ```ts
|
|
370
|
-
* // next.config.ts
|
|
371
347
|
* import { withCanopy } from 'canopycms-next/config'
|
|
372
|
-
*
|
|
373
|
-
* export default withCanopy({
|
|
374
|
-
* reactStrictMode: true,
|
|
375
|
-
* // ...your config
|
|
376
|
-
* })
|
|
348
|
+
* export default withCanopy({ reactStrictMode: true })
|
|
377
349
|
* ```
|
|
378
350
|
*/
|
|
379
351
|
export function withCanopy(nextConfig = {}, options = {}) {
|
|
@@ -387,7 +359,6 @@ export function withCanopy(nextConfig = {}, options = {}) {
|
|
|
387
359
|
return false;
|
|
388
360
|
}
|
|
389
361
|
});
|
|
390
|
-
// Merge transpilePackages (deduped)
|
|
391
362
|
const existingPackages = nextConfig.transpilePackages ?? [];
|
|
392
363
|
const allPackages = [
|
|
393
364
|
...new Set([
|
|
@@ -398,7 +369,6 @@ export function withCanopy(nextConfig = {}, options = {}) {
|
|
|
398
369
|
]),
|
|
399
370
|
];
|
|
400
371
|
const reactAlias = resolveReactAliases(resolve);
|
|
401
|
-
// Scope React aliases to only canopycms files using module.rules[].resolve.
|
|
402
372
|
// A global resolve.alias would also override Next.js's own internal React
|
|
403
373
|
// (bundled at next/dist/compiled/react/), breaking its devtools and internals.
|
|
404
374
|
const existingWebpack = nextConfig.webpack;
|
|
@@ -414,7 +384,6 @@ export function withCanopy(nextConfig = {}, options = {}) {
|
|
|
414
384
|
alias: reactAlias,
|
|
415
385
|
},
|
|
416
386
|
});
|
|
417
|
-
// Chain consumer's existing webpack config
|
|
418
387
|
if (typeof existingWebpack === 'function') {
|
|
419
388
|
return existingWebpack(config, ctx);
|
|
420
389
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopycms-next",
|
|
3
|
-
"version": "0.0.67-int.
|
|
3
|
+
"version": "0.0.67-int.91",
|
|
4
4
|
"description": "Next.js adapter for CanopyCMS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"next": "^13.5.7 || ^14.2.25 || ^15.5.21 || >=16.0.0 <16.2.0 || >=16.3.0 <17.0.0",
|
|
40
40
|
"react": "^18.0.0 || ^19.0.0",
|
|
41
|
-
"canopycms": "0.0.67-int.
|
|
41
|
+
"canopycms": "0.0.67-int.91"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^22.9.0",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"react": "^18.3.1",
|
|
49
49
|
"typescript": "^5.6.3",
|
|
50
50
|
"vitest": "^4.1.0",
|
|
51
|
-
"canopycms": "0.0.67-int.
|
|
51
|
+
"canopycms": "0.0.67-int.91"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"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",
|