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 +262 -4
- package/dist/config.mjs +262 -4
- package/dist/context-wrapper.js +5 -4
- package/dist/sharp-tracing.d.ts +57 -0
- package/dist/sharp-tracing.js +347 -0
- package/dist/with-canopy.d.ts +14 -0
- package/dist/with-canopy.js +202 -0
- package/package.json +3 -3
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:
|
|
74
|
-
"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)(
|
|
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:
|
|
38
|
-
"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(
|
|
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 {
|
package/dist/context-wrapper.js
CHANGED
|
@@ -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/
|
|
187
|
-
// provision the base/active branch themselves via
|
|
188
|
-
// (see context.ts's resolveSchemaContext
|
|
189
|
-
//
|
|
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
|
+
}
|
package/dist/with-canopy.d.ts
CHANGED
|
@@ -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
|
package/dist/with-canopy.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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",
|