canopycms-next 0.0.67-int.89 → 0.0.67-int.90

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/config.cjs CHANGED
@@ -36,7 +36,187 @@ module.exports = __toCommonJS(config_exports);
36
36
 
37
37
  // src/with-canopy.ts
38
38
  var import_node_module = require("node:module");
39
+ var import_node_path2 = __toESM(require("node:path"), 1);
40
+
41
+ // src/sharp-tracing.ts
42
+ var import_node_fs = require("node:fs");
39
43
  var import_node_path = __toESM(require("node:path"), 1);
44
+ var LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"];
45
+ var LEGACY_LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
46
+ var NEXT_CONFIG_FILES = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.mts"];
47
+ var GLOB_METACHARACTERS = /[*?[\]{}(),!\\]/;
48
+ var LIBVIPS_PACKAGE = /^@img\/sharp-libvips-/;
49
+ var NATIVE_BINDING_PACKAGE = /^@img\/sharp-(?!libvips-)/;
50
+ function messageOf(err) {
51
+ if (err instanceof Error) return err.message;
52
+ if (typeof err === "string") return err;
53
+ return String(err);
54
+ }
55
+ function isFile(candidate) {
56
+ try {
57
+ return (0, import_node_fs.statSync)(candidate).isFile();
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ function isDirectory(candidate) {
63
+ try {
64
+ return (0, import_node_fs.statSync)(candidate).isDirectory();
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+ function isOutside(root, target) {
70
+ const relative = import_node_path.default.relative(root, target);
71
+ return relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative);
72
+ }
73
+ function readManifest(packageDir) {
74
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(import_node_path.default.join(packageDir, "package.json"), "utf8"));
75
+ const manifest = { optionalDependencyNames: [] };
76
+ if (typeof parsed !== "object" || parsed === null) return manifest;
77
+ if ("name" in parsed && typeof parsed.name === "string") manifest.name = parsed.name;
78
+ if ("version" in parsed && typeof parsed.version === "string") manifest.version = parsed.version;
79
+ if ("optionalDependencies" in parsed && typeof parsed.optionalDependencies === "object" && parsed.optionalDependencies !== null) {
80
+ manifest.optionalDependencyNames = Object.keys(parsed.optionalDependencies);
81
+ }
82
+ return manifest;
83
+ }
84
+ function nodeModulesLookupDirs(fromDir) {
85
+ const dirs = [];
86
+ for (let dir = import_node_path.default.resolve(fromDir); ; dir = import_node_path.default.dirname(dir)) {
87
+ if (import_node_path.default.basename(dir) !== "node_modules") dirs.push(import_node_path.default.join(dir, "node_modules"));
88
+ if (import_node_path.default.dirname(dir) === dir) return dirs;
89
+ }
90
+ }
91
+ function findInstalledPackage(fromDir, name) {
92
+ for (const lookupDir of nodeModulesLookupDirs(fromDir)) {
93
+ const candidate = import_node_path.default.join(lookupDir, name);
94
+ if (isFile(import_node_path.default.join(candidate, "package.json"))) return (0, import_node_fs.realpathSync)(candidate);
95
+ }
96
+ return null;
97
+ }
98
+ function installedNextMajor(projectDir) {
99
+ try {
100
+ const nextDir = findInstalledPackage(projectDir, "next");
101
+ if (!nextDir) return null;
102
+ const major = Number.parseInt(readManifest(nextDir).version ?? "", 10);
103
+ return Number.isNaN(major) ? null : major;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+ function hasNextConfig(dir) {
109
+ return NEXT_CONFIG_FILES.some((name) => isFile(import_node_path.default.join(dir, name)));
110
+ }
111
+ function findLockFileUpwards(startDir, names) {
112
+ for (let dir = startDir; ; dir = import_node_path.default.dirname(dir)) {
113
+ for (const name of names) {
114
+ const candidate = import_node_path.default.join(dir, name);
115
+ if (isFile(candidate)) return candidate;
116
+ }
117
+ if (import_node_path.default.dirname(dir) === dir) return null;
118
+ }
119
+ }
120
+ function resolveTracingRoot(input) {
121
+ const configured = input.outputFileTracingRoot || input.turbopackRoot;
122
+ if (configured) return import_node_path.default.resolve(input.projectDir, configured);
123
+ if (input.lockfileRoot === "closest") {
124
+ const closest = findLockFileUpwards(input.projectDir, LEGACY_LOCKFILES);
125
+ return closest ? import_node_path.default.dirname(closest) : input.projectDir;
126
+ }
127
+ let lockFile = findLockFileUpwards(input.projectDir, LOCKFILES);
128
+ if (!lockFile) return input.projectDir;
129
+ for (; ; ) {
130
+ const lockDir = import_node_path.default.dirname(lockFile);
131
+ const parentDir = import_node_path.default.dirname(lockDir);
132
+ if (parentDir === lockDir) break;
133
+ const outer = findLockFileUpwards(parentDir, LOCKFILES);
134
+ if (!outer) break;
135
+ lockFile = outer;
136
+ }
137
+ return import_node_path.default.dirname(lockFile);
138
+ }
139
+ function nativeLibraryDirsFor(sharpDir) {
140
+ const found = /* @__PURE__ */ new Set();
141
+ const collectLibvips = (fromDir, dependencyNames) => {
142
+ for (const name of dependencyNames.filter((n) => LIBVIPS_PACKAGE.test(n))) {
143
+ const dir = findInstalledPackage(fromDir, name);
144
+ if (dir) found.add(dir);
145
+ }
146
+ };
147
+ const sharpDependencies = readManifest(sharpDir).optionalDependencyNames;
148
+ collectLibvips(sharpDir, sharpDependencies);
149
+ for (const bindingName of sharpDependencies.filter((n) => NATIVE_BINDING_PACKAGE.test(n))) {
150
+ const bindingDir = findInstalledPackage(sharpDir, bindingName);
151
+ if (!bindingDir) continue;
152
+ const bindingDependencies = readManifest(bindingDir).optionalDependencyNames;
153
+ if (bindingDependencies.some((n) => LIBVIPS_PACKAGE.test(n))) {
154
+ collectLibvips(bindingDir, bindingDependencies);
155
+ } else {
156
+ found.add(bindingDir);
157
+ }
158
+ }
159
+ return [...found];
160
+ }
161
+ function sharpTracingIncludes(input) {
162
+ try {
163
+ const projectDir = (0, import_node_fs.realpathSync)(input.projectDir);
164
+ const root = (0, import_node_fs.realpathSync)(resolveTracingRoot(input));
165
+ if (isOutside(root, projectDir)) {
166
+ return {
167
+ includes: [],
168
+ problem: `the project directory ${projectDir} is outside the tracing root ${root}`
169
+ };
170
+ }
171
+ const sharpDirs = /* @__PURE__ */ new Set();
172
+ const canopycmsDir = findInstalledPackage(projectDir, "canopycms");
173
+ for (const candidate of [
174
+ canopycmsDir && findInstalledPackage(canopycmsDir, "sharp"),
175
+ findInstalledPackage(projectDir, "sharp")
176
+ ]) {
177
+ if (candidate && readManifest(candidate).name === "sharp") sharpDirs.add(candidate);
178
+ }
179
+ if (sharpDirs.size === 0) {
180
+ return {
181
+ includes: [],
182
+ problem: canopycmsDir ? `no sharp is installed for canopycms at ${canopycmsDir}` : `canopycms is not installed where ${projectDir} can find it`
183
+ };
184
+ }
185
+ const includes = /* @__PURE__ */ new Set();
186
+ const notes = [];
187
+ for (const sharpDir of sharpDirs) {
188
+ const libraryDirs = nativeLibraryDirsFor(sharpDir);
189
+ if (libraryDirs.length === 0) {
190
+ notes.push(
191
+ `no libvips package is installed for sharp at ${sharpDir}, and no installed native binding carries its own library`
192
+ );
193
+ }
194
+ for (const libraryDir of libraryDirs) {
195
+ const libDir = import_node_path.default.join(libraryDir, "lib");
196
+ if (!isDirectory(libDir)) {
197
+ notes.push(`${libDir} does not exist`);
198
+ continue;
199
+ }
200
+ if (isOutside(root, libDir)) {
201
+ notes.push(`${libDir} is outside the tracing root ${root}`);
202
+ continue;
203
+ }
204
+ const relative = import_node_path.default.relative(projectDir, libDir).split(import_node_path.default.sep).join("/");
205
+ if (GLOB_METACHARACTERS.test(relative)) {
206
+ notes.push(`${relative} contains a glob metacharacter`);
207
+ continue;
208
+ }
209
+ includes.add(`${relative}/**/*`);
210
+ }
211
+ }
212
+ if (includes.size === 0) return { includes: [], problem: notes.join("; ") };
213
+ return { includes: [...includes] };
214
+ } catch (err) {
215
+ return { includes: [], problem: messageOf(err) };
216
+ }
217
+ }
218
+
219
+ // src/with-canopy.ts
40
220
  var REQUIRED_PACKAGES = ["canopycms"];
41
221
  var OPTIONAL_PACKAGES = [
42
222
  "canopycms-next",
@@ -70,8 +250,8 @@ function withAssetsRewrite(existingRewrites) {
70
250
  function resolveReactAliases(resolve) {
71
251
  try {
72
252
  return {
73
- react: import_node_path.default.dirname(resolve("react")),
74
- "react-dom": import_node_path.default.dirname(resolve("react-dom"))
253
+ react: import_node_path2.default.dirname(resolve("react")),
254
+ "react-dom": import_node_path2.default.dirname(resolve("react-dom"))
75
255
  };
76
256
  } catch {
77
257
  return null;
@@ -100,8 +280,82 @@ function resolveStaticBuildId() {
100
280
  }
101
281
  return trimmed;
102
282
  }
283
+ var ALL_ROUTES = "/**";
284
+ function isTracingIncludes(value) {
285
+ return typeof value === "object" && value !== null && Object.values(value).every(
286
+ (globs) => Array.isArray(globs) && globs.every((g) => typeof g === "string")
287
+ );
288
+ }
289
+ function readProperty(value, key) {
290
+ return typeof value === "object" && value !== null ? Reflect.get(value, key) : void 0;
291
+ }
292
+ function stringOrUndefined(value) {
293
+ return typeof value === "string" ? value : void 0;
294
+ }
295
+ function isSet(value) {
296
+ return value !== void 0 && value !== null;
297
+ }
298
+ function mergeTracingIncludes(existing, includes) {
299
+ return {
300
+ ...existing,
301
+ [ALL_ROUTES]: [.../* @__PURE__ */ new Set([...existing?.[ALL_ROUTES] ?? [], ...includes])]
302
+ };
303
+ }
304
+ function sharpTracingWarning(problem, nextVersionUnknown) {
305
+ return "CanopyCMS: could not add sharp's libvips to this standalone build's file tracing" + (problem ? ` (${problem})` : "") + ". Next.js file tracing can miss that library for sharp 0.35 (https://github.com/vercel/next.js/issues/97973), and the built server then fails to load sharp with ERR_DLOPEN_FAILED. Add it in next.config yourself, with paths relative to the project directory. For pnpm: outputFileTracingIncludes: { '/**': ['node_modules/.pnpm/@img+sharp-libvips-*/node_modules/@img/*/lib/**/*'] }. For npm: outputFileTracingIncludes: { '/**': ['node_modules/@img/sharp-libvips-*/lib/**/*'] }." + (nextVersionUnknown ? " The installed Next.js version could not be read either: on Next 13 or 14, put outputFileTracingIncludes under experimental." : "");
306
+ }
307
+ function unknownNextVersionWarning(projectDir) {
308
+ return `CanopyCMS: could not read the version of the next package installed for ${projectDir}, so sharp's libvips was added to the top-level outputFileTracingIncludes key, which Next 15 and later read. Next 13 and 14 ignore that key: on those versions, move the entry under experimental.outputFileTracingIncludes.`;
309
+ }
310
+ var warnedAboutSharpTracing = false;
311
+ function sharpTracingConfig(nextConfig, options) {
312
+ if (options.staticBuild || nextConfig.output === "export") return {};
313
+ const projectDir = process.cwd();
314
+ const nextMajor = installedNextMajor(projectDir);
315
+ const experimental = nextConfig.experimental ?? {};
316
+ const experimentalOnly = nextMajor !== null && nextMajor < 15;
317
+ const includesUnderExperimental = experimentalOnly || isSet(readProperty(experimental, "outputFileTracingIncludes"));
318
+ const rootUnderExperimental = experimentalOnly || isSet(readProperty(experimental, "outputFileTracingRoot"));
319
+ const writtenIncludes = includesUnderExperimental ? readProperty(experimental, "outputFileTracingIncludes") : nextConfig.outputFileTracingIncludes;
320
+ const existing = isSet(writtenIncludes) ? writtenIncludes : void 0;
321
+ if (existing !== void 0 && !isTracingIncludes(existing)) return {};
322
+ const configuredRoot = stringOrUndefined(
323
+ rootUnderExperimental ? readProperty(experimental, "outputFileTracingRoot") : nextConfig.outputFileTracingRoot
324
+ );
325
+ const turbopack = nextConfig.turbopack;
326
+ const turbopackHasRootKey = typeof turbopack === "object" && turbopack !== null && "root" in turbopack;
327
+ const legacyTurbopackRoot = nextMajor === 15 && !turbopackHasRootKey ? stringOrUndefined(readProperty(readProperty(experimental, "turbo"), "root")) : void 0;
328
+ const turbopackRoot = experimentalOnly ? void 0 : stringOrUndefined(readProperty(turbopack, "root")) ?? legacyTurbopackRoot;
329
+ const { includes, problem } = hasNextConfig(projectDir) ? sharpTracingIncludes({
330
+ projectDir,
331
+ outputFileTracingRoot: configuredRoot,
332
+ turbopackRoot,
333
+ lockfileRoot: experimentalOnly ? "closest" : "outermost"
334
+ }) : {
335
+ includes: [],
336
+ problem: `the working directory ${projectDir} has no next.config file, so withCanopy cannot tell where the project is`
337
+ };
338
+ const nextVersionUnknown = nextMajor === null;
339
+ if (includes.length === 0) {
340
+ if (nextConfig.output === "standalone" && !warnedAboutSharpTracing) {
341
+ warnedAboutSharpTracing = true;
342
+ console.warn(sharpTracingWarning(problem, nextVersionUnknown));
343
+ }
344
+ return {};
345
+ }
346
+ const merged = mergeTracingIncludes(existing, includes);
347
+ if (!includesUnderExperimental) {
348
+ if (nextVersionUnknown && nextConfig.output === "standalone" && !warnedAboutSharpTracing) {
349
+ warnedAboutSharpTracing = true;
350
+ console.warn(unknownNextVersionWarning(projectDir));
351
+ }
352
+ return { outputFileTracingIncludes: merged };
353
+ }
354
+ const legacyExperimental = { ...experimental, outputFileTracingIncludes: merged };
355
+ return { experimental: legacyExperimental };
356
+ }
103
357
  function withCanopy(nextConfig = {}, options = {}) {
104
- const resolve = (0, import_node_module.createRequire)(import_node_path.default.join(process.cwd(), "noop.js")).resolve;
358
+ const resolve = (0, import_node_module.createRequire)(import_node_path2.default.join(process.cwd(), "noop.js")).resolve;
105
359
  const installedOptional = OPTIONAL_PACKAGES.filter((pkg) => {
106
360
  try {
107
361
  resolve(pkg);
@@ -136,6 +390,8 @@ function withCanopy(nextConfig = {}, options = {}) {
136
390
  }
137
391
  return config;
138
392
  } : existingWebpack;
393
+ const nextMajor = installedNextMajor(process.cwd());
394
+ const answerTurbopackGuard = webpack !== void 0 && !isSet(existingWebpack) && !isSet(nextConfig.turbopack) && nextMajor !== null && nextMajor >= 16;
139
395
  const pageExtensions = [
140
396
  .../* @__PURE__ */ new Set([
141
397
  ...nextConfig.pageExtensions ?? NEXTJS_DEFAULT_PAGE_EXTENSIONS,
@@ -149,9 +405,11 @@ function withCanopy(nextConfig = {}, options = {}) {
149
405
  pageExtensions,
150
406
  webpack,
151
407
  rewrites: withAssetsRewrite(nextConfig.rewrites),
408
+ ...sharpTracingConfig(nextConfig, options),
152
409
  // Spread conditionally: emitting `generateBuildId: undefined` would be a key Next has to
153
410
  // reason about, where absence is unambiguous.
154
- ...generateBuildId ? { generateBuildId } : {}
411
+ ...generateBuildId ? { generateBuildId } : {},
412
+ ...answerTurbopackGuard ? { turbopack: {} } : {}
155
413
  };
156
414
  }
157
415
  // Annotate the CommonJS export names for ESM import in node:
package/dist/config.mjs CHANGED
@@ -1,6 +1,186 @@
1
1
  // src/with-canopy.ts
2
2
  import { createRequire } from "node:module";
3
+ import path2 from "node:path";
4
+
5
+ // src/sharp-tracing.ts
6
+ import { readFileSync, realpathSync, statSync } from "node:fs";
3
7
  import path from "node:path";
8
+ var LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"];
9
+ var LEGACY_LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
10
+ var NEXT_CONFIG_FILES = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.mts"];
11
+ var GLOB_METACHARACTERS = /[*?[\]{}(),!\\]/;
12
+ var LIBVIPS_PACKAGE = /^@img\/sharp-libvips-/;
13
+ var NATIVE_BINDING_PACKAGE = /^@img\/sharp-(?!libvips-)/;
14
+ function messageOf(err) {
15
+ if (err instanceof Error) return err.message;
16
+ if (typeof err === "string") return err;
17
+ return String(err);
18
+ }
19
+ function isFile(candidate) {
20
+ try {
21
+ return statSync(candidate).isFile();
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function isDirectory(candidate) {
27
+ try {
28
+ return statSync(candidate).isDirectory();
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ function isOutside(root, target) {
34
+ const relative = path.relative(root, target);
35
+ return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
36
+ }
37
+ function readManifest(packageDir) {
38
+ const parsed = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
39
+ const manifest = { optionalDependencyNames: [] };
40
+ if (typeof parsed !== "object" || parsed === null) return manifest;
41
+ if ("name" in parsed && typeof parsed.name === "string") manifest.name = parsed.name;
42
+ if ("version" in parsed && typeof parsed.version === "string") manifest.version = parsed.version;
43
+ if ("optionalDependencies" in parsed && typeof parsed.optionalDependencies === "object" && parsed.optionalDependencies !== null) {
44
+ manifest.optionalDependencyNames = Object.keys(parsed.optionalDependencies);
45
+ }
46
+ return manifest;
47
+ }
48
+ function nodeModulesLookupDirs(fromDir) {
49
+ const dirs = [];
50
+ for (let dir = path.resolve(fromDir); ; dir = path.dirname(dir)) {
51
+ if (path.basename(dir) !== "node_modules") dirs.push(path.join(dir, "node_modules"));
52
+ if (path.dirname(dir) === dir) return dirs;
53
+ }
54
+ }
55
+ function findInstalledPackage(fromDir, name) {
56
+ for (const lookupDir of nodeModulesLookupDirs(fromDir)) {
57
+ const candidate = path.join(lookupDir, name);
58
+ if (isFile(path.join(candidate, "package.json"))) return realpathSync(candidate);
59
+ }
60
+ return null;
61
+ }
62
+ function installedNextMajor(projectDir) {
63
+ try {
64
+ const nextDir = findInstalledPackage(projectDir, "next");
65
+ if (!nextDir) return null;
66
+ const major = Number.parseInt(readManifest(nextDir).version ?? "", 10);
67
+ return Number.isNaN(major) ? null : major;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ function hasNextConfig(dir) {
73
+ return NEXT_CONFIG_FILES.some((name) => isFile(path.join(dir, name)));
74
+ }
75
+ function findLockFileUpwards(startDir, names) {
76
+ for (let dir = startDir; ; dir = path.dirname(dir)) {
77
+ for (const name of names) {
78
+ const candidate = path.join(dir, name);
79
+ if (isFile(candidate)) return candidate;
80
+ }
81
+ if (path.dirname(dir) === dir) return null;
82
+ }
83
+ }
84
+ function resolveTracingRoot(input) {
85
+ const configured = input.outputFileTracingRoot || input.turbopackRoot;
86
+ if (configured) return path.resolve(input.projectDir, configured);
87
+ if (input.lockfileRoot === "closest") {
88
+ const closest = findLockFileUpwards(input.projectDir, LEGACY_LOCKFILES);
89
+ return closest ? path.dirname(closest) : input.projectDir;
90
+ }
91
+ let lockFile = findLockFileUpwards(input.projectDir, LOCKFILES);
92
+ if (!lockFile) return input.projectDir;
93
+ for (; ; ) {
94
+ const lockDir = path.dirname(lockFile);
95
+ const parentDir = path.dirname(lockDir);
96
+ if (parentDir === lockDir) break;
97
+ const outer = findLockFileUpwards(parentDir, LOCKFILES);
98
+ if (!outer) break;
99
+ lockFile = outer;
100
+ }
101
+ return path.dirname(lockFile);
102
+ }
103
+ function nativeLibraryDirsFor(sharpDir) {
104
+ const found = /* @__PURE__ */ new Set();
105
+ const collectLibvips = (fromDir, dependencyNames) => {
106
+ for (const name of dependencyNames.filter((n) => LIBVIPS_PACKAGE.test(n))) {
107
+ const dir = findInstalledPackage(fromDir, name);
108
+ if (dir) found.add(dir);
109
+ }
110
+ };
111
+ const sharpDependencies = readManifest(sharpDir).optionalDependencyNames;
112
+ collectLibvips(sharpDir, sharpDependencies);
113
+ for (const bindingName of sharpDependencies.filter((n) => NATIVE_BINDING_PACKAGE.test(n))) {
114
+ const bindingDir = findInstalledPackage(sharpDir, bindingName);
115
+ if (!bindingDir) continue;
116
+ const bindingDependencies = readManifest(bindingDir).optionalDependencyNames;
117
+ if (bindingDependencies.some((n) => LIBVIPS_PACKAGE.test(n))) {
118
+ collectLibvips(bindingDir, bindingDependencies);
119
+ } else {
120
+ found.add(bindingDir);
121
+ }
122
+ }
123
+ return [...found];
124
+ }
125
+ function sharpTracingIncludes(input) {
126
+ try {
127
+ const projectDir = realpathSync(input.projectDir);
128
+ const root = realpathSync(resolveTracingRoot(input));
129
+ if (isOutside(root, projectDir)) {
130
+ return {
131
+ includes: [],
132
+ problem: `the project directory ${projectDir} is outside the tracing root ${root}`
133
+ };
134
+ }
135
+ const sharpDirs = /* @__PURE__ */ new Set();
136
+ const canopycmsDir = findInstalledPackage(projectDir, "canopycms");
137
+ for (const candidate of [
138
+ canopycmsDir && findInstalledPackage(canopycmsDir, "sharp"),
139
+ findInstalledPackage(projectDir, "sharp")
140
+ ]) {
141
+ if (candidate && readManifest(candidate).name === "sharp") sharpDirs.add(candidate);
142
+ }
143
+ if (sharpDirs.size === 0) {
144
+ return {
145
+ includes: [],
146
+ problem: canopycmsDir ? `no sharp is installed for canopycms at ${canopycmsDir}` : `canopycms is not installed where ${projectDir} can find it`
147
+ };
148
+ }
149
+ const includes = /* @__PURE__ */ new Set();
150
+ const notes = [];
151
+ for (const sharpDir of sharpDirs) {
152
+ const libraryDirs = nativeLibraryDirsFor(sharpDir);
153
+ if (libraryDirs.length === 0) {
154
+ notes.push(
155
+ `no libvips package is installed for sharp at ${sharpDir}, and no installed native binding carries its own library`
156
+ );
157
+ }
158
+ for (const libraryDir of libraryDirs) {
159
+ const libDir = path.join(libraryDir, "lib");
160
+ if (!isDirectory(libDir)) {
161
+ notes.push(`${libDir} does not exist`);
162
+ continue;
163
+ }
164
+ if (isOutside(root, libDir)) {
165
+ notes.push(`${libDir} is outside the tracing root ${root}`);
166
+ continue;
167
+ }
168
+ const relative = path.relative(projectDir, libDir).split(path.sep).join("/");
169
+ if (GLOB_METACHARACTERS.test(relative)) {
170
+ notes.push(`${relative} contains a glob metacharacter`);
171
+ continue;
172
+ }
173
+ includes.add(`${relative}/**/*`);
174
+ }
175
+ }
176
+ if (includes.size === 0) return { includes: [], problem: notes.join("; ") };
177
+ return { includes: [...includes] };
178
+ } catch (err) {
179
+ return { includes: [], problem: messageOf(err) };
180
+ }
181
+ }
182
+
183
+ // src/with-canopy.ts
4
184
  var REQUIRED_PACKAGES = ["canopycms"];
5
185
  var OPTIONAL_PACKAGES = [
6
186
  "canopycms-next",
@@ -34,8 +214,8 @@ function withAssetsRewrite(existingRewrites) {
34
214
  function resolveReactAliases(resolve) {
35
215
  try {
36
216
  return {
37
- react: path.dirname(resolve("react")),
38
- "react-dom": path.dirname(resolve("react-dom"))
217
+ react: path2.dirname(resolve("react")),
218
+ "react-dom": path2.dirname(resolve("react-dom"))
39
219
  };
40
220
  } catch {
41
221
  return null;
@@ -64,8 +244,82 @@ function resolveStaticBuildId() {
64
244
  }
65
245
  return trimmed;
66
246
  }
247
+ var ALL_ROUTES = "/**";
248
+ function isTracingIncludes(value) {
249
+ return typeof value === "object" && value !== null && Object.values(value).every(
250
+ (globs) => Array.isArray(globs) && globs.every((g) => typeof g === "string")
251
+ );
252
+ }
253
+ function readProperty(value, key) {
254
+ return typeof value === "object" && value !== null ? Reflect.get(value, key) : void 0;
255
+ }
256
+ function stringOrUndefined(value) {
257
+ return typeof value === "string" ? value : void 0;
258
+ }
259
+ function isSet(value) {
260
+ return value !== void 0 && value !== null;
261
+ }
262
+ function mergeTracingIncludes(existing, includes) {
263
+ return {
264
+ ...existing,
265
+ [ALL_ROUTES]: [.../* @__PURE__ */ new Set([...existing?.[ALL_ROUTES] ?? [], ...includes])]
266
+ };
267
+ }
268
+ function sharpTracingWarning(problem, nextVersionUnknown) {
269
+ return "CanopyCMS: could not add sharp's libvips to this standalone build's file tracing" + (problem ? ` (${problem})` : "") + ". Next.js file tracing can miss that library for sharp 0.35 (https://github.com/vercel/next.js/issues/97973), and the built server then fails to load sharp with ERR_DLOPEN_FAILED. Add it in next.config yourself, with paths relative to the project directory. For pnpm: outputFileTracingIncludes: { '/**': ['node_modules/.pnpm/@img+sharp-libvips-*/node_modules/@img/*/lib/**/*'] }. For npm: outputFileTracingIncludes: { '/**': ['node_modules/@img/sharp-libvips-*/lib/**/*'] }." + (nextVersionUnknown ? " The installed Next.js version could not be read either: on Next 13 or 14, put outputFileTracingIncludes under experimental." : "");
270
+ }
271
+ function unknownNextVersionWarning(projectDir) {
272
+ return `CanopyCMS: could not read the version of the next package installed for ${projectDir}, so sharp's libvips was added to the top-level outputFileTracingIncludes key, which Next 15 and later read. Next 13 and 14 ignore that key: on those versions, move the entry under experimental.outputFileTracingIncludes.`;
273
+ }
274
+ var warnedAboutSharpTracing = false;
275
+ function sharpTracingConfig(nextConfig, options) {
276
+ if (options.staticBuild || nextConfig.output === "export") return {};
277
+ const projectDir = process.cwd();
278
+ const nextMajor = installedNextMajor(projectDir);
279
+ const experimental = nextConfig.experimental ?? {};
280
+ const experimentalOnly = nextMajor !== null && nextMajor < 15;
281
+ const includesUnderExperimental = experimentalOnly || isSet(readProperty(experimental, "outputFileTracingIncludes"));
282
+ const rootUnderExperimental = experimentalOnly || isSet(readProperty(experimental, "outputFileTracingRoot"));
283
+ const writtenIncludes = includesUnderExperimental ? readProperty(experimental, "outputFileTracingIncludes") : nextConfig.outputFileTracingIncludes;
284
+ const existing = isSet(writtenIncludes) ? writtenIncludes : void 0;
285
+ if (existing !== void 0 && !isTracingIncludes(existing)) return {};
286
+ const configuredRoot = stringOrUndefined(
287
+ rootUnderExperimental ? readProperty(experimental, "outputFileTracingRoot") : nextConfig.outputFileTracingRoot
288
+ );
289
+ const turbopack = nextConfig.turbopack;
290
+ const turbopackHasRootKey = typeof turbopack === "object" && turbopack !== null && "root" in turbopack;
291
+ const legacyTurbopackRoot = nextMajor === 15 && !turbopackHasRootKey ? stringOrUndefined(readProperty(readProperty(experimental, "turbo"), "root")) : void 0;
292
+ const turbopackRoot = experimentalOnly ? void 0 : stringOrUndefined(readProperty(turbopack, "root")) ?? legacyTurbopackRoot;
293
+ const { includes, problem } = hasNextConfig(projectDir) ? sharpTracingIncludes({
294
+ projectDir,
295
+ outputFileTracingRoot: configuredRoot,
296
+ turbopackRoot,
297
+ lockfileRoot: experimentalOnly ? "closest" : "outermost"
298
+ }) : {
299
+ includes: [],
300
+ problem: `the working directory ${projectDir} has no next.config file, so withCanopy cannot tell where the project is`
301
+ };
302
+ const nextVersionUnknown = nextMajor === null;
303
+ if (includes.length === 0) {
304
+ if (nextConfig.output === "standalone" && !warnedAboutSharpTracing) {
305
+ warnedAboutSharpTracing = true;
306
+ console.warn(sharpTracingWarning(problem, nextVersionUnknown));
307
+ }
308
+ return {};
309
+ }
310
+ const merged = mergeTracingIncludes(existing, includes);
311
+ if (!includesUnderExperimental) {
312
+ if (nextVersionUnknown && nextConfig.output === "standalone" && !warnedAboutSharpTracing) {
313
+ warnedAboutSharpTracing = true;
314
+ console.warn(unknownNextVersionWarning(projectDir));
315
+ }
316
+ return { outputFileTracingIncludes: merged };
317
+ }
318
+ const legacyExperimental = { ...experimental, outputFileTracingIncludes: merged };
319
+ return { experimental: legacyExperimental };
320
+ }
67
321
  function withCanopy(nextConfig = {}, options = {}) {
68
- const resolve = createRequire(path.join(process.cwd(), "noop.js")).resolve;
322
+ const resolve = createRequire(path2.join(process.cwd(), "noop.js")).resolve;
69
323
  const installedOptional = OPTIONAL_PACKAGES.filter((pkg) => {
70
324
  try {
71
325
  resolve(pkg);
@@ -100,6 +354,8 @@ function withCanopy(nextConfig = {}, options = {}) {
100
354
  }
101
355
  return config;
102
356
  } : existingWebpack;
357
+ const nextMajor = installedNextMajor(process.cwd());
358
+ const answerTurbopackGuard = webpack !== void 0 && !isSet(existingWebpack) && !isSet(nextConfig.turbopack) && nextMajor !== null && nextMajor >= 16;
103
359
  const pageExtensions = [
104
360
  .../* @__PURE__ */ new Set([
105
361
  ...nextConfig.pageExtensions ?? NEXTJS_DEFAULT_PAGE_EXTENSIONS,
@@ -113,9 +369,11 @@ function withCanopy(nextConfig = {}, options = {}) {
113
369
  pageExtensions,
114
370
  webpack,
115
371
  rewrites: withAssetsRewrite(nextConfig.rewrites),
372
+ ...sharpTracingConfig(nextConfig, options),
116
373
  // Spread conditionally: emitting `generateBuildId: undefined` would be a key Next has to
117
374
  // reason about, where absence is unambiguous.
118
- ...generateBuildId ? { generateBuildId } : {}
375
+ ...generateBuildId ? { generateBuildId } : {},
376
+ ...answerTurbopackGuard ? { turbopack: {} } : {}
119
377
  };
120
378
  }
121
379
  export {
@@ -183,10 +183,11 @@ export async function createNextCanopyContext(options) {
183
183
  //
184
184
  // No base-branch context resolution here (unlike the previous copy):
185
185
  // that call existed only to source groups.json from the (wrong) content
186
- // branch. Actual content reads (buildContentTree/listEntries/read) already
187
- // provision the base/active branch themselves via loadOrCreateBranchContext
188
- // (see context.ts's resolveSchemaContext), so dropping it here removes a
189
- // redundant per-request EFS round-trip rather than losing provisioning.
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.
190
191
  const extractUser = async () => {
191
192
  const headersList = await headers();
192
193
  const authResult = await authPlugin.authenticate(headersList);
@@ -0,0 +1,57 @@
1
+ export interface SharpTracingInput {
2
+ /** The directory `next build` runs for. `withCanopy` passes `process.cwd()`. */
3
+ projectDir: string;
4
+ /** The adopter's `outputFileTracingRoot`, as written (relative values resolve like Next's). */
5
+ outputFileTracingRoot?: string;
6
+ /** The adopter's `turbopack.root`, as written. */
7
+ turbopackRoot?: string;
8
+ /**
9
+ * Which lockfile a root nobody configured comes from.
10
+ * - `'outermost'`, the default: Next 15 and 16 walk up to the outermost lockfile (`findRootDir` in
11
+ * 15.5.21, `findRootDirAndLockFiles` in 16.1.7, both in `next/dist/lib/find-root.js`).
12
+ * - `'closest'`: Next 13 and 14 stop at the nearest one. Their `findRootDir` is a different
13
+ * function with the same name, called from `assignDefaults` in `server/config.ts` (14.2.25 lines
14
+ * 629-630, 13.5.7 lines 524-525).
15
+ */
16
+ lockfileRoot?: 'outermost' | 'closest';
17
+ }
18
+ export interface SharpTracingResult {
19
+ /** Project-relative POSIX globs, one per native-library `lib/` directory found. */
20
+ includes: string[];
21
+ /** Why nothing was found. Set exactly when `includes` is empty. */
22
+ problem?: string;
23
+ }
24
+ /**
25
+ * The real directory of package `name` as a bundler would find it from `fromDir`, or null.
26
+ *
27
+ * Deliberately not `require.resolve(name)`. That goes through the package's `exports` map, and two
28
+ * of the packages this module needs defeat it:
29
+ * - a `canopycms` published without a `require` condition throws ERR_PACKAGE_PATH_NOT_EXPORTED;
30
+ * - sharp exports no `./package.json` at all.
31
+ */
32
+ export declare function findInstalledPackage(fromDir: string, name: string): string | null;
33
+ /** The major version of the `next` installed for `projectDir`, or null if it cannot be read. */
34
+ export declare function installedNextMajor(projectDir: string): number | null;
35
+ /** Whether `dir` holds a config file Next would load, i.e. whether it can be Next's project dir. */
36
+ export declare function hasNextConfig(dir: string): boolean;
37
+ /**
38
+ * The tracing root Next will use (`loadConfig` in `next/dist/server/config.js`).
39
+ *
40
+ * The precedence is `outputFileTracingRoot`, then `turbopack.root`, then a lockfile's directory:
41
+ * - by default, as in Next 15 and 16: the outermost lockfile, found by searching upwards again from
42
+ * each found lockfile's parent;
43
+ * - with `lockfileRoot: 'closest'`, as in Next 13 and 14: the nearest one.
44
+ *
45
+ * `SharpTracingInput.lockfileRoot` cites the Next source for each.
46
+ *
47
+ * Next resolves a relative configured root against the working directory; `withCanopy` only calls
48
+ * this when that directory is the project dir.
49
+ */
50
+ export declare function resolveTracingRoot(input: SharpTracingInput): string;
51
+ /**
52
+ * The `outputFileTracingIncludes` globs that ship sharp's libvips, or why there are none.
53
+ *
54
+ * Never throws. A failure anywhere becomes `problem`, because this runs inside the adopter's
55
+ * `next.config` and must not be the thing that breaks their build.
56
+ */
57
+ export declare function sharpTracingIncludes(input: SharpTracingInput): SharpTracingResult;
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Finds sharp's libvips shared library so `withCanopy` can add it to Next.js output file tracing.
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.
9
+ *
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`).
55
+ */
56
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
57
+ import path from 'node:path';
58
+ /**
59
+ * The lockfiles Next 15 and later look for when inferring a root, in their order
60
+ * (`findRootLockFile` in `next/dist/lib/find-root.js`).
61
+ */
62
+ const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'bun.lock', 'bun.lockb'];
63
+ /**
64
+ * The same list in Next 14.2.25 (`lib/find-root.ts`), which predates `bun.lock`. Next 13.5.7's list
65
+ * also lacks `bun.lockb`. So for a Bun project on Next 13, this list can pick a wider root than
66
+ * Next does.
67
+ */
68
+ const LEGACY_LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'bun.lockb'];
69
+ /**
70
+ * The config file names Next loads (`CONFIG_FILES` in `next/dist/shared/lib/constants.js`).
71
+ * `.mts` is Next 16's, loaded when Node supports TypeScript.
72
+ */
73
+ const NEXT_CONFIG_FILES = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.mts'];
74
+ /**
75
+ * Characters that make a path mean something else as a glob.
76
+ *
77
+ * - Turbopack's glob parser treats `? * [ { } , \` specially (`turbo-tasks-fs/src/globset.rs:397-403`
78
+ * at v16.1.7), and `]` is refused along with `[`. The comma matters because Turbopack joins a
79
+ * route's includes into one `{a,b}` alternation (`crates/next-api/src/nft_json.rs:336`).
80
+ * - Next's JS tracer globs includes with its compiled `glob` (`collect-build-traces.js:424` in
81
+ * 16.1.7), whose minimatch also reads `( ) !` as extglob syntax.
82
+ *
83
+ * `@` and `+` stay allowed: pnpm directory names are full of them, and neither tracer gives them a
84
+ * meaning unless `(` follows.
85
+ */
86
+ const GLOB_METACHARACTERS = /[*?[\]{}(),!\\]/;
87
+ const LIBVIPS_PACKAGE = /^@img\/sharp-libvips-/;
88
+ const NATIVE_BINDING_PACKAGE = /^@img\/sharp-(?!libvips-)/;
89
+ /**
90
+ * `getErrorMessage` from `canopycms/utils/error`, restated.
91
+ *
92
+ * This module is bundled into `dist/config.*`, which esbuild builds with `--packages=external`. An
93
+ * import of that subpath would stay a runtime import inside the adopter's `next.config`, and in this
94
+ * workspace that subpath resolves to raw TypeScript. ARCHITECTURE.md explains why this entrypoint
95
+ * must be plain JavaScript all the way down ("Why the `./config` export ships pre-built").
96
+ */
97
+ function messageOf(err) {
98
+ if (err instanceof Error)
99
+ return err.message;
100
+ if (typeof err === 'string')
101
+ return err;
102
+ return String(err);
103
+ }
104
+ /**
105
+ * Whether `candidate` is a regular file.
106
+ *
107
+ * Any failed stat answers false instead of throwing: a missing path, a race with a delete, or a
108
+ * permission error. `hasNextConfig` calls this outside any `try`, and it runs inside the adopter's
109
+ * `next.config`.
110
+ */
111
+ function isFile(candidate) {
112
+ try {
113
+ return statSync(candidate).isFile();
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ /** Whether `candidate` is a directory, answering false on any failed stat, as `isFile` does. */
120
+ function isDirectory(candidate) {
121
+ try {
122
+ return statSync(candidate).isDirectory();
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ /** True when `target` is not `root` itself or somewhere beneath it. */
129
+ function isOutside(root, target) {
130
+ const relative = path.relative(root, target);
131
+ return relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
132
+ }
133
+ function readManifest(packageDir) {
134
+ const parsed = JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
135
+ const manifest = { optionalDependencyNames: [] };
136
+ if (typeof parsed !== 'object' || parsed === null)
137
+ return manifest;
138
+ if ('name' in parsed && typeof parsed.name === 'string')
139
+ manifest.name = parsed.name;
140
+ if ('version' in parsed && typeof parsed.version === 'string')
141
+ manifest.version = parsed.version;
142
+ if ('optionalDependencies' in parsed &&
143
+ typeof parsed.optionalDependencies === 'object' &&
144
+ parsed.optionalDependencies !== null) {
145
+ manifest.optionalDependencyNames = Object.keys(parsed.optionalDependencies);
146
+ }
147
+ return manifest;
148
+ }
149
+ /**
150
+ * The `node_modules` directories a bare specifier is looked up in from `fromDir`, nearest first.
151
+ *
152
+ * This is only the hierarchy half of Node's lookup (`Module._nodeModulePaths`), on purpose.
153
+ * `require.resolve.paths` also appends Node's global folders (`~/.node_modules`,
154
+ * `~/.node_libraries`, the install prefix's `lib/node`, and `NODE_PATH`), which bundlers do not
155
+ * search. pnpm's bin shims add more: running vitest through `node_modules/.bin`, `NODE_PATH` also
156
+ * holds vitest's store directories and the repo's hoisted `node_modules/.pnpm/node_modules`. A
157
+ * package found that way is not one the build resolves.
158
+ */
159
+ function nodeModulesLookupDirs(fromDir) {
160
+ const dirs = [];
161
+ for (let dir = path.resolve(fromDir);; dir = path.dirname(dir)) {
162
+ if (path.basename(dir) !== 'node_modules')
163
+ dirs.push(path.join(dir, 'node_modules'));
164
+ if (path.dirname(dir) === dir)
165
+ return dirs;
166
+ }
167
+ }
168
+ /**
169
+ * The real directory of package `name` as a bundler would find it from `fromDir`, or null.
170
+ *
171
+ * Deliberately not `require.resolve(name)`. That goes through the package's `exports` map, and two
172
+ * of the packages this module needs defeat it:
173
+ * - a `canopycms` published without a `require` condition throws ERR_PACKAGE_PATH_NOT_EXPORTED;
174
+ * - sharp exports no `./package.json` at all.
175
+ */
176
+ export function findInstalledPackage(fromDir, name) {
177
+ for (const lookupDir of nodeModulesLookupDirs(fromDir)) {
178
+ const candidate = path.join(lookupDir, name);
179
+ if (isFile(path.join(candidate, 'package.json')))
180
+ return realpathSync(candidate);
181
+ }
182
+ return null;
183
+ }
184
+ /** The major version of the `next` installed for `projectDir`, or null if it cannot be read. */
185
+ export function installedNextMajor(projectDir) {
186
+ try {
187
+ const nextDir = findInstalledPackage(projectDir, 'next');
188
+ if (!nextDir)
189
+ return null;
190
+ const major = Number.parseInt(readManifest(nextDir).version ?? '', 10);
191
+ return Number.isNaN(major) ? null : major;
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ }
197
+ /** Whether `dir` holds a config file Next would load, i.e. whether it can be Next's project dir. */
198
+ export function hasNextConfig(dir) {
199
+ return NEXT_CONFIG_FILES.some((name) => isFile(path.join(dir, name)));
200
+ }
201
+ function findLockFileUpwards(startDir, names) {
202
+ for (let dir = startDir;; dir = path.dirname(dir)) {
203
+ for (const name of names) {
204
+ const candidate = path.join(dir, name);
205
+ if (isFile(candidate))
206
+ return candidate;
207
+ }
208
+ if (path.dirname(dir) === dir)
209
+ return null;
210
+ }
211
+ }
212
+ /**
213
+ * The tracing root Next will use (`loadConfig` in `next/dist/server/config.js`).
214
+ *
215
+ * The precedence is `outputFileTracingRoot`, then `turbopack.root`, then a lockfile's directory:
216
+ * - by default, as in Next 15 and 16: the outermost lockfile, found by searching upwards again from
217
+ * each found lockfile's parent;
218
+ * - with `lockfileRoot: 'closest'`, as in Next 13 and 14: the nearest one.
219
+ *
220
+ * `SharpTracingInput.lockfileRoot` cites the Next source for each.
221
+ *
222
+ * Next resolves a relative configured root against the working directory; `withCanopy` only calls
223
+ * this when that directory is the project dir.
224
+ */
225
+ export function resolveTracingRoot(input) {
226
+ const configured = input.outputFileTracingRoot || input.turbopackRoot;
227
+ if (configured)
228
+ return path.resolve(input.projectDir, configured);
229
+ if (input.lockfileRoot === 'closest') {
230
+ const closest = findLockFileUpwards(input.projectDir, LEGACY_LOCKFILES);
231
+ return closest ? path.dirname(closest) : input.projectDir;
232
+ }
233
+ let lockFile = findLockFileUpwards(input.projectDir, LOCKFILES);
234
+ if (!lockFile)
235
+ return input.projectDir;
236
+ for (;;) {
237
+ const lockDir = path.dirname(lockFile);
238
+ const parentDir = path.dirname(lockDir);
239
+ if (parentDir === lockDir)
240
+ break;
241
+ const outer = findLockFileUpwards(parentDir, LOCKFILES);
242
+ if (!outer)
243
+ break;
244
+ lockFile = outer;
245
+ }
246
+ return path.dirname(lockFile);
247
+ }
248
+ /**
249
+ * Every installed package directory, reachable from one copy of sharp, whose `lib/` holds sharp's
250
+ * native library.
251
+ *
252
+ * That is each `@img/sharp-libvips-*` package, plus any installed native binding that lists no
253
+ * libvips package and so has to carry the library itself.
254
+ */
255
+ function nativeLibraryDirsFor(sharpDir) {
256
+ const found = new Set();
257
+ const collectLibvips = (fromDir, dependencyNames) => {
258
+ for (const name of dependencyNames.filter((n) => LIBVIPS_PACKAGE.test(n))) {
259
+ const dir = findInstalledPackage(fromDir, name);
260
+ if (dir)
261
+ found.add(dir);
262
+ }
263
+ };
264
+ const sharpDependencies = readManifest(sharpDir).optionalDependencyNames;
265
+ collectLibvips(sharpDir, sharpDependencies);
266
+ for (const bindingName of sharpDependencies.filter((n) => NATIVE_BINDING_PACKAGE.test(n))) {
267
+ const bindingDir = findInstalledPackage(sharpDir, bindingName);
268
+ if (!bindingDir)
269
+ continue;
270
+ const bindingDependencies = readManifest(bindingDir).optionalDependencyNames;
271
+ if (bindingDependencies.some((n) => LIBVIPS_PACKAGE.test(n))) {
272
+ collectLibvips(bindingDir, bindingDependencies);
273
+ }
274
+ else {
275
+ found.add(bindingDir);
276
+ }
277
+ }
278
+ return [...found];
279
+ }
280
+ /**
281
+ * The `outputFileTracingIncludes` globs that ship sharp's libvips, or why there are none.
282
+ *
283
+ * Never throws. A failure anywhere becomes `problem`, because this runs inside the adopter's
284
+ * `next.config` and must not be the thing that breaks their build.
285
+ */
286
+ export function sharpTracingIncludes(input) {
287
+ try {
288
+ const projectDir = realpathSync(input.projectDir);
289
+ const root = realpathSync(resolveTracingRoot(input));
290
+ if (isOutside(root, projectDir)) {
291
+ return {
292
+ includes: [],
293
+ problem: `the project directory ${projectDir} is outside the tracing root ${root}`,
294
+ };
295
+ }
296
+ const sharpDirs = new Set();
297
+ const canopycmsDir = findInstalledPackage(projectDir, 'canopycms');
298
+ for (const candidate of [
299
+ canopycmsDir && findInstalledPackage(canopycmsDir, 'sharp'),
300
+ findInstalledPackage(projectDir, 'sharp'),
301
+ ]) {
302
+ if (candidate && readManifest(candidate).name === 'sharp')
303
+ sharpDirs.add(candidate);
304
+ }
305
+ if (sharpDirs.size === 0) {
306
+ return {
307
+ includes: [],
308
+ problem: canopycmsDir
309
+ ? `no sharp is installed for canopycms at ${canopycmsDir}`
310
+ : `canopycms is not installed where ${projectDir} can find it`,
311
+ };
312
+ }
313
+ const includes = new Set();
314
+ const notes = [];
315
+ for (const sharpDir of sharpDirs) {
316
+ const libraryDirs = nativeLibraryDirsFor(sharpDir);
317
+ if (libraryDirs.length === 0) {
318
+ notes.push(`no libvips package is installed for sharp at ${sharpDir}, and no installed native binding carries its own library`);
319
+ }
320
+ for (const libraryDir of libraryDirs) {
321
+ const libDir = path.join(libraryDir, 'lib');
322
+ if (!isDirectory(libDir)) {
323
+ notes.push(`${libDir} does not exist`);
324
+ continue;
325
+ }
326
+ // Turbopack fails the whole build, not just this include, on a glob that climbs out of
327
+ // the tracing root.
328
+ if (isOutside(root, libDir)) {
329
+ notes.push(`${libDir} is outside the tracing root ${root}`);
330
+ continue;
331
+ }
332
+ const relative = path.relative(projectDir, libDir).split(path.sep).join('/');
333
+ if (GLOB_METACHARACTERS.test(relative)) {
334
+ notes.push(`${relative} contains a glob metacharacter`);
335
+ continue;
336
+ }
337
+ includes.add(`${relative}/**/*`);
338
+ }
339
+ }
340
+ if (includes.size === 0)
341
+ return { includes: [], problem: notes.join('; ') };
342
+ return { includes: [...includes] };
343
+ }
344
+ catch (err) {
345
+ return { includes: [], problem: messageOf(err) };
346
+ }
347
+ }
@@ -39,6 +39,20 @@ export interface WithCanopyOptions {
39
39
  * - With `staticBuild: true`, honors `CANOPY_BUILD_ID` as Next's build id (Next's default is
40
40
  * random, which puts two builds of one source tree in different `_next/static/` directories).
41
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.
42
56
  *
43
57
  * **When you need this:**
44
58
  * - Always recommended — it replaces manual `transpilePackages` configuration
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import path from 'node:path';
3
+ import { hasNextConfig, installedNextMajor, sharpTracingIncludes } from './sharp-tracing.js';
3
4
  /** The core package — always required when using withCanopy. */
4
5
  const REQUIRED_PACKAGES = ['canopycms'];
5
6
  /**
@@ -160,6 +161,166 @@ function resolveStaticBuildId() {
160
161
  }
161
162
  return trimmed;
162
163
  }
164
+ /**
165
+ * Every route. Both tracers match include keys as a "contains" glob against the route:
166
+ * - Turbopack against "/" plus the page name (`crates/next-api/src/nft_json.rs` lines 56 and 312 at
167
+ * v16.1.7);
168
+ * - the JS tracer through picomatch with `contains: true` (`next/dist/build/collect-build-traces.js`
169
+ * lines 464 and 475 in 16.1.7).
170
+ */
171
+ const ALL_ROUTES = '/**';
172
+ function isTracingIncludes(value) {
173
+ return (typeof value === 'object' &&
174
+ value !== null &&
175
+ Object.values(value).every((globs) => Array.isArray(globs) && globs.every((g) => typeof g === 'string')));
176
+ }
177
+ /** `value[key]` for any object, else undefined: for config keys Next's types no longer declare. */
178
+ function readProperty(value, key) {
179
+ return typeof value === 'object' && value !== null ? Reflect.get(value, key) : undefined;
180
+ }
181
+ function stringOrUndefined(value) {
182
+ return typeof value === 'string' ? value : undefined;
183
+ }
184
+ /**
185
+ * Whether Next treats a config value as set.
186
+ *
187
+ * `assignDefaults` in `next/dist/server/config.js` (`assignDefaultsAndValidate` in 16.1.7) drops
188
+ * `undefined` and `null` values before it migrates legacy keys. That covers top-level keys and, for
189
+ * object options such as `experimental`, their direct keys. It does not cover Next 15's
190
+ * `experimental.turbo` merge, which runs on the raw config earlier.
191
+ */
192
+ function isSet(value) {
193
+ return value !== undefined && value !== null;
194
+ }
195
+ /** `includes` added under `'/**'`, deduped. Every entry the adopter already wrote is kept as written. */
196
+ function mergeTracingIncludes(existing, includes) {
197
+ return {
198
+ ...existing,
199
+ [ALL_ROUTES]: [...new Set([...(existing?.[ALL_ROUTES] ?? []), ...includes])],
200
+ };
201
+ }
202
+ function sharpTracingWarning(problem, nextVersionUnknown) {
203
+ return ("CanopyCMS: could not add sharp's libvips to this standalone build's file tracing" +
204
+ (problem ? ` (${problem})` : '') +
205
+ '. Next.js file tracing can miss that library for sharp 0.35 ' +
206
+ '(https://github.com/vercel/next.js/issues/97973), and the built server then fails to load ' +
207
+ 'sharp with ERR_DLOPEN_FAILED. Add it in next.config yourself, with paths relative to the ' +
208
+ "project directory. For pnpm: outputFileTracingIncludes: { '/**': " +
209
+ "['node_modules/.pnpm/@img+sharp-libvips-*/node_modules/@img/*/lib/**/*'] }. For npm: " +
210
+ "outputFileTracingIncludes: { '/**': ['node_modules/@img/sharp-libvips-*/lib/**/*'] }." +
211
+ (nextVersionUnknown
212
+ ? ' The installed Next.js version could not be read either: on Next 13 or 14, put ' +
213
+ 'outputFileTracingIncludes under experimental.'
214
+ : ''));
215
+ }
216
+ /**
217
+ * The warning for an include that WAS written, under a key chosen without knowing the Next version.
218
+ *
219
+ * That choice is always the top-level key. Next 13 and 14 ignore it, because their config schema
220
+ * declares the option only under `experimental` (`server/config-schema.ts` line 311 in 14.2.25).
221
+ */
222
+ function unknownNextVersionWarning(projectDir) {
223
+ return (`CanopyCMS: could not read the version of the next package installed for ${projectDir}, so ` +
224
+ "sharp's libvips was added to the top-level outputFileTracingIncludes key, which Next 15 and " +
225
+ 'later read. Next 13 and 14 ignore that key: on those versions, move the entry under ' +
226
+ 'experimental.outputFileTracingIncludes.');
227
+ }
228
+ /**
229
+ * Whether the warning has fired in this module instance.
230
+ *
231
+ * A Turbopack build evaluates the config in the main process and again in a worker thread with its
232
+ * own module registry (`next/dist/build/turbopack-build/index.js:26` and `impl.js:209` in 16.1.7).
233
+ * So "once" means once per process or thread, not once per build.
234
+ */
235
+ let warnedAboutSharpTracing = false;
236
+ /**
237
+ * The config keys that make a Turbopack `output: 'standalone'` server able to load sharp.
238
+ *
239
+ * The rules live in `./sharp-tracing`. This function decides only whether to apply them, and under
240
+ * which key.
241
+ */
242
+ function sharpTracingConfig(nextConfig, options) {
243
+ // A static export has no server, so there is nothing to trace for.
244
+ if (options.staticBuild || nextConfig.output === 'export')
245
+ return {};
246
+ const projectDir = process.cwd();
247
+ const nextMajor = installedNextMajor(projectDir);
248
+ // May not be an object in an untyped config (`experimental: true` loads fine in Next), which is why
249
+ // every read of it below goes through `readProperty`, never an `in` check.
250
+ const experimental = nextConfig.experimental ?? {};
251
+ // Read and write each option where Next reads it (`loadConfig` in `next/dist/server/config.js`).
252
+ // A legacy `experimental` value of `undefined` or `null` counts as unset, because Next drops those
253
+ // before migrating legacy keys (see `isSet`).
254
+ // - Next 13 and 14 read the tracing options only under `experimental`.
255
+ // - A top-level key is reported as an invalid option and ignored. Their config schema is a
256
+ // strict object, and only `images` errors stop the build (`server/config.ts:84-90` in 14.2.25).
257
+ // - A root nobody configured comes from the CLOSEST lockfile there (`findRootDir`, called from
258
+ // `assignDefaults`), not the outermost.
259
+ // - Next 15 and 16 still accept the `experimental` spellings, and copy any that is set over the
260
+ // top-level key (`warnOptionHasBeenMovedOutOfExperimental`, `config.js:542-544` in 16.1.7 and
261
+ // `:546-548` in 15.5.21). So a legacy value wins, and an include written only at the top level
262
+ // would be silently replaced.
263
+ // - Only Next 15 merges `experimental.turbo` into `turbopack`, as `{ ...turbo, ...turbopack }`
264
+ // (`config.js:1190` in 15.5.21). Any `root` key on `turbopack` therefore wins, even an empty one.
265
+ const experimentalOnly = nextMajor !== null && nextMajor < 15;
266
+ const includesUnderExperimental = experimentalOnly || isSet(readProperty(experimental, 'outputFileTracingIncludes'));
267
+ const rootUnderExperimental = experimentalOnly || isSet(readProperty(experimental, 'outputFileTracingRoot'));
268
+ const writtenIncludes = includesUnderExperimental
269
+ ? readProperty(experimental, 'outputFileTracingIncludes')
270
+ : nextConfig.outputFileTracingIncludes;
271
+ const existing = isSet(writtenIncludes) ? writtenIncludes : undefined;
272
+ // A malformed value is Next's to reject. Merging into it could only turn that into a crash here.
273
+ if (existing !== undefined && !isTracingIncludes(existing))
274
+ return {};
275
+ const configuredRoot = stringOrUndefined(rootUnderExperimental
276
+ ? readProperty(experimental, 'outputFileTracingRoot')
277
+ : nextConfig.outputFileTracingRoot);
278
+ const turbopack = nextConfig.turbopack;
279
+ const turbopackHasRootKey = typeof turbopack === 'object' && turbopack !== null && 'root' in turbopack;
280
+ const legacyTurbopackRoot = nextMajor === 15 && !turbopackHasRootKey
281
+ ? stringOrUndefined(readProperty(readProperty(experimental, 'turbo'), 'root'))
282
+ : undefined;
283
+ const turbopackRoot = experimentalOnly
284
+ ? undefined
285
+ : (stringOrUndefined(readProperty(turbopack, 'root')) ?? legacyTurbopackRoot);
286
+ // Next never changes directory for `next build <dir>`: in 16.1.7 its only `process.chdir` is in
287
+ // the generated standalone `server.js` (`next/dist/build/utils.js:1111`). So a working directory
288
+ // with no config file in it is not the project, and any glob computed relative to it would be
289
+ // wrong.
290
+ const { includes, problem } = hasNextConfig(projectDir)
291
+ ? sharpTracingIncludes({
292
+ projectDir,
293
+ outputFileTracingRoot: configuredRoot,
294
+ turbopackRoot,
295
+ lockfileRoot: experimentalOnly ? 'closest' : 'outermost',
296
+ })
297
+ : {
298
+ includes: [],
299
+ problem: `the working directory ${projectDir} has no next.config file, so withCanopy cannot tell where the project is`,
300
+ };
301
+ const nextVersionUnknown = nextMajor === null;
302
+ if (includes.length === 0) {
303
+ if (nextConfig.output === 'standalone' && !warnedAboutSharpTracing) {
304
+ warnedAboutSharpTracing = true;
305
+ console.warn(sharpTracingWarning(problem, nextVersionUnknown));
306
+ }
307
+ return {};
308
+ }
309
+ const merged = mergeTracingIncludes(existing, includes);
310
+ if (!includesUnderExperimental) {
311
+ // An unreadable version falls back to the key Next 15 and later read. A standalone build says
312
+ // so: Next 13 and 14 ignore that key, so libvips would not be traced.
313
+ if (nextVersionUnknown && nextConfig.output === 'standalone' && !warnedAboutSharpTracing) {
314
+ warnedAboutSharpTracing = true;
315
+ console.warn(unknownNextVersionWarning(projectDir));
316
+ }
317
+ return { outputFileTracingIncludes: merged };
318
+ }
319
+ // Built as a variable, not returned as a literal, because Next 15's `ExperimentalConfig` type no
320
+ // longer declares this key.
321
+ const legacyExperimental = { ...experimental, outputFileTracingIncludes: merged };
322
+ return { experimental: legacyExperimental };
323
+ }
163
324
  /**
164
325
  * Wrap your Next.js config to set up module transpilation and React
165
326
  * resolution for CanopyCMS packages.
@@ -177,6 +338,20 @@ function resolveStaticBuildId() {
177
338
  * - With `staticBuild: true`, honors `CANOPY_BUILD_ID` as Next's build id (Next's default is
178
339
  * random, which puts two builds of one source tree in different `_next/static/` directories).
179
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.
180
355
  *
181
356
  * **When you need this:**
182
357
  * - Always recommended — it replaces manual `transpilePackages` configuration
@@ -251,6 +426,31 @@ export function withCanopy(nextConfig = {}, options = {}) {
251
426
  // Until Turbopack supports absolute path aliases, consumers using
252
427
  // file: symlinks must use `next dev --webpack` for local development.
253
428
  // Turbopack works fine when canopycms is installed from npm (no symlinks).
429
+ // Next 16 defaults both `next build` and `next dev` to Turbopack, marking the default with
430
+ // `TURBOPACK=auto` (`next/dist/lib/bundler.js:76`), and then exits with "This build is using
431
+ // Turbopack, with a `webpack` config and no `turbopack` config" whenever the exported config's
432
+ // `webpack` is truthy, its `turbopack` is not, and it has no `experimental.turbo*` key
433
+ // (`validateTurboNextConfig` in `next/dist/lib/turbopack-warning.js:137-138` and `:158-174`,
434
+ // 16.1.7).
435
+ //
436
+ // The `webpack` function above is withCanopy's own, and Turbopack never runs a `webpack`
437
+ // function, so its React aliases never applied under Turbopack. That is why the NOTE above sends
438
+ // `file:` symlink installs to `next dev --webpack`, and why answering the guard with an empty
439
+ // `turbopack` object is safe for this function. The rules:
440
+ // - Only when withCanopy added that function and the adopter wrote no `webpack` of their own.
441
+ // For theirs, Next's guard is the right one.
442
+ // - Never over a `turbopack` the adopter set, so a config that already has `turbopack: {}` keeps
443
+ // its own value.
444
+ // - Only on a detected Next 16 or later. Next 15 only warns here (`turbopack-warning.js:172-175`,
445
+ // 15.5.21), and Next 13 and 14 report an unknown top-level `turbopack` as an invalid option
446
+ // (their config schema is a strict object with no such key), so an unreadable version gets no
447
+ // key.
448
+ const nextMajor = installedNextMajor(process.cwd());
449
+ const answerTurbopackGuard = webpack !== undefined &&
450
+ !isSet(existingWebpack) &&
451
+ !isSet(nextConfig.turbopack) &&
452
+ nextMajor !== null &&
453
+ nextMajor >= 16;
254
454
  // Dual-build support: a static build gets STATIC_PAGE_EXTENSIONS (e.g. `page.static.tsx`)
255
455
  // instead of CMS_PAGE_EXTENSIONS, so CMS-only files (`route.server.ts`, `page.server.tsx`)
256
456
  // are excluded from static export while the static-only page variants are included.
@@ -285,8 +485,10 @@ export function withCanopy(nextConfig = {}, options = {}) {
285
485
  pageExtensions,
286
486
  webpack,
287
487
  rewrites: withAssetsRewrite(nextConfig.rewrites),
488
+ ...sharpTracingConfig(nextConfig, options),
288
489
  // Spread conditionally: emitting `generateBuildId: undefined` would be a key Next has to
289
490
  // reason about, where absence is unambiguous.
290
491
  ...(generateBuildId ? { generateBuildId } : {}),
492
+ ...(answerTurbopackGuard ? { turbopack: {} } : {}),
291
493
  };
292
494
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canopycms-next",
3
- "version": "0.0.67-int.89",
3
+ "version": "0.0.67-int.90",
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.89"
41
+ "canopycms": "0.0.67-int.90"
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.89"
51
+ "canopycms": "0.0.67-int.90"
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",