cdk-nextjs 0.7.1 → 0.7.2

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.
@@ -107,8 +107,10 @@ them — only the Containers types still build images.
107
107
  deployment: a function group's own `overrides.functionProps.architecture`
108
108
  that differs is a synth error. An x86 CI runner can deploy arm64 functions. Only
109
109
  `sharp` is staged for the target. Any other native dependency in your app is
110
- still traced from the build machine, so build on the architecture you deploy
111
- if you have one.
110
+ still traced from the build machine, and synth warns naming each package
111
+ whose addons are all built for another platform. See the
112
+ [Native Dependencies Guide](./native-dependencies-guide.md) to fix or
113
+ acknowledge it.
112
114
  - **The post-deploy Lambda is always arm64.** It has no native dependencies,
113
115
  so it no longer follows the synth machine. The first deploy after upgrading
114
116
  from an x86 machine updates it in place.
@@ -0,0 +1,223 @@
1
+ # Native Dependencies Guide
2
+
3
+ `next build` traces native addons (`.node` files) from the machine it runs on.
4
+ cdk-nextjs swaps `sharp` for the platform your app deploys to, but nothing else.
5
+ Any other native dependency built on a Mac, or on an x64 CI runner deploying
6
+ arm64, is staged for the wrong platform and fails with `MODULE_NOT_FOUND` or
7
+ `ERR_DLOPEN_FAILED` on the first request that loads it.
8
+
9
+ Synth warns about this, naming each staged package whose addons are all for a
10
+ platform other than the target:
11
+
12
+ ```
13
+ [WARNING] /MyStack/Nextjs/NextjsBuild: [cdk-nextjs] Deployment root "default"
14
+ has native binaries built for a platform other than linux-arm64, which it runs
15
+ on: node_modules/@valkey/valkey-glide-darwin-arm64. ...
16
+ [ack: cdk-nextjs:foreignNativeBinaries]
17
+ ```
18
+
19
+ The target is:
20
+
21
+ | Construct | Target |
22
+ | -------------------- | ------------------------------------------ |
23
+ | `*Functions` | `linux-<arch>` glibc (Amazon Linux 2023) |
24
+ | `*Containers` | `linux-<arch>` musl (`node:24-alpine`) |
25
+
26
+ `<arch>` is the Lambda's architecture for Functions (`arm64` or `x64`), and the
27
+ synth machine's for Containers.
28
+
29
+ The check reads only the OS and CPU of each addon, not its libc, so the wrong
30
+ libc variant passes synth. A `-musl` package on a Functions Lambda passes synth,
31
+ then fails at runtime with `ERR_DLOPEN_FAILED`. Use `-gnu` packages for
32
+ Functions and `-musl` packages for Containers.
33
+
34
+ There are three ways to fix it.
35
+
36
+ ## 1. Build on the target platform
37
+
38
+ Run synth on a Linux runner of the architecture you deploy. Nothing else to do.
39
+
40
+ ## 2. Stage the target's package after `next build` (any package manager)
41
+
42
+ Most native libraries ship each platform as its own package
43
+ (`@valkey/valkey-glide-linux-arm64-gnu`, `@node-rs/argon2-linux-arm64-gnu`, …),
44
+ pinned in the main package's `optionalDependencies`. After `next build`, remove
45
+ the build machine's variant from the staged output and extract the target's in
46
+ its place, the way cdk-nextjs does for `sharp`. Your local `node_modules` is
47
+ untouched, so local dev keeps working.
48
+
49
+ Run the script from `buildCommand`, which runs in your Next.js project directory
50
+ before cdk-nextjs zips the staged output:
51
+
52
+ ```ts
53
+ new NextjsGlobalFunctions(this, "Nextjs", {
54
+ buildDirectory: "./app",
55
+ buildCommand: "npm run build && node scripts/stage-native.mjs arm64",
56
+ overrides: {
57
+ nextjsFunctions: { functionProps: { architecture: Architecture.ARM_64 } },
58
+ },
59
+ });
60
+ ```
61
+
62
+ With `skipBuild: true`, `buildCommand` doesn't run, so run the script yourself
63
+ after your own build.
64
+
65
+ The warning checks the script's result: `NextjsBuild` runs the check after
66
+ `buildCommand` and after it stages `sharp`. If the script stages nothing, for
67
+ example because a cdk-nextjs release changed the staged layout, the foreign
68
+ addon stays and synth warns. Run `cdk synth --strict` in CI so that a staging
69
+ miss stops the deploy instead of shipping.
70
+
71
+ `npm pack` fetches the tarball from the registry without your package manager's
72
+ checks: it doesn't verify the lockfile's integrity hash, and it ignores rules
73
+ such as pnpm's `minimumReleaseAge`.
74
+
75
+ `scripts/stage-native.mjs`, for `@valkey/valkey-glide` on Lambda. Edit the
76
+ constants for your package; for Containers, use the `-musl` variant.
77
+
78
+ ```js
79
+ // Replaces a native package's platform variant in the cdk-nextjs staged build
80
+ // with the one for the deploy target.
81
+ // Usage: node scripts/stage-native.mjs <arm64|x64>
82
+ //
83
+ // Known limits:
84
+ // - It downloads the tarball once for each root that uses the package.
85
+ // - If a step fails, the temporary directory stays behind.
86
+ // - If a root has more than one version of the main package, it stages the
87
+ // version from the last manifest it reads.
88
+ import { execFileSync } from "node:child_process";
89
+ import {
90
+ cpSync,
91
+ existsSync,
92
+ mkdtempSync,
93
+ readdirSync,
94
+ readFileSync,
95
+ rmSync,
96
+ unlinkSync,
97
+ } from "node:fs";
98
+ import { tmpdir } from "node:os";
99
+ import { join } from "node:path";
100
+
101
+ const arch = process.argv[2];
102
+ if (arch !== "arm64" && arch !== "x64") {
103
+ throw new Error(`stage-native: expected "arm64" or "x64", got "${arch}"`);
104
+ }
105
+
106
+ // Edit these for your package.
107
+ const scope = "@valkey";
108
+ const name = "valkey-glide";
109
+ // Lambda's managed runtime uses glibc; Containers use `-musl`.
110
+ const target = `${scope}/${name}-linux-${arch}-gnu`;
111
+
112
+ // Platform packages are `<scope>/<name>-<os>-<arch>[-<libc>]` links and
113
+ // directories, plus their pnpm store directories (`<scope>+<name>-…`). Match
114
+ // the platform, not only the prefix: Turbopack links the main package as
115
+ // `<name>-<hash>`.
116
+ const platform = new RegExp(`^(?:${scope}\\+)?${name}-(?:darwin|linux|win32)-`);
117
+ const isPlatformPackage = (parent, entry) =>
118
+ platform.test(entry) &&
119
+ (parent.endsWith(scope) || entry.startsWith(`${scope}+`));
120
+
121
+ // One deployment root without `functionGroups`, one per group with them.
122
+ // Relative to this script, which lives in `<project>/scripts/`.
123
+ const adapter = join(import.meta.dirname, "..", ".next", "cdk-nextjs-adapter");
124
+ const groups = join(adapter, "groups");
125
+ const roots = [
126
+ join(adapter, "app"),
127
+ ...(existsSync(groups)
128
+ ? readdirSync(groups).map((group) => join(groups, group))
129
+ : []),
130
+ ].filter((root) => existsSync(root));
131
+
132
+ for (const root of roots) {
133
+ let version;
134
+ const pending = [root];
135
+ for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
136
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
137
+ const path = join(dir, entry.name);
138
+ if (isPlatformPackage(dir, entry.name)) {
139
+ if (entry.isSymbolicLink()) {
140
+ // Not rmSync: the walk can remove a pnpm store directory before it
141
+ // reaches a link into it, and rmSync(path, { force: true }) leaves
142
+ // that dangling link in place without an error.
143
+ unlinkSync(path);
144
+ } else {
145
+ rmSync(path, { recursive: true, force: true });
146
+ }
147
+ } else if (entry.isDirectory()) {
148
+ if (entry.name === name && dir.endsWith(scope)) {
149
+ const manifest = JSON.parse(
150
+ readFileSync(join(path, "package.json"), "utf8"),
151
+ );
152
+ version = manifest.optionalDependencies?.[target];
153
+ }
154
+ pending.push(path);
155
+ }
156
+ }
157
+ }
158
+ if (!version) {
159
+ continue; // This root doesn't use the package.
160
+ }
161
+
162
+ // The root's node_modules is on the resolution path of the staged package
163
+ // and of the app's server chunks.
164
+ const tmp = mkdtempSync(join(tmpdir(), "stage-native-"));
165
+ const tarball = execFileSync("npm", ["pack", `${target}@${version}`, "--silent"], {
166
+ cwd: tmp,
167
+ encoding: "utf8",
168
+ }).trim();
169
+ execFileSync("tar", ["-xzf", tarball], { cwd: tmp });
170
+ // A copy, not a rename: the temporary directory can be on another file system.
171
+ cpSync(join(tmp, "package"), join(root, "node_modules", target), {
172
+ recursive: true,
173
+ });
174
+ rmSync(tmp, { recursive: true, force: true });
175
+ console.log(`stage-native: staged ${target}@${version} in ${root}`);
176
+ }
177
+ ```
178
+
179
+ The script depends on cdk-nextjs's staged layout (`.next/cdk-nextjs-adapter/app`
180
+ and `.next/cdk-nextjs-adapter/groups/<name>`), which can change between minor
181
+ versions. Libraries that don't ship per-platform packages (they build from
182
+ source at install, or bundle every platform's prebuild in one package) need
183
+ approach 1.
184
+
185
+ ## 3. Install the target's variant with pnpm
186
+
187
+ pnpm can install other platforms' optional dependencies next to your own. Keep
188
+ `current` in the list so local dev still gets the host's variant:
189
+
190
+ ```yaml
191
+ # pnpm-workspace.yaml
192
+ supportedArchitectures:
193
+ os: [current, linux]
194
+ cpu: [current, arm64]
195
+ libc: [current, glibc]
196
+ ```
197
+
198
+ This applies to every package in the workspace, and installs every combination
199
+ of the listed values. Tracing still only follows the variant that loads on the
200
+ build machine, so add the target's with Next.js's `outputFileTracingIncludes`
201
+ and drop the host's with `outputFileTracingExcludes`. Both are needed: the
202
+ host's package stays traced beside the one you add, and still has only a
203
+ foreign addon. Under pnpm both live in `node_modules/.pnpm`, so the globs
204
+ match the store directories, not `node_modules/<scope>/<name>`. Approach 2 is
205
+ usually simpler.
206
+
207
+ npm's `--os`/`--cpu` install flags replace the host's platform instead of adding
208
+ to it, which breaks local dev, so use approach 2 with npm.
209
+
210
+ ## Optional native dependencies
211
+
212
+ Some packages load an addon inside `try`/`catch` and fall back to JavaScript
213
+ without it: `ws` with `bufferutil`/`utf-8-validate`, and `fsevents`. Their
214
+ foreign addon never loads, so the app works as deployed. Acknowledge the
215
+ warning:
216
+
217
+ ```ts
218
+ import { Annotations } from "aws-cdk-lib";
219
+
220
+ Annotations.of(nextjs).acknowledgeWarning("cdk-nextjs:foreignNativeBinaries");
221
+ ```
222
+
223
+ To fail synth on it instead, run `cdk synth --strict`.
package/lib/nextjs-api.js CHANGED
@@ -22,7 +22,7 @@ const API_PATH_PART = /^[a-zA-Z0-9:._\-$]+$/;
22
22
  * Creates an API Gateway REST API for Next.js applications
23
23
  */
24
24
  class NextjsApi extends constructs_1.Construct {
25
- static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-nextjs.NextjsApi", version: "0.7.1" };
25
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-nextjs.NextjsApi", version: "0.7.2" };
26
26
  /**
27
27
  * The API Gateway REST API
28
28
  */
@@ -209,6 +209,16 @@ export declare class NextjsBuild extends Construct {
209
209
  * function that cannot serve it) is this, several steps downstream.
210
210
  */
211
211
  private resolveDeploymentRoots;
212
+ /**
213
+ * `next build` traces native addons from the machine it runs on, and only
214
+ * `sharp` is swapped for the target. Anything else built for another platform
215
+ * fails with MODULE_NOT_FOUND or ERR_DLOPEN_FAILED on the first request that
216
+ * loads it, so say which package at synth. A warning, not an error: some
217
+ * addons are optional (`ws`'s `bufferutil`, `fsevents`) and fall back to JS.
218
+ *
219
+ * Only the OS and CPU are checked: glibc and musl builds share a header.
220
+ */
221
+ private warnOnForeignNativeBinaries;
212
222
  /**
213
223
  * Lambda enforces 250 MB unzipped at CloudFormation time, which is minutes
214
224
  * into a deploy and reports only a size. Splitting exists to stay under that
@@ -414,6 +424,21 @@ export declare function sharpBinaryDir(root: string): string;
414
424
  * monorepo.
415
425
  */
416
426
  export declare function listTree(root: string): Dirent[];
427
+ /**
428
+ * The platform a native addon was compiled for, read from its header:
429
+ * `linux-<arch>`, `darwin` or `win32`, or `undefined` for a file that is none
430
+ * of those.
431
+ */
432
+ export declare function nativeBinaryPlatform(file: string): string | undefined;
433
+ /**
434
+ * The packages under `root` that have `.node` addons but none built for
435
+ * `target`, so they'd fail to load there. Returned as `root`-relative paths.
436
+ *
437
+ * A package with addons for several platforms is fine as long as one of them
438
+ * is `target`: packages like `bufferutil` bundle a `darwin` and a
439
+ * `linux-x64` addon and load whichever matches the machine.
440
+ */
441
+ export declare function findForeignNativePackages(root: string, target: string): string[];
417
442
  /**
418
443
  * Write {@link PUBLIC_FILES_FILE_NAME} into a deployment root's runtime
419
444
  * directory: every file under `publicDir`, listed the way the runtime would