canopycms-next 0.0.66-int.81 → 0.0.66-int.83
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 +28 -1
- package/dist/config.mjs +28 -1
- package/dist/with-canopy.d.ts +5 -1
- package/dist/with-canopy.js +73 -0
- package/package.json +3 -3
package/dist/config.cjs
CHANGED
|
@@ -77,6 +77,29 @@ function resolveReactAliases(resolve) {
|
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
var SAFE_BUILD_ID = /^[A-Za-z0-9._-]{1,255}$/;
|
|
81
|
+
var BUILD_ID_RULE = 'must be 1-255 characters of [A-Za-z0-9._-] and not "." or ".."';
|
|
82
|
+
function isUsableBuildId(value) {
|
|
83
|
+
return SAFE_BUILD_ID.test(value) && value !== "." && value !== "..";
|
|
84
|
+
}
|
|
85
|
+
function resolveStaticBuildId() {
|
|
86
|
+
const raw = process.env.CANOPY_BUILD_ID;
|
|
87
|
+
const trimmed = raw?.trim();
|
|
88
|
+
if (raw === void 0) return null;
|
|
89
|
+
if (!trimmed) {
|
|
90
|
+
console.warn(
|
|
91
|
+
"CanopyCMS: CANOPY_BUILD_ID is set but blank \u2014 using Next's random build id instead. This export is NOT reproducible; two builds of one source tree will differ."
|
|
92
|
+
);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (!isUsableBuildId(trimmed)) {
|
|
96
|
+
console.warn(
|
|
97
|
+
`CanopyCMS: ignoring CANOPY_BUILD_ID="${trimmed}" \u2014 a build id becomes a single path segment under _next/static/, so it ${BUILD_ID_RULE}. Using Next's random build id instead; this export is NOT reproducible.`
|
|
98
|
+
);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
80
103
|
function withCanopy(nextConfig = {}, options = {}) {
|
|
81
104
|
const resolve = (0, import_node_module.createRequire)(import_node_path.default.join(process.cwd(), "noop.js")).resolve;
|
|
82
105
|
const installedOptional = OPTIONAL_PACKAGES.filter((pkg) => {
|
|
@@ -119,12 +142,16 @@ function withCanopy(nextConfig = {}, options = {}) {
|
|
|
119
142
|
...options.staticBuild ? STATIC_PAGE_EXTENSIONS : CMS_PAGE_EXTENSIONS
|
|
120
143
|
])
|
|
121
144
|
];
|
|
145
|
+
const generateBuildId = nextConfig.generateBuildId ?? (options.staticBuild ? resolveStaticBuildId : void 0);
|
|
122
146
|
return {
|
|
123
147
|
...nextConfig,
|
|
124
148
|
transpilePackages: allPackages,
|
|
125
149
|
pageExtensions,
|
|
126
150
|
webpack,
|
|
127
|
-
rewrites: withAssetsRewrite(nextConfig.rewrites)
|
|
151
|
+
rewrites: withAssetsRewrite(nextConfig.rewrites),
|
|
152
|
+
// Spread conditionally: emitting `generateBuildId: undefined` would be a key Next has to
|
|
153
|
+
// reason about, where absence is unambiguous.
|
|
154
|
+
...generateBuildId ? { generateBuildId } : {}
|
|
128
155
|
};
|
|
129
156
|
}
|
|
130
157
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/config.mjs
CHANGED
|
@@ -41,6 +41,29 @@ function resolveReactAliases(resolve) {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
var SAFE_BUILD_ID = /^[A-Za-z0-9._-]{1,255}$/;
|
|
45
|
+
var BUILD_ID_RULE = 'must be 1-255 characters of [A-Za-z0-9._-] and not "." or ".."';
|
|
46
|
+
function isUsableBuildId(value) {
|
|
47
|
+
return SAFE_BUILD_ID.test(value) && value !== "." && value !== "..";
|
|
48
|
+
}
|
|
49
|
+
function resolveStaticBuildId() {
|
|
50
|
+
const raw = process.env.CANOPY_BUILD_ID;
|
|
51
|
+
const trimmed = raw?.trim();
|
|
52
|
+
if (raw === void 0) return null;
|
|
53
|
+
if (!trimmed) {
|
|
54
|
+
console.warn(
|
|
55
|
+
"CanopyCMS: CANOPY_BUILD_ID is set but blank \u2014 using Next's random build id instead. This export is NOT reproducible; two builds of one source tree will differ."
|
|
56
|
+
);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (!isUsableBuildId(trimmed)) {
|
|
60
|
+
console.warn(
|
|
61
|
+
`CanopyCMS: ignoring CANOPY_BUILD_ID="${trimmed}" \u2014 a build id becomes a single path segment under _next/static/, so it ${BUILD_ID_RULE}. Using Next's random build id instead; this export is NOT reproducible.`
|
|
62
|
+
);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return trimmed;
|
|
66
|
+
}
|
|
44
67
|
function withCanopy(nextConfig = {}, options = {}) {
|
|
45
68
|
const resolve = createRequire(path.join(process.cwd(), "noop.js")).resolve;
|
|
46
69
|
const installedOptional = OPTIONAL_PACKAGES.filter((pkg) => {
|
|
@@ -83,12 +106,16 @@ function withCanopy(nextConfig = {}, options = {}) {
|
|
|
83
106
|
...options.staticBuild ? STATIC_PAGE_EXTENSIONS : CMS_PAGE_EXTENSIONS
|
|
84
107
|
])
|
|
85
108
|
];
|
|
109
|
+
const generateBuildId = nextConfig.generateBuildId ?? (options.staticBuild ? resolveStaticBuildId : void 0);
|
|
86
110
|
return {
|
|
87
111
|
...nextConfig,
|
|
88
112
|
transpilePackages: allPackages,
|
|
89
113
|
pageExtensions,
|
|
90
114
|
webpack,
|
|
91
|
-
rewrites: withAssetsRewrite(nextConfig.rewrites)
|
|
115
|
+
rewrites: withAssetsRewrite(nextConfig.rewrites),
|
|
116
|
+
// Spread conditionally: emitting `generateBuildId: undefined` would be a key Next has to
|
|
117
|
+
// reason about, where absence is unambiguous.
|
|
118
|
+
...generateBuildId ? { generateBuildId } : {}
|
|
92
119
|
};
|
|
93
120
|
}
|
|
94
121
|
export {
|
package/dist/with-canopy.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export interface WithCanopyOptions {
|
|
|
11
11
|
*
|
|
12
12
|
* When `true`: adds `static.ts` and `static.tsx` to `pageExtensions` instead, so
|
|
13
13
|
* Next.js processes the static-export-only variant of a dual-build content route
|
|
14
|
-
* (e.g. `page.static.tsx`) while ignoring `.server.*` CMS-only files.
|
|
14
|
+
* (e.g. `page.static.tsx`) while ignoring `.server.*` CMS-only files. It also lets
|
|
15
|
+
* `CANOPY_BUILD_ID` pin Next's build id, so a content-addressed artifact is reproducible.
|
|
15
16
|
*
|
|
16
17
|
* @example
|
|
17
18
|
* ```ts
|
|
@@ -35,6 +36,9 @@ export interface WithCanopyOptions {
|
|
|
35
36
|
* of the static-export-only page variants (e.g. `page.static.tsx`).
|
|
36
37
|
* - Resolves React to a single copy from your project root, preventing
|
|
37
38
|
* dual-instance crashes when using `file:` symlinks for local development
|
|
39
|
+
* - With `staticBuild: true`, honors `CANOPY_BUILD_ID` as Next's build id (Next's default is
|
|
40
|
+
* random, which puts two builds of one source tree in different `_next/static/` directories).
|
|
41
|
+
* Unset, or on a non-static build, Next's default is used unchanged.
|
|
38
42
|
*
|
|
39
43
|
* **When you need this:**
|
|
40
44
|
* - Always recommended — it replaces manual `transpilePackages` configuration
|
package/dist/with-canopy.js
CHANGED
|
@@ -110,6 +110,56 @@ function resolveReactAliases(resolve) {
|
|
|
110
110
|
return null;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* A build id Next can safely use as a single path segment.
|
|
115
|
+
*
|
|
116
|
+
* Next applies NO validation: `generateBuildId`'s return is trimmed and interpolated straight
|
|
117
|
+
* into `out/_next/static/<id>/`. So `CANOPY_BUILD_ID=$(git describe --all)` yields `heads/main`
|
|
118
|
+
* and silently nests that directory one level deeper than every emitted URL expects, and a value
|
|
119
|
+
* containing `..` climbs out of it. Both are the adopter's own pipeline misfiring rather than an
|
|
120
|
+
* injection vector, which is exactly why a clear message beats a broken deploy.
|
|
121
|
+
*/
|
|
122
|
+
const SAFE_BUILD_ID = /^[A-Za-z0-9._-]{1,255}$/;
|
|
123
|
+
/** The message every rejection uses, so the stated rule and the enforced rule cannot drift. */
|
|
124
|
+
const BUILD_ID_RULE = 'must be 1-255 characters of [A-Za-z0-9._-] and not "." or ".."';
|
|
125
|
+
function isUsableBuildId(value) {
|
|
126
|
+
// `.` and `..` clear the character class but are not names — as a path segment they resolve to
|
|
127
|
+
// the static directory itself or its parent. `a..b` is an ordinary filename and stays allowed.
|
|
128
|
+
// The 255 bound is the same rule: a longer segment fails at `mkdir` with ENAMETOOLONG.
|
|
129
|
+
return SAFE_BUILD_ID.test(value) && value !== '.' && value !== '..';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve Next's build id from `CANOPY_BUILD_ID`, or `null` to keep Next's random default.
|
|
133
|
+
*
|
|
134
|
+
* Both rejections warn rather than passing the value through or throwing. Unset means "I did not
|
|
135
|
+
* ask for a reproducible build" and says nothing; set-but-unusable almost always means a pipeline
|
|
136
|
+
* computed the id and the command failed (`CANOPY_BUILD_ID=$(git rev-parse ...)`), so the adopter
|
|
137
|
+
* believes they pinned it and would otherwise get a random — or structurally broken — artifact
|
|
138
|
+
* with nothing said. Falling back to Next's default keeps the build working; it is only the
|
|
139
|
+
* reproducibility that is lost, and the warning is what makes that recoverable.
|
|
140
|
+
*
|
|
141
|
+
* Declared above `withCanopy` deliberately: sitting between that function and its JSDoc block
|
|
142
|
+
* orphans the block onto this one, and the shipped `dist/config.d.ts` loses every line of
|
|
143
|
+
* `withCanopy`'s adopter-facing documentation.
|
|
144
|
+
*/
|
|
145
|
+
function resolveStaticBuildId() {
|
|
146
|
+
const raw = process.env.CANOPY_BUILD_ID;
|
|
147
|
+
const trimmed = raw?.trim();
|
|
148
|
+
if (raw === undefined)
|
|
149
|
+
return null;
|
|
150
|
+
if (!trimmed) {
|
|
151
|
+
console.warn("CanopyCMS: CANOPY_BUILD_ID is set but blank — using Next's random build id instead. " +
|
|
152
|
+
'This export is NOT reproducible; two builds of one source tree will differ.');
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (!isUsableBuildId(trimmed)) {
|
|
156
|
+
console.warn(`CanopyCMS: ignoring CANOPY_BUILD_ID="${trimmed}" — a build id becomes a single path ` +
|
|
157
|
+
`segment under _next/static/, so it ${BUILD_ID_RULE}. Using Next's random ` +
|
|
158
|
+
'build id instead; this export is NOT reproducible.');
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return trimmed;
|
|
162
|
+
}
|
|
113
163
|
/**
|
|
114
164
|
* Wrap your Next.js config to set up module transpilation and React
|
|
115
165
|
* resolution for CanopyCMS packages.
|
|
@@ -124,6 +174,9 @@ function resolveReactAliases(resolve) {
|
|
|
124
174
|
* of the static-export-only page variants (e.g. `page.static.tsx`).
|
|
125
175
|
* - Resolves React to a single copy from your project root, preventing
|
|
126
176
|
* dual-instance crashes when using `file:` symlinks for local development
|
|
177
|
+
* - With `staticBuild: true`, honors `CANOPY_BUILD_ID` as Next's build id (Next's default is
|
|
178
|
+
* random, which puts two builds of one source tree in different `_next/static/` directories).
|
|
179
|
+
* Unset, or on a non-static build, Next's default is used unchanged.
|
|
127
180
|
*
|
|
128
181
|
* **When you need this:**
|
|
129
182
|
* - Always recommended — it replaces manual `transpilePackages` configuration
|
|
@@ -209,11 +262,31 @@ export function withCanopy(nextConfig = {}, options = {}) {
|
|
|
209
262
|
...(options.staticBuild ? STATIC_PAGE_EXTENSIONS : CMS_PAGE_EXTENSIONS),
|
|
210
263
|
]),
|
|
211
264
|
];
|
|
265
|
+
// Static exports are routinely content-addressed (an S3/CloudFront artifact keyed on a tree
|
|
266
|
+
// hash), and Next defaults `generateBuildId` to `nanoid()` — so two builds of one source tree
|
|
267
|
+
// land under different `out/_next/static/<id>/` directories and the id names two different file
|
|
268
|
+
// sets. An explicit `generateBuildId` in the host config always wins.
|
|
269
|
+
//
|
|
270
|
+
// Deliberately gated on `staticBuild`. Under the dual-build convention the two flavors have
|
|
271
|
+
// different `pageExtensions` and therefore different chunk sets; pinning both from one env var
|
|
272
|
+
// would give two different file sets the SAME `_next/static/<id>/` path, which nothing can route
|
|
273
|
+
// between if they ever share an origin. The CMS build keeps nanoid so the ids stay distinct.
|
|
274
|
+
//
|
|
275
|
+
// `resolveStaticBuildId` (above) holds the rules for reading the variable and why each exists.
|
|
276
|
+
// One rule lives here instead, because it constrains this line rather than that function:
|
|
277
|
+
// returning the resolver's string directly matters. Next re-rolls ids containing `ad`
|
|
278
|
+
// (ad-blocker false positives) only on the `null` fallback path — a returned string is used
|
|
279
|
+
// verbatim (`next/dist/build/generate-build-id.js`) — which is what makes a hex tree hash usable
|
|
280
|
+
// as a build id at all. Do not "helpfully" route this through the fallback.
|
|
281
|
+
const generateBuildId = nextConfig.generateBuildId ?? (options.staticBuild ? resolveStaticBuildId : undefined);
|
|
212
282
|
return {
|
|
213
283
|
...nextConfig,
|
|
214
284
|
transpilePackages: allPackages,
|
|
215
285
|
pageExtensions,
|
|
216
286
|
webpack,
|
|
217
287
|
rewrites: withAssetsRewrite(nextConfig.rewrites),
|
|
288
|
+
// Spread conditionally: emitting `generateBuildId: undefined` would be a key Next has to
|
|
289
|
+
// reason about, where absence is unambiguous.
|
|
290
|
+
...(generateBuildId ? { generateBuildId } : {}),
|
|
218
291
|
};
|
|
219
292
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopycms-next",
|
|
3
|
-
"version": "0.0.66-int.
|
|
3
|
+
"version": "0.0.66-int.83",
|
|
4
4
|
"description": "Next.js adapter for CanopyCMS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"next": "^13.5.7 || ^14.2.25 || ^15.5.21 || >=16.0.0 <16.2.0 || >=16.3.0 <17.0.0",
|
|
38
38
|
"react": "^18.0.0 || ^19.0.0",
|
|
39
|
-
"canopycms": "0.0.66-int.
|
|
39
|
+
"canopycms": "0.0.66-int.83"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^22.9.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"react": "^18.3.1",
|
|
47
47
|
"typescript": "^5.6.3",
|
|
48
48
|
"vitest": "^4.1.0",
|
|
49
|
-
"canopycms": "0.0.66-int.
|
|
49
|
+
"canopycms": "0.0.66-int.83"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"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",
|