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.
- package/.jsii +38 -38
- package/README.md +1 -0
- package/docs/breaking-changes.md +4 -2
- package/docs/native-dependencies-guide.md +223 -0
- package/lib/nextjs-api.js +1 -1
- package/lib/nextjs-build/nextjs-build.d.ts +25 -0
- package/lib/nextjs-build/nextjs-build.js +103 -2
- package/lib/nextjs-build/nextjs-build.test.js +50 -1
- package/lib/nextjs-cache.js +1 -1
- package/lib/nextjs-compute/nextjs-containers.js +1 -1
- package/lib/nextjs-compute/nextjs-functions.js +1 -1
- package/lib/nextjs-distribution.js +1 -1
- package/lib/nextjs-post-deploy.js +1 -1
- package/lib/nextjs-static-assets.js +1 -1
- package/lib/root-constructs/nextjs-base-construct.js +1 -1
- package/lib/root-constructs/nextjs-global-containers.js +1 -1
- package/lib/root-constructs/nextjs-global-functions.js +1 -1
- package/lib/root-constructs/nextjs-regional-containers.js +1 -1
- package/lib/root-constructs/nextjs-regional-functions.js +1 -1
- package/package.json +1 -1
package/docs/breaking-changes.md
CHANGED
|
@@ -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,
|
|
111
|
-
|
|
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.
|
|
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
|