canopycms-next 0.0.67-int.90 → 0.0.67

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 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
- // Node Buffer), which doesn't structurally satisfy the DOM lib's
56
- // BodyInit/ArrayBufferView (specifically ArrayBuffer-backed). Copying
57
- // through the typed-array constructor yields a plain ArrayBuffer-backed
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
- // top-level try/catch, but this adapter also wraps request conversion
121
- // (wrapNextRequest/extractPathSegments/toNextResponse) so a failure there
122
- // can never escape as Next's generic unhandled-error 500, which would
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. The HTTP body was
126
- // already sanitized, but this line went to the server log verbatim -- and
127
- // a git failure message can embed a token-bearing clone URL, which is the
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
- * to `CanopyEditorPage`. Forwarded rather than dropped for a specific reason:
8
- * Next is the primary target, so this wrapper is the entrypoint adopters
9
- * actually import (it is what the README's Quick Start scaffolds). Accepting
10
- * the argument only on the core `CanopyEditorPage` made the extension point
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
- * to `CanopyEditorPage`. Forwarded rather than dropped for a specific reason:
12
- * Next is the primary target, so this wrapper is the entrypoint adopters
13
- * actually import (it is what the README's Quick Start scaffolds). Accepting
14
- * the argument only on the core `CanopyEditorPage` made the extension point
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
- // Reads URL search params. Next.js 15 requires useSearchParams() consumers to
35
- // sit under a <Suspense> boundary, otherwise static rendering errors with
36
- // "useSearchParams() should be wrapped in a suspense boundary". We provide the
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`). Set once here
33
- * and it is shared by BOTH the returned `generateContentSitemap`'s `noindex` exclusion and the
34
- * returned `entryToMetadata`'s field extraction, so the two surfaces cannot independently forget
35
- * or mis-set it and drift apart — see `mergeSeoFieldLocation`. Omit for the flat (inline-group)
36
- * convention, which is the default on both the schema side (`defineSeoFieldGroup()`) and here.
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
- * Reads the SAME SEO field location as `generateContentSitemap` above by default (the
131
- * `createNextCanopyContext({ seo })` value); the two are bound from one context-wide default so
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
- * Adds React cache() for per-request memoization and API handler.
145
- * This function is async because it needs to load .collection.json meta files.
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>;
@@ -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 (which now carries
102
- // meta.physicalPath); the returned object is validated against CanopyBuildContext.
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
- * Adds React cache() for per-request memoization and API handler.
116
- * This function is async because it needs to load .collection.json meta files.
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
- // Resolve the auth plugin: auto-wrap with CachingAuthPlugin for prod/dev when
140
- // the plugin supports token-only verification. This keeps auth networkless (required for
141
- // Lambda in prod, consistent in dev) without exposing caching internals to adopters.
142
- // For static deployments, use the stub that returns 401 for all requests.
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
- // User extractor: passes Next.js headers to auth plugin, resolves internal
176
- // groups, applies authorization. Delegates to the shared
177
- // "authenticate -> load internal groups -> merge" pipeline
178
- // (resolveCanopyUser, in canopycms core) so this stays in lockstep with
179
- // http/handler.ts's API-layer equivalent rather than re-implementing it —
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 (unlike the previous copy):
185
- // that call existed only to source groups.json from the (wrong) content
186
- // branch. Actual request-time content reads (buildContentTree/listEntries/
187
- // read) already provision the base/active branch themselves via
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 (see item #5).
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
- // Both this and boundEntryToMetadata below feed options.seo (the context-wide default set on
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,
@@ -16,7 +16,12 @@ export interface SharpTracingInput {
16
16
  lockfileRoot?: 'outermost' | 'closest';
17
17
  }
18
18
  export interface SharpTracingResult {
19
- /** Project-relative POSIX globs, one per native-library `lib/` directory found. */
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
  /**
@@ -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`, through its native binding's rpath. Nothing
5
- * `require`s the library, so a tracer that follows imports never sees it. Next's JS tracer
6
- * (`next/dist/compiled/@vercel/nft`) covers that with a sharp-specific handler, but the handler only
7
- * fires on a path ending in `sharp/lib/index.js`, which is sharp 0.34's entry point. sharp 0.35
8
- * ships `dist/index.{cjs,mjs}` instead.
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
- * Measured on a Next 16.1.7 Turbopack `output: 'standalone'` build (`.claude/future-tasks/`
11
- * `cms-image-build-epic.md`, "The tracer misses the `.so`"): the route traces list libvips's
12
- * `package.json` and the binding's rpath symlink, but not `lib/libvips-cpp.so.*`. Every load of
13
- * sharp in the resulting server then fails with ERR_DLOPEN_FAILED.
14
- *
15
- * A webpack build under pnpm has a different problem. On Next 15.5.21, Next's webpack externals
16
- * resolution bundles sharp's JavaScript into a server chunk, so no sharp 0.35 package is traced and
17
- * the bundled copy cannot reach its native binding, whether or not this include ships libvips (root
18
- * cause in `.claude/future-tasks/webpack-standalone-sharp-bundled.md`). An npm install and Next
19
- * 16's `next build --webpack` have not been checked. Where Next's JS tracer does trace the library
20
- * itself, the include adds no duplicate: it merges traced and included files into one `Set`
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.** Nothing checks the SEO flag for an extra URL, because there is no
59
- * entry data to check. If you are using `extraUrls` to re-advertise a real entry under a
60
- * different path (the usual reason — an entry whose structural `urlPath` no route serves),
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 simply ships without a date.
65
- *
66
- * Both are easy to get wrong in exactly the same direction, and both used to be unavoidable:
67
- * `extraUrls` was once the only way to advertise a real entry at a different URL, so every such
68
- * use re-derived `noindex` and `lastModified` by hand — this repo's own reference app among them,
69
- * and it got `lastModified` wrong.
70
- *
71
- * It is no longer the only way, so it should no longer be that way:
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`; return `null`
130
- * (or `undefined`) to leave it alone.
131
- *
132
- * **`null` means "no opinion", not "drop it".** So the obvious shape — handle the one entry
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
- * Reach for it only when the URL is fixed by something OUTSIDE the content tree: published URLs
154
- * you cannot change, or a route prefix that deliberately differs from the content layout. When
155
- * you control the modelling, model the entry so its natural `urlPath` is already the URL you
156
- * serve — an `index` entry collapses onto its collection's path, and a root `index` entry onto
157
- * `/` — which needs no option at all. That ordering is not advice we only give: it is what the
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
- * Runs AFTER the `noindex` and `exclude` gates (a dropped entry needs no URL). `exclude`,
161
- * `lastModified` and `priority` receive the entry as ENUMERATED, so `entry.urlPath` there is
162
- * always the structural path and never your override — branch on the entry, not on the URL.
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
- * Two entries rewritten onto one URL are NOT caught by the build's `assertNoDuplicateUrlPaths`
165
- * guard, which runs on the raw listing and never sees these rewrites; `dedupeSitemapItems`
166
- * warns and keeps the first.
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. Surrounding whitespace is never intended and survives into the
132
- * URL otherwise (`' /blog'` → `<siteUrl>/ /blog`) — which this function would previously detect
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
@@ -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;
@@ -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 () => [],
@@ -23,58 +23,32 @@ export interface WithCanopyOptions {
23
23
  staticBuild?: boolean;
24
24
  }
25
25
  /**
26
- * Wrap your Next.js config to set up module transpilation and React
27
- * resolution for CanopyCMS packages.
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
- * - Auto-detects installed Canopy packages and adds them to `transpilePackages`
31
- * (they export raw TypeScript). Only packages found in your node_modules are
32
- * added, so you don't need to worry about optional packages you haven't installed.
33
- * - Adds `server.ts`/`server.tsx` (or, when `staticBuild: true`, `static.ts`/`static.tsx`)
34
- * to `pageExtensions` for dual-build support. CMS-only files (e.g., `route.server.ts`)
35
- * are included in dev/CMS builds but excluded when `staticBuild: true` is set, in favor
36
- * of the static-export-only page variants (e.g. `page.static.tsx`).
37
- * - Resolves React to a single copy from your project root, preventing
38
- * dual-instance crashes when using `file:` symlinks for local development
39
- * - With `staticBuild: true`, honors `CANOPY_BUILD_ID` as Next's build id (Next's default is
40
- * random, which puts two builds of one source tree in different `_next/static/` directories).
41
- * Unset, or on a non-static build, Next's default is used unchanged.
42
- * - Outside a static export, adds sharp's libvips directory to `outputFileTracingIncludes['/**']`,
43
- * so a Turbopack `output: 'standalone'` server can load sharp. Next's file tracing can miss that
44
- * library for sharp 0.35. This does not fix a webpack build: under pnpm, Next 15.5.21 bundles
45
- * sharp into a server chunk that cannot load its native binding.
46
- * - The key follows your Next version (under `experimental` on 13 and 14) and any legacy
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;
@@ -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 to set up module transpilation and React
326
- * resolution for CanopyCMS packages.
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
- * - Auto-detects installed Canopy packages and adds them to `transpilePackages`
330
- * (they export raw TypeScript). Only packages found in your node_modules are
331
- * added, so you don't need to worry about optional packages you haven't installed.
332
- * - Adds `server.ts`/`server.tsx` (or, when `staticBuild: true`, `static.ts`/`static.tsx`)
333
- * to `pageExtensions` for dual-build support. CMS-only files (e.g., `route.server.ts`)
334
- * are included in dev/CMS builds but excluded when `staticBuild: true` is set, in favor
335
- * of the static-export-only page variants (e.g. `page.static.tsx`).
336
- * - Resolves React to a single copy from your project root, preventing
337
- * dual-instance crashes when using `file:` symlinks for local development
338
- * - With `staticBuild: true`, honors `CANOPY_BUILD_ID` as Next's build id (Next's default is
339
- * random, which puts two builds of one source tree in different `_next/static/` directories).
340
- * Unset, or on a non-static build, Next's default is used unchanged.
341
- * - Outside a static export, adds sharp's libvips directory to `outputFileTracingIncludes['/**']`,
342
- * so a Turbopack `output: 'standalone'` server can load sharp. Next's file tracing can miss that
343
- * library for sharp 0.35. This does not fix a webpack build: under pnpm, Next 15.5.21 bundles
344
- * sharp into a server chunk that cannot load its native binding.
345
- * - The key follows your Next version (under `experimental` on 13 and 14) and any legacy
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.90",
3
+ "version": "0.0.67",
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.90"
41
+ "canopycms": "0.0.67"
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.90"
51
+ "canopycms": "0.0.67"
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",