cdk-nextjs 0.6.1 → 0.7.0

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.
Files changed (266) hide show
  1. package/.jsii +2467 -2714
  2. package/.jsii.tabl.json +1 -1
  3. package/API.md +1725 -1909
  4. package/CLAUDE.md +2 -1
  5. package/README.md +429 -130
  6. package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +152 -170
  7. package/assets/lambdas/redeploy-stage/redeploy-stage.lambda/index.js +875 -0
  8. package/docs/adapter-runtime.md +185 -0
  9. package/docs/breaking-changes.md +687 -4
  10. package/docs/caching-guide.md +156 -112
  11. package/docs/cdk-nextjs-cfn-exec-policy.json +2 -1
  12. package/docs/development-guide.md +1 -1
  13. package/docs/harness-coverage.md +598 -0
  14. package/docs/init-cache-deployment.md +32 -1
  15. package/docs/next-build-output-guide.md +30 -20
  16. package/examples/README.md +5 -1
  17. package/lib/adapter/__fixtures__/app-playground-base-path.json +4926 -0
  18. package/lib/adapter/__fixtures__/app-playground.json +4917 -0
  19. package/lib/adapter/__fixtures__/pages-i18n.json +1728 -0
  20. package/lib/adapter/adapter.mjs +1548 -185
  21. package/lib/adapter/adapter.test.js +106 -0
  22. package/lib/adapter/aws-cache-store.d.ts +489 -0
  23. package/lib/adapter/aws-cache-store.js +1004 -0
  24. package/lib/adapter/aws-cache-store.test.d.ts +1 -0
  25. package/lib/adapter/aws-cache-store.test.js +361 -0
  26. package/lib/adapter/build-outputs.d.ts +80 -0
  27. package/lib/adapter/build-outputs.js +852 -0
  28. package/lib/adapter/build-outputs.test.d.ts +1 -0
  29. package/lib/adapter/build-outputs.test.js +861 -0
  30. package/lib/adapter/cache-handler.d.ts +9 -2
  31. package/lib/adapter/cache-handler.js +72 -96
  32. package/lib/adapter/cache-handler.mjs +18956 -17774
  33. package/lib/adapter/cache-handler.test.js +263 -31
  34. package/lib/adapter/cache-kinds.d.ts +32 -0
  35. package/lib/adapter/cache-kinds.js +35 -0
  36. package/lib/adapter/cache-kinds.test.d.ts +1 -0
  37. package/lib/adapter/cache-kinds.test.js +69 -0
  38. package/lib/adapter/cache-utils.d.ts +90 -0
  39. package/lib/adapter/cache-utils.js +199 -4
  40. package/lib/adapter/cache-utils.test.d.ts +1 -0
  41. package/lib/adapter/cache-utils.test.js +179 -0
  42. package/lib/adapter/function-groups.d.ts +215 -0
  43. package/lib/adapter/function-groups.js +1099 -0
  44. package/lib/adapter/function-groups.test.d.ts +1 -0
  45. package/lib/adapter/function-groups.test.js +743 -0
  46. package/lib/adapter/init-cache.d.ts +6 -0
  47. package/lib/adapter/init-cache.js +245 -0
  48. package/lib/adapter/init-cache.test.d.ts +1 -0
  49. package/lib/adapter/init-cache.test.js +137 -0
  50. package/lib/adapter/local-file-cache-handler.d.ts +23 -0
  51. package/lib/adapter/local-file-cache-handler.js +52 -1
  52. package/lib/adapter/memory-cache-handler.d.ts +26 -17
  53. package/lib/adapter/memory-cache-handler.js +32 -50
  54. package/lib/adapter/memory-cache-handler.test.js +105 -85
  55. package/lib/adapter/s3-cache-handler.d.ts +179 -40
  56. package/lib/adapter/s3-cache-handler.js +807 -277
  57. package/lib/adapter/s3-cache-handler.test.js +1186 -31
  58. package/lib/adapter/use-cache-common.d.ts +90 -0
  59. package/lib/adapter/use-cache-common.js +250 -0
  60. package/lib/adapter/use-cache-common.test.d.ts +1 -0
  61. package/lib/adapter/use-cache-common.test.js +646 -0
  62. package/lib/adapter/use-cache-default-handler.d.ts +24 -0
  63. package/lib/adapter/use-cache-default-handler.js +90 -0
  64. package/lib/adapter/use-cache-default-handler.mjs +38645 -0
  65. package/lib/adapter/use-cache-handlers.test.d.ts +1 -0
  66. package/lib/adapter/use-cache-handlers.test.js +349 -0
  67. package/lib/adapter/use-cache-remote-handler.d.ts +26 -0
  68. package/lib/adapter/use-cache-remote-handler.js +186 -0
  69. package/lib/{image-optimization/handler.mjs → adapter/use-cache-remote-handler.mjs} +33572 -38798
  70. package/lib/generated-structs/OptionalNextjsApiProps.d.ts +59 -0
  71. package/lib/generated-structs/OptionalNextjsApiProps.js +3 -0
  72. package/lib/generated-structs/OptionalNextjsCacheProps.d.ts +15 -6
  73. package/lib/generated-structs/OptionalNextjsCacheProps.js +1 -1
  74. package/lib/generated-structs/OptionalNextjsContainersProps.d.ts +25 -11
  75. package/lib/generated-structs/OptionalNextjsContainersProps.js +1 -1
  76. package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +31 -9
  77. package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
  78. package/lib/generated-structs/OptionalNextjsPostDeployProps.d.ts +13 -0
  79. package/lib/generated-structs/OptionalNextjsPostDeployProps.js +1 -1
  80. package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.d.ts +11 -2
  81. package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.js +1 -1
  82. package/lib/index.d.ts +6 -7
  83. package/lib/index.js +2 -4
  84. package/lib/lambdas/post-deploy/post-deploy.lambda.js +3 -2
  85. package/lib/lambdas/post-deploy/prune-cache-bucket.d.ts +7 -3
  86. package/lib/lambdas/post-deploy/prune-cache-bucket.js +57 -71
  87. package/lib/lambdas/post-deploy/prune-cache-bucket.test.d.ts +1 -0
  88. package/lib/lambdas/post-deploy/prune-cache-bucket.test.js +114 -0
  89. package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +1 -0
  90. package/lib/lambdas/post-deploy/prune-revalidation-table.js +23 -31
  91. package/lib/lambdas/post-deploy/prune-revalidation-table.test.d.ts +1 -0
  92. package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +71 -0
  93. package/lib/lambdas/post-deploy/prune-s3.d.ts +19 -3
  94. package/lib/lambdas/post-deploy/prune-s3.js +54 -15
  95. package/lib/lambdas/post-deploy/prune-s3.test.d.ts +1 -0
  96. package/lib/lambdas/post-deploy/prune-s3.test.js +255 -0
  97. package/lib/lambdas/redeploy-stage/redeploy-stage-function.d.ts +13 -0
  98. package/lib/lambdas/redeploy-stage/redeploy-stage-function.js +55 -0
  99. package/lib/lambdas/redeploy-stage/redeploy-stage.d.ts +24 -0
  100. package/lib/lambdas/redeploy-stage/redeploy-stage.js +68 -0
  101. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.d.ts +20 -0
  102. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.js +24 -0
  103. package/lib/lambdas/redeploy-stage/redeploy-stage.test.d.ts +1 -0
  104. package/lib/lambdas/redeploy-stage/redeploy-stage.test.js +93 -0
  105. package/lib/nextjs-api.d.ts +107 -18
  106. package/lib/nextjs-api.js +349 -39
  107. package/lib/nextjs-api.test.js +582 -1
  108. package/lib/nextjs-build/global-containers.Dockerfile +15 -10
  109. package/lib/nextjs-build/nextjs-build.d.ts +339 -63
  110. package/lib/nextjs-build/nextjs-build.js +684 -327
  111. package/lib/nextjs-build/nextjs-build.test.d.ts +1 -0
  112. package/lib/nextjs-build/nextjs-build.test.js +412 -0
  113. package/lib/nextjs-build/patch-fetch.js +1 -1
  114. package/lib/nextjs-build/patch-fetch.test.js +166 -11
  115. package/lib/nextjs-build/regional-containers.Dockerfile +17 -10
  116. package/lib/nextjs-cache.d.ts +82 -4
  117. package/lib/nextjs-cache.js +186 -10
  118. package/lib/nextjs-cache.test.js +176 -1
  119. package/lib/nextjs-compute/nextjs-compute-base-props.d.ts +21 -8
  120. package/lib/nextjs-compute/nextjs-compute-base-props.js +41 -1
  121. package/lib/nextjs-compute/nextjs-compute-base-props.test.d.ts +1 -0
  122. package/lib/nextjs-compute/nextjs-compute-base-props.test.js +44 -0
  123. package/lib/nextjs-compute/nextjs-containers.d.ts +19 -0
  124. package/lib/nextjs-compute/nextjs-containers.js +57 -20
  125. package/lib/nextjs-compute/nextjs-functions.d.ts +122 -7
  126. package/lib/nextjs-compute/nextjs-functions.js +113 -67
  127. package/lib/nextjs-compute/nextjs-functions.test.d.ts +1 -0
  128. package/lib/nextjs-compute/nextjs-functions.test.js +99 -0
  129. package/lib/nextjs-distribution.d.ts +194 -23
  130. package/lib/nextjs-distribution.js +535 -62
  131. package/lib/nextjs-distribution.test.d.ts +1 -0
  132. package/lib/nextjs-distribution.test.js +697 -0
  133. package/lib/nextjs-post-deploy.d.ts +23 -3
  134. package/lib/nextjs-post-deploy.js +14 -10
  135. package/lib/nextjs-post-deploy.test.d.ts +1 -0
  136. package/lib/nextjs-post-deploy.test.js +34 -0
  137. package/lib/nextjs-static-assets.d.ts +23 -4
  138. package/lib/nextjs-static-assets.js +29 -6
  139. package/lib/nextjs-static-assets.test.js +20 -1
  140. package/lib/root-constructs/nextjs-base-construct.d.ts +105 -45
  141. package/lib/root-constructs/nextjs-base-construct.js +144 -34
  142. package/lib/root-constructs/nextjs-global-containers.d.ts +18 -20
  143. package/lib/root-constructs/nextjs-global-containers.js +20 -70
  144. package/lib/root-constructs/nextjs-global-functions.d.ts +26 -28
  145. package/lib/root-constructs/nextjs-global-functions.js +52 -69
  146. package/lib/root-constructs/nextjs-regional-containers.d.ts +25 -5
  147. package/lib/root-constructs/nextjs-regional-containers.js +17 -15
  148. package/lib/root-constructs/nextjs-regional-functions.d.ts +22 -18
  149. package/lib/root-constructs/nextjs-regional-functions.js +8 -25
  150. package/lib/runtime/api-gateway-path.d.ts +34 -0
  151. package/lib/runtime/api-gateway-path.js +71 -0
  152. package/lib/runtime/api-gateway-path.test.d.ts +1 -0
  153. package/lib/runtime/api-gateway-path.test.js +65 -0
  154. package/lib/runtime/case-redirect.d.ts +22 -0
  155. package/lib/runtime/case-redirect.js +63 -0
  156. package/lib/runtime/case-redirect.test.d.ts +1 -0
  157. package/lib/runtime/case-redirect.test.js +32 -0
  158. package/lib/runtime/core.d.ts +234 -0
  159. package/lib/runtime/core.js +1213 -0
  160. package/lib/runtime/core.test.d.ts +1 -0
  161. package/lib/runtime/core.test.js +1678 -0
  162. package/lib/runtime/deployment-root.d.ts +13 -0
  163. package/lib/runtime/deployment-root.js +29 -0
  164. package/lib/runtime/dispatch.d.ts +331 -0
  165. package/lib/runtime/dispatch.js +751 -0
  166. package/lib/runtime/dispatch.test.d.ts +1 -0
  167. package/lib/runtime/dispatch.test.js +853 -0
  168. package/lib/runtime/entrypoints.d.ts +57 -0
  169. package/lib/runtime/entrypoints.js +115 -0
  170. package/lib/runtime/http/abort-signal.d.ts +6 -0
  171. package/lib/runtime/http/abort-signal.js +19 -0
  172. package/lib/runtime/http/abort-signal.test.d.ts +1 -0
  173. package/lib/runtime/http/abort-signal.test.js +49 -0
  174. package/lib/runtime/http/node-server.d.ts +38 -0
  175. package/lib/runtime/http/node-server.js +99 -0
  176. package/lib/runtime/http/node-server.test.d.ts +1 -0
  177. package/lib/runtime/http/node-server.test.js +96 -0
  178. package/lib/runtime/http/node-sink.d.ts +16 -0
  179. package/lib/runtime/http/node-sink.js +48 -0
  180. package/lib/runtime/http/node-sink.test.d.ts +1 -0
  181. package/lib/runtime/http/node-sink.test.js +66 -0
  182. package/lib/runtime/http/request.d.ts +62 -0
  183. package/lib/runtime/http/request.js +134 -0
  184. package/lib/runtime/http/request.test.d.ts +1 -0
  185. package/lib/runtime/http/request.test.js +103 -0
  186. package/lib/runtime/http/response.d.ts +77 -0
  187. package/lib/runtime/http/response.js +229 -0
  188. package/lib/runtime/http/response.test.d.ts +1 -0
  189. package/lib/runtime/http/response.test.js +178 -0
  190. package/lib/runtime/http/sink.d.ts +73 -0
  191. package/lib/runtime/http/sink.js +188 -0
  192. package/lib/runtime/http/sink.test.d.ts +1 -0
  193. package/lib/runtime/http/sink.test.js +257 -0
  194. package/lib/runtime/image-utils.d.ts +78 -0
  195. package/lib/runtime/image-utils.js +129 -0
  196. package/lib/runtime/image-utils.test.d.ts +1 -0
  197. package/lib/runtime/image-utils.test.js +237 -0
  198. package/lib/runtime/image.d.ts +76 -0
  199. package/lib/runtime/image.js +296 -0
  200. package/lib/runtime/image.test.d.ts +1 -0
  201. package/lib/runtime/image.test.js +405 -0
  202. package/lib/runtime/lambda-event.d.ts +4 -0
  203. package/lib/runtime/lambda-event.js +113 -0
  204. package/lib/runtime/lambda-event.test.d.ts +1 -0
  205. package/lib/runtime/lambda-event.test.js +119 -0
  206. package/lib/runtime/lambda.mjs +41595 -0
  207. package/lib/runtime/load-module.d.ts +8 -0
  208. package/lib/runtime/load-module.js +35 -0
  209. package/lib/runtime/manifest.d.ts +215 -0
  210. package/lib/runtime/manifest.js +118 -0
  211. package/lib/runtime/middleware.d.ts +43 -0
  212. package/lib/runtime/middleware.js +113 -0
  213. package/lib/runtime/middleware.test.d.ts +1 -0
  214. package/lib/runtime/middleware.test.js +259 -0
  215. package/lib/runtime/next-modules.d.ts +55 -0
  216. package/lib/runtime/next-modules.js +124 -0
  217. package/lib/runtime/next-modules.test.d.ts +1 -0
  218. package/lib/runtime/next-modules.test.js +34 -0
  219. package/lib/runtime/public-files.d.ts +32 -0
  220. package/lib/runtime/public-files.js +117 -0
  221. package/lib/runtime/public-files.test.d.ts +1 -0
  222. package/lib/runtime/public-files.test.js +97 -0
  223. package/lib/runtime/server.mjs +41588 -0
  224. package/lib/runtime/static-files.d.ts +50 -0
  225. package/lib/runtime/static-files.js +266 -0
  226. package/lib/runtime/static-files.test.d.ts +1 -0
  227. package/lib/runtime/static-files.test.js +118 -0
  228. package/lib/runtime/util.d.ts +28 -0
  229. package/lib/runtime/util.js +79 -0
  230. package/lib/utils/base-path.d.ts +123 -11
  231. package/lib/utils/base-path.js +276 -39
  232. package/lib/utils/base-path.test.js +275 -50
  233. package/lib/utils/get-architecture.d.ts +7 -8
  234. package/lib/utils/get-architecture.js +12 -13
  235. package/lib/utils/zip-directory.d.ts +14 -0
  236. package/lib/utils/zip-directory.js +121 -0
  237. package/lib/utils/zip-directory.test.d.ts +1 -0
  238. package/lib/utils/zip-directory.test.js +99 -0
  239. package/package.json +22 -3
  240. package/scripts/capture-adapter-fixture.mjs +325 -0
  241. package/scripts/e2e-deploy.sh +398 -0
  242. package/scripts/e2e-harness/README.md +707 -0
  243. package/scripts/e2e-harness/app.js +370 -0
  244. package/scripts/e2e-harness/common.sh +249 -0
  245. package/scripts/e2e-harness/screen.mjs +302 -0
  246. package/scripts/e2e-harness/stage-proxy.mjs +145 -0
  247. package/scripts/e2e-harness/type-filters.mjs +47 -0
  248. package/scripts/e2e-logs.sh +75 -0
  249. package/scripts/e2e-offline.sh +91 -0
  250. package/scripts/e2e-sweep.sh +370 -0
  251. package/scripts/e2e-warm.sh +114 -0
  252. package/scripts/zero-config-build.mjs +418 -0
  253. package/docs/1k-concurrent-users-executions.png +0 -0
  254. package/docs/plans/adapter-runtime-release.md +0 -362
  255. package/docs/plans/phase-0-image-function-opt-in.md +0 -295
  256. package/lib/generated-structs/OptionalDockerImageFunctionProps.d.ts +0 -425
  257. package/lib/generated-structs/OptionalDockerImageFunctionProps.js +0 -3
  258. package/lib/image-optimization/handler-utils.d.ts +0 -34
  259. package/lib/image-optimization/handler-utils.js +0 -83
  260. package/lib/image-optimization/handler-utils.test.js +0 -178
  261. package/lib/nextjs-build/functions.Dockerfile +0 -31
  262. package/lib/nextjs-compute/nextjs-image-function.d.ts +0 -53
  263. package/lib/nextjs-compute/nextjs-image-function.js +0 -59
  264. package/lib/utils/experimental-flags.d.ts +0 -24
  265. package/lib/utils/experimental-flags.js +0 -30
  266. /package/lib/{image-optimization/handler-utils.test.d.ts → adapter/adapter.test.d.ts} +0 -0
@@ -1,362 +0,0 @@
1
- # Adapter runtime — one branch, one release, all four `NextjsType`s
2
-
3
- Self-contained; assumes no context from the session that wrote it.
4
-
5
- Verified against Next.js **16.3.5** (the version in `package.json`) and the
6
- adapters docs under `https://nextjs.org/docs/app/api-reference/adapters/`.
7
-
8
- ## Decision
9
-
10
- Move **all four existing `NextjsType`s** onto adapter entrypoint invocation:
11
- `@next/routing` resolves routes, a middleware runner handles `outputs.middleware`,
12
- the existing image handler serves `_next/image`, and the Next.js entrypoints
13
- render. No new `NextjsType`s. Phases 1-3 ship as one user-facing release.
14
-
15
- **Phase 4 (per-output Lambda splitting) is out of this release.**
16
-
17
- ## Execution model: one branch, no runtime-selection flag
18
-
19
- Everything in this plan — Spike 0 through Phase 3, plus the testing work —
20
- happens on a single feature branch and merges to `main` as one PR (or a small
21
- number of PRs merged in immediate succession, with no release cut in between).
22
- `main` runs `server.js` unchanged for the entire duration of development; the
23
- branch is where the new runtime is built, iterated on, and proven, and nothing
24
- crosses over until it's ready to fully replace the old path.
25
-
26
- This is a deliberate departure from `docs/plans/phase-0-image-function-opt-in.md`,
27
- which shipped incrementally to `main` behind
28
- `CDK_NEXTJS_EXPERIMENTAL_DEDICATED_IMAGE_FUNCTION=1` across many small merged
29
- PRs. That pattern is not used here:
30
-
31
- - **No env var gate.** There is no `CDK_NEXTJS_EXPERIMENTAL_ADAPTER_RUNTIME`
32
- or equivalent, in any form — undocumented or otherwise. The old and new
33
- runtimes are never both reachable from the same `main` commit. Selection
34
- between them is "which branch you're on," not a flag evaluated at synth or
35
- request time.
36
- - **No incremental merges to `main`.** Phase boundaries are checkpoints on the
37
- branch (commits, or PRs against the branch if that helps reviewability), not
38
- points where anything ships to users. Semantic-release does not see this work
39
- until the branch merges.
40
- - **CI runs on the branch,** the same way it would on any long-lived feature
41
- branch — via normal branch/PR CI against the branch, not via a matrix that
42
- toggles a flag on `main`.
43
-
44
- Consequence: there is no cheap "known-good `server.js` path" oracle available
45
- on the same commit to byte-diff against mid-development, since the two paths
46
- never coexist. That's fine — the official adapter compatibility harness (see
47
- Testing, below) is the primary correctness gate regardless, and is a stronger
48
- signal than a homegrown byte-diff would have been. If a byte-diff comparison
49
- is useful while iterating, do it by checking out `main` in a worktree and
50
- diffing output against the branch, not by carrying a flag.
51
-
52
- The other reason an earlier draft hedged with new `*_ADAPTER` types or a flag
53
- is also gone: the dispatcher was going to be a large hand-written component,
54
- which made sharing it between Lambda and Fargate look expensive. `@next/routing`
55
- supplies it (below), so one request path across all four types is cheap enough
56
- to build and prove out entirely on the branch before it ever reaches `main`.
57
-
58
- ## Terminology, because "go all in on adapters" is two separate things
59
-
60
- The docs are explicit
61
- ([Runtime Integration](https://nextjs.org/docs/app/api-reference/adapters/runtime-integration)):
62
-
63
- > The Deployment Adapter API is a **build-time** interface. It tells your
64
- > platform what was built and how to route requests. **Runtime** behavior
65
- > (request handling, streaming, caching) is handled by the Next.js server itself
66
- > and by the cache interfaces `cacheHandler` and `cacheHandlers`.
67
-
68
- So there are two independent things:
69
-
70
- 1. **Implementing a `NextAdapter`** — `modifyConfig` + `onBuildComplete`.
71
- cdk-nextjs **already does this** (`src/adapter/adapter.mts`). Nothing to
72
- change here.
73
- 2. **Invoking build output entrypoints** instead of running `node server.js` —
74
- the subject of this plan.
75
-
76
- Nothing in the docs deprecates `output: "standalone"`. Entrypoint invocation is
77
- the supported path for a deployment platform and is what unlocks control over
78
- dispatch (hence middleware on `_next/image`) and per-route splitting. Keep the
79
- release notes precise about this: the claim is "cdk-nextjs now serves requests
80
- through the adapter entrypoints", not "cdk-nextjs now uses the adapters API",
81
- which has been true for a while.
82
-
83
- ## Spike 0 — packaging (before any Phase 3 code)
84
-
85
- **Question:** can the runtime asset fit in a zip Lambda, or must it stay a
86
- container image?
87
-
88
- Zip Lambdas cap at **250 MB unzipped**. The entrypoints `require`
89
- `next/dist/...` at runtime, so the `next` runtime closure ships with them.
90
-
91
- Measure, from `examples/app-playground` after a real build:
92
-
93
- 1. Dump `ctx.outputs` to JSON from `onBuildComplete` (`src/adapter/adapter.mts:34`).
94
- 2. For `appPages`, `appRoutes`, `pages`, `pagesApi`, `middleware`: record
95
- `filePath` and every key of `assets`.
96
- 3. `du -sh` the union of `assets` values plus the entrypoint files.
97
- 4. Answer specifically: **does `assets` include the `next` package itself**, or
98
- only app code and static deps? If not, the runtime needs
99
- `.next/standalone/node_modules` or an equivalent traced closure alongside it,
100
- and the number changes a lot.
101
-
102
- **Decision rule:**
103
-
104
- - Comfortably under ~200 MB unzipped → **zip Lambda**. Full win: no Docker for
105
- Functions, `awslambda.streamifyResponse` for real Lambda response streaming,
106
- glibc `sharp` only.
107
- - Over, or close → **keep a container image**, but rebase on
108
- `public.ecr.aws/docker/library/node:24` (glibc, not alpine) and drop Lambda
109
- Web Adapter for the Lambda Node runtime interface client. Still deletes LWA and
110
- the musl `sharp` path; loses the "no Docker required" headline.
111
-
112
- Record the measured numbers in this file before starting Phase 3, and do not put
113
- "no Docker required" in release notes until it is settled.
114
-
115
- ## Phase 1 — middleware runner (`src/middleware/`)
116
-
117
- This is now narrowly scoped: it is the `invokeMiddleware` callback that
118
- `resolveRoutes()` calls (Phase 2), nothing more.
119
-
120
- **Build side**, in `onBuildComplete`:
121
-
122
- - If `ctx.outputs.middleware` exists with `runtime !== "nodejs"`, **throw with a
123
- clear message**. Edge is
124
- [deprecated](https://nextjs.org/docs/messages/edge-runtime-deprecated) and
125
- Next 16's `proxy.ts` is always Node. Do not carry an edge code path,
126
- `globalThis._ENTRIES`, or `edgeRuntime` handling.
127
- - Copy `middleware.filePath` and every `assets` entry at its repo-root-relative
128
- key (keys are relative paths, values absolute).
129
- - Persist `config.matchers` and `config.env`.
130
-
131
- **Runtime side:** invoke the Node entrypoint signature
132
- `handler(req: IncomingMessage, res: ServerResponse, ctx)` and translate the
133
- `x-middleware-next` / `x-middleware-rewrite` / `x-middleware-request-*` response
134
- headers into whatever shape `resolveRoutes`'s `invokeMiddleware` expects. That
135
- header protocol is the part most likely to be subtly wrong — unit-test it
136
- directly.
137
-
138
- Matcher evaluation: `resolveRoutes` may handle matcher gating itself given
139
- `routes: routing`. **Verify before writing a matcher evaluator** — if it does,
140
- this phase is only the invocation plus header translation. If it does not, match
141
- on `matchers[].sourceRegex` honouring `has`/`missing`, and respect the injected
142
- `missing` rule for `x-prerender-revalidate` or ISR revalidation requests will be
143
- intercepted by user middleware.
144
-
145
- ## Phase 2 — dispatch via `@next/routing`
146
-
147
- **Do not hand-write a router.** `@next/routing` is first-party, published, zero
148
- dependencies, versioned in lockstep with Next (`16.3.5` exists on npm today).
149
-
150
- ```ts
151
- import { resolveRoutes } from '@next/routing'
152
-
153
- const result = await resolveRoutes({
154
- url: new URL(requestUrl),
155
- buildId,
156
- basePath: config.basePath || '',
157
- i18n: config.i18n,
158
- headers: new Headers(requestHeaders),
159
- requestBody, // ReadableStream
160
- pathnames, // pages+pagesApi+appPages+appRoutes+staticFiles pathnames
161
- routes: routing, // ctx.routing from onBuildComplete
162
- invokeMiddleware: async (ctx) => { /* Phase 1 */ },
163
- })
164
- ```
165
-
166
- It returns `middlewareResponded`, `externalRewrite`, `redirect`,
167
- `resolvedPathname` (the *template*, e.g. `/blog/[slug]`), `resolvedQuery`,
168
- `invocationTarget` (the concrete pathname/query to invoke), `resolvedHeaders`,
169
- `status`, and `routeMatches`.
170
-
171
- That collapses most of what an earlier draft scoped as hand-written work:
172
- locale normalization, `_next/data` normalization, `.rsc` suffixes, `basePath`,
173
- rewrites/redirects/headers ordering, and dynamic param extraction. Remaining
174
- work in this phase:
175
-
176
- - Persist `ctx.routing` + the `pathnames` list into the runtime asset at build
177
- time.
178
- - Map `resolvedPathname` → the right entrypoint `filePath`, and handle each
179
- non-entrypoint outcome (`redirect`, `externalRewrite`, `middlewareResponded`,
180
- `staticFiles` → S3/CloudFront).
181
- - Route `_next/image` to `src/image-optimization/`. It is **not** an adapter
182
- output type — `AdapterOutputs` has no image optimizer member — so it is
183
- permanently our own code, reached through dispatch *after* middleware. That is
184
- the fix for the problem that started this whole line of work.
185
- - Add `@next/routing` as a dependency and decide its version pinning relative to
186
- `next`. Lockstep versioning means a `next` bump likely needs a matching bump.
187
-
188
- **Tests:** commit `routing` + `outputs` JSON from a few fixture builds
189
- (app-playground, an i18n app, a `basePath` app) and assert dispatch decisions for
190
- synthetic URLs. Pure unit tests, no AWS.
191
-
192
- ## Phase 3 — one runtime, two thin shells
193
-
194
- One asset: all Node entrypoints, the union of their `assets`, the middleware
195
- runner, the image handler, the dispatcher. One `handleRequest(req, res)` core,
196
- wrapped by two shells:
197
-
198
- - **Lambda** handler (Functions types) — `streamifyResponse` if Spike 0 says zip.
199
- - **HTTP server** (Containers types) — same core behind `node:http`.
200
-
201
- Invoke entrypoints as `handler(req, res, { waitUntil, requestMeta })`.
202
-
203
- **Documented `requestMeta` fields** (from
204
- [Invoking Entrypoints](https://nextjs.org/docs/app/api-reference/adapters/invoking-entrypoints)):
205
-
206
- - `relativeProjectDir` — "Relative path from `process.cwd()` to the Next.js
207
- project directory." The in-Lambda layout is load-bearing; pin it and assert it.
208
- - `hostname` — used by route handlers building absolute URLs.
209
- - `revalidate` — "internal revalidate function to avoid revalidating over the
210
- network". Back it with the existing S3/DynamoDB path.
211
- - `render404` — for pages-router `notFound: true`.
212
-
213
- **`ctx.waitUntil`** keeps the function alive after the response so background
214
- revalidation completes. Wire it to the Lambda invocation lifecycle.
215
-
216
- **`requestMeta.onCacheEntryV2`** (documented, set via `addRequestMeta`) fires
217
- when a cache entry is generated or looked up, on the instance that handled the
218
- request. The docs name it as the hook for propagating cache updates to shared
219
- platform storage. Evaluate it against what `src/adapter/cache-handler.ts`
220
- already does via `cacheHandler` — they overlap, and the answer may be that the
221
- existing handler is sufficient. Decide explicitly rather than wiring both.
222
-
223
- **Do not set `minimalMode`.** See the note below. Absent is correct.
224
-
225
- **PPR resume protocol** — a real adapter responsibility, documented:
226
- `outputs.prerenders[].pprChain.headers` contains `{ 'next-resume': '1' }`. When a
227
- PPR route has a cached static shell, set those headers on the internal request,
228
- send it as a **POST** with `postponedState` as the body, and the handler renders
229
- only the deferred Suspense boundaries and streams. `next start` does shell +
230
- dynamic in one pass automatically; adapters must implement the chain. See
231
- [Implementing PPR in an Adapter](https://nextjs.org/docs/app/api-reference/adapters/implementing-ppr-in-an-adapter).
232
-
233
- **`output: "standalone"`** in `modifyConfig` (`src/adapter/adapter.mts:19`):
234
- keep it until Spike 0 confirms `assets` is a complete closure. Once all four
235
- types invoke entrypoints and the closure is proven, it can go — and with it the
236
- alpine standalone server and the `@img/sharp-linuxmusl-*` download
237
- (`src/nextjs-build/nextjs-build.ts:403`).
238
-
239
- **Sequencing on the branch:** do the Functions types first — that is where the
240
- benefit is concentrated. Containers second, before the branch merges, so there
241
- is one request path proven rather than two. Both must land before the PR opens
242
- against `main`; unlike an incremental-release plan, there is no "ship
243
- Functions-only, follow up on Containers later" option here — the branch isn't
244
- done, and doesn't merge, until all four types are on the new runtime. If
245
- Containers work turns out to be substantially harder than Functions, that's a
246
- timeline/scope conversation to have explicitly, not a default fallback.
247
-
248
- ## Phase 4 — splitting (NOT in this release)
249
-
250
- Per-output packaging driven by `config.maxDuration` / `preferredRegion`, with
251
- routing at CloudFront / API Gateway. If Phase 3 keeps dispatch separate from
252
- invocation this is a packaging change plus distribution wiring, with no runtime
253
- rework — which is exactly why it need not gate the release.
254
-
255
- ## Testing: use the official adapter harness as the primary gate
256
-
257
- Next.js ships a
258
- [compatibility test harness](https://nextjs.org/docs/app/api-reference/adapters/testing-adapters)
259
- that runs **vercel/next.js's own e2e suite against a real deployment**. This is a
260
- far better gate than any fixture app we would write, and it is the single most
261
- valuable item in this plan — more so now that there's no env-var-toggled
262
- byte-diff oracle to lean on mid-development.
263
-
264
- Wiring: three executables in `scripts/`, pointed at by env vars —
265
-
266
- - `NEXT_TEST_DEPLOY_SCRIPT_PATH` → builds and deploys the harness's isolated
267
- temp app; **prints only the deployment URL to stdout**, diagnostics to stderr,
268
- non-zero exit on failure. Runs with `cwd` set to the temp app.
269
- - `NEXT_TEST_DEPLOY_LOGS_SCRIPT_PATH` → must print lines starting `BUILD_ID:`,
270
- `DEPLOYMENT_ID:`, `NEXT_SUPPORTS_IMMUTABLE_ASSETS:`, then any logs. Separate
271
- process, so persist those to a file (e.g. `.adapter-build.log`) from the deploy
272
- script and replay them here.
273
- - `NEXT_TEST_CLEANUP_SCRIPT_PATH` → tears the stack down. **Not optional for us**
274
- — every run deploys real AWS resources, so a missing or failing cleanup leaks
275
- stacks and costs money. Make it idempotent and verify it runs on test failure.
276
-
277
- Harness run config from the docs: `NEXT_TEST_MODE=deploy`,
278
- `NEXT_EXTERNAL_TESTS_FILTERS=test/deploy-tests-manifest.json`, `ADAPTER_DIR`,
279
- `NEXT_ADAPTER_PATH` pointing at the built adapter, then
280
- `node run-tests.js --timings -g <n>/16 -c 2 --type e2e` from a next.js checkout.
281
- The example workflow shards 16 ways with a 60-minute timeout per shard.
282
-
283
- Cost and time are the real constraints here: 16 shards each deploying a CDK
284
- stack is not a per-PR gate. Run it `workflow_dispatch` + nightly against the
285
- release branch while the branch is under development, and keep
286
- `examples/e2e-tests/` as the fast per-commit check on the branch itself. Both
287
- must be green before the branch merges to `main`.
288
-
289
- The existing suite stays, but do not trust it as the sole gate for a runtime
290
- rewrite: it asserts status codes and one content-type, and **it cannot see the
291
- class of bug that produced this work** — a broken `sharp` returning HTTP 200
292
- with unoptimized original bytes, through four green e2e jobs. Add byte- and
293
- header-level assertions (`etag` shape: Next.js uses sha256/base64url while S3
294
- uses MD5 — that difference is what caught it), plus a middleware e2e proving a
295
- 403 or redirect on `_next/image`.
296
-
297
- Also still absent: the first CDK assertion test in `test/` (only `tsconfig.json`
298
- today). Blocked on `NextjsBuild` running `next build` in its constructor — needs
299
- `skipBuild: true` plus a committed fixture `.next`.
300
-
301
- ## On `minimalMode`: undocumented internal, do not set it
302
-
303
- An earlier draft said to "set `minimalMode: false` deliberately". That was
304
- overstated, and the field is not part of the documented API.
305
-
306
- - The
307
- [Invoking Entrypoints](https://nextjs.org/docs/app/api-reference/adapters/invoking-entrypoints)
308
- page lists only `relativeProjectDir`, `hostname`, `revalidate`, and `render404`
309
- ("Some of the supported fields are..."). `minimalMode` is not mentioned.
310
- - It is an internal `RequestMeta` field. From
311
- `next/dist/server/request-meta.d.ts:252-255`: *"Whether server is in minimal
312
- mode (this will be replaced with more specific flags in future)"* — explicitly
313
- marked for replacement.
314
- - Entrypoints read it via `getRequestMeta(req, 'minimalMode')`
315
- (`next/dist/build/templates/app-page-runtime.js:160`,
316
- `app-route.js:178`) and branch on it in ~20 places, including the one that
317
- matters: `getIncrementalCache(req, nextConfig, prerenderManifest, isMinimalMode)`
318
- at `app-page-runtime.js:504`.
319
-
320
- Because it is read with `Boolean(getRequestMeta(...))`, **absent means false**,
321
- which is the non-minimal path that keeps the incremental cache inside the
322
- entrypoint and therefore keeps `src/adapter/cache-handler.ts` working. That
323
- matches what Runtime Integration documents as the intended split: the adapter is
324
- build-time, caching is `cacheHandler`'s job. So there is no decision to make —
325
- just do not pass the field, and do not build anything that depends on its
326
- semantics.
327
-
328
- ## Open questions
329
-
330
- 1. **Spike 0's answer** — zip or container. Everything downstream of "no Docker
331
- required" depends on it.
332
- 2. Does `outputs.*.assets` include the `next` runtime closure, or only app code?
333
- 3. Does `resolveRoutes` gate middleware by `config.matchers` itself, or must the
334
- `invokeMiddleware` callback do it?
335
- 4. Cold-start cost of one Lambda holding N entrypoints. Eagerly `require`ing all
336
- of them makes every cold start pay for the whole app — lazy-require per
337
- dispatch decision, and measure.
338
- 5. `onCacheEntryV2` versus the existing `cacheHandler`: which owns propagation to
339
- S3/DynamoDB? Pick one.
340
- 6. `@next/routing` version pinning against `next`.
341
- 7. What happens to `docs/plans/phase-0-image-function-opt-in.md`'s
342
- `CDK_NEXTJS_EXPERIMENTAL_DEDICATED_IMAGE_FUNCTION` flag and the dedicated
343
- image Lambda it introduced. This plan's dispatcher makes that Lambda
344
- unnecessary (`_next/image` is reached through dispatch after middleware on
345
- every `NextjsType`), so the flag, the dedicated Lambda, and the branching it
346
- caused in `suppress-nags.ts`/`NextjsApi` are candidates for deletion in the
347
- same PR rather than being carried forward.
348
-
349
- ## Exit criteria for merging to `main`
350
-
351
- - Official harness green (or every exclusion documented with a reason).
352
- - All existing e2e suites green, including `streaming`, `isr`, `revalidation`,
353
- `server-actions`, on **all four** `NextjsType`s — not a subset.
354
- - Middleware e2e proving interception on `_next/image`.
355
- - Cold-start measurement on a non-trivial app versus the LWA path.
356
- - `docs/breaking-changes.md` covering the removed `dedicatedImageFunction` prop
357
- and — if Functions stop being container images — the
358
- `nextjsFunctionsProps.dockerImageFunctionProps` override keys, which become
359
- meaningless. That is a real breaking change for anyone using them.
360
- - No env var, flag, or dead code path left behind from development. If Spike 0
361
- or an earlier phase produced throwaway scaffolding, it is deleted before the
362
- PR opens, not carried into `main` "to remove later."
@@ -1,295 +0,0 @@
1
- # Phase 0 — Make the dedicated image optimization Lambda opt-in
2
-
3
- Handoff plan. Self-contained; assumes no context from the session that wrote it.
4
-
5
- ## Goal
6
-
7
- Land the `feat-nextjs-img-opt` branch **with zero breaking changes** by making the
8
- dedicated image optimization Lambda opt-in (`dedicatedImageFunction`, default
9
- `false`). The default stays what `main` does today: `_next/image` is served by the
10
- Next.js server function.
11
-
12
- ## Why
13
-
14
- The branch currently routes `_next/image` to a dedicated Lambda unconditionally.
15
- That Lambda is not the Next.js server, so **Next.js middleware no longer runs for
16
- image requests** — a real breaking change for anyone whose middleware matcher
17
- covers `/_next/image` (the default matcher `/:path*` does; the matcher
18
- `create-next-app` scaffolds does not).
19
-
20
- Making it opt-in means:
21
-
22
- - No breaking change, no migration doc.
23
- - The sharp fix in `40b7837` ships now. Without it the dedicated Lambda serves
24
- every image as unoptimized original bytes with an HTTP 200 and nothing in the
25
- logs — see "Background" below.
26
- - The flag is not throwaway. A later phase adds a middleware runner to the image
27
- Lambda (using `outputs.middleware` from the Next.js adapters API), and *that*
28
- release flips the default and deprecates the flag.
29
-
30
- ## Starting state
31
-
32
- Branch `feat-nextjs-img-opt`. `HEAD` = `40b7837 fix: ship a loadable sharp to the
33
- image optimization Lambda`.
34
-
35
- Ten files are **uncommitted** and complete — a second batch of code-review fixes.
36
- All checks were green when they were written (`tsc`, `jest src/` 51 passing,
37
- `eslint`, `jsii`):
38
-
39
- | File | What changed |
40
- | --- | --- |
41
- | `src/image-optimization/handler-utils.ts` + `.test.ts` | S3 key derivation now *ensures* the `basePath` prefix instead of stripping it (`NextjsStaticAssets` uploads under `basePath`), and matches `basePath` only on a path boundary so `/basement/logo.png` isn't read as `/base` + `ment/...` |
42
- | `src/image-optimization/handler.mts` | 304 responses only write the API-Gateway-appeasing space byte on the API Gateway path (RFC 9110 forbids a body on 304); adds `isApiGatewayEvent` type guard |
43
- | `src/utils/get-architecture.ts` | `getNodeArchitecture()` as the single source of truth, replacing two inline `process.arch.startsWith("arm")` copies |
44
- | `src/nextjs-build/nextjs-build.ts` | Uses `getNodeArchitecture()` |
45
- | `src/root-constructs/nextjs-base-construct.ts`, `nextjs-global-functions.ts`, `nextjs-regional-functions.ts`, `src/index.ts` | New `NextjsFunctionsConstructOverrides` holds `nextjsFunctionsProps` / `nextjsImageFunctionProps`, which the Containers constructs previously accepted and silently ignored |
46
- | `docs/breaking-changes.md` | Two new `## 0.6.0` sections |
47
-
48
- ### Step 0 — commit them first
49
-
50
- ```
51
- git add -A && git commit
52
- ```
53
-
54
- Suggested message: `fix: correct image optimization basePath keys, 304 bodies, and Functions-only overrides`
55
-
56
- Commit as-is even though Step 5 deletes one of the breaking-changes sections it
57
- adds — each commit stays internally consistent, and the sections are removed in
58
- the same PR.
59
-
60
- ## Design decisions (already made — don't relitigate)
61
-
62
- **Flag name:** `dedicatedImageFunction?: boolean`, `@default false`.
63
-
64
- **Where it goes:** three sibling interfaces.
65
-
66
- - `NextjsBaseConstructProps` (`src/root-constructs/nextjs-base-construct.ts:124`)
67
- — so `NextjsBaseConstruct` can read `this.baseProps.dedicatedImageFunction`
68
- when it builds `NextjsBuild`.
69
- - `NextjsGlobalFunctionsProps` (`src/root-constructs/nextjs-global-functions.ts:47`)
70
- - `NextjsRegionalFunctionsProps` (`src/root-constructs/nextjs-regional-functions.ts:43`)
71
-
72
- **Not** on `NextjsBaseProps` — the Containers constructs would accept it and
73
- silently ignore it, which is the exact anti-pattern the uncommitted
74
- `NextjsFunctionsConstructOverrides` change fixes.
75
-
76
- > JSII note: all three interfaces extend `NextjsBaseProps` but none extends
77
- > another, so declaring the same member on all three is legal. Declaring it on
78
- > both a parent and a child interface is **not** — that produces
79
- > `Interface ... re-declares member`, the error documented at
80
- > `nextjs-base-construct.ts:120-123`.
81
-
82
- **Why a boolean and not an enum:** JSII rejects string-literal union types, so a
83
- `"server" | "dedicated"` choice would need a real exported enum. Not worth it for
84
- a flag that gets deprecated in two releases.
85
-
86
- ## Tasks
87
-
88
- ### 1. `src/nextjs-build/nextjs-build.ts` — the part that will bite
89
-
90
- Add `dedicatedImageFunction?: boolean` to `NextjsBuildProps`, then rework the
91
- constructor block at **lines 132-152**.
92
-
93
- Current logic strips whatever platform sharp binaries output file tracing
94
- bundled, then installs musl replacements **only for Containers**, on the
95
- assumption that Functions deployments never invoke sharp from the standalone
96
- server. That assumption is exactly what this flag breaks.
97
-
98
- ```ts
99
- const useDedicatedImageFunction =
100
- isFunctions && props.dedicatedImageFunction === true;
101
-
102
- this.removeExistingSharpBinaries(standalonePath);
103
- // The standalone server serves `_next/image` itself unless a dedicated image
104
- // Lambda takes that route, and it runs on node:24-alpine (see
105
- // functions.Dockerfile), so it needs musl binaries.
106
- if (!useDedicatedImageFunction) {
107
- this.downloadAndInstallSharpBinaries();
108
- }
109
- if (useDedicatedImageFunction) {
110
- this.imageOptimizationAssetPath =
111
- this.prepareImageOptimizationAssets(standalonePath);
112
- }
113
- ```
114
-
115
- **Get this right or Phase 0 ships broken image optimization on the default
116
- path.** `removeExistingSharpBinaries()` runs unconditionally and deletes the
117
- host's (macOS/glibc) binaries. If the musl install doesn't run for Functions
118
- deployments that no longer have a dedicated image Lambda, `require('sharp')`
119
- throws inside the server, Next.js's `imageOptimizer` catches it internally and
120
- returns the *original* buffer, and you get an HTTP 200 with plausible headers
121
- serving unoptimized images. Silent. This is the same failure the branch already
122
- fixed once for the dedicated Lambda; don't reintroduce it on the default path.
123
-
124
- Also update the comment at lines 137-143, which currently states the now-
125
- conditional claim as fact.
126
-
127
- Adding a prop to `NextjsBuildProps` means `OptionalNextjsBuildProps` must be
128
- regenerated — see Verification.
129
-
130
- ### 2. `src/root-constructs/nextjs-base-construct.ts`
131
-
132
- - Add the prop to `NextjsBaseConstructProps`.
133
- - Pass it into `NextjsBuild` in `createNextjsBuild()` (line 195).
134
- - `createNextjsImageFunction()` (line 251) keeps throwing when
135
- `imageOptimizationAssetPath` is missing — that's still a genuine invariant
136
- violation, since callers only reach it when the flag is on.
137
-
138
- ### 3. The two Functions root constructs
139
-
140
- Make the field optional and conditional:
141
-
142
- ```ts
143
- nextjsImageFunction?: NextjsImageFunction;
144
- // ...
145
- if (props.dedicatedImageFunction) {
146
- this.nextjsImageFunction = this.createNextjsImageFunction(
147
- this.props.overrides?.nextjsImageFunction,
148
- );
149
- }
150
- ```
151
-
152
- - `nextjs-global-functions.ts:67` (field), `:83` (creation), `:95` — pass
153
- `imageFunctionUrl: this.nextjsImageFunction?.functionUrl`.
154
- - `nextjs-regional-functions.ts:56` (field), `:76` (creation), `:87` — pass
155
- `imageFunction: this.nextjsImageFunction?.function`.
156
-
157
- An optional class property is fine for JSII (`NextjsBuild.imageOptimizationAssetPath`
158
- already is one).
159
-
160
- ### 4. Make the two consumers tolerate a missing image function
161
-
162
- **`src/nextjs-api.ts`** — revert this branch's tightening:
163
-
164
- - `validateProps` (line 110-114): drop the "imageFunction must be set when
165
- serverFunction is set" throw.
166
- - Constructor (line 97-100): call `createImageIntegration` only when
167
- `props.imageFunction` is set. With no image integration, `_next/image` falls
168
- through to the `{proxy+}` catch-all on the server function — which is what
169
- `main` does.
170
- - Update the `imageFunction` doc comment (line 53-62): no longer "Required if
171
- `serverFunction` is set".
172
-
173
- **`src/nextjs-distribution.ts`** — `createImageOrigin()` (line 201-215) already
174
- falls back to `this.dynamicOrigin` for non-Functions compute. Extend that
175
- fallback instead of throwing:
176
-
177
- ```ts
178
- if (!this.isFunctionCompute || !this.props.imageFunctionUrl) {
179
- return this.dynamicOrigin;
180
- }
181
- ```
182
-
183
- Keep the dedicated `_next/image*` behavior and its image-specific cache policy
184
- in both cases — pointing that behavior at the dynamic origin is strictly better
185
- than dropping it, because the image cache policy (`queryStringBehavior: all()`,
186
- `accept` in the cache key) is the right one for image requests either way. Update
187
- the doc comment on `imageFunctionUrl` (line 79-83) and on `createImageOrigin`.
188
-
189
- ### 5. `docs/breaking-changes.md`
190
-
191
- From the `## 0.6.0` section added in Step 0:
192
-
193
- - **Delete** "`_next/image` no longer runs Next.js middleware (Functions only)".
194
- It isn't true once the flag defaults off.
195
- - **Rewrite** "NextjsApi: `imageFunction` now required alongside
196
- `serverFunction`" — it's optional again. Either delete it or restate it as a
197
- non-breaking addition.
198
- - **Keep** "Functions-only overrides moved off the Containers constructs". That
199
- one is a real (tiny) breaking change and it ships.
200
-
201
- Document the trade-off on the new prop's JSDoc, since that's what users will
202
- read:
203
-
204
- - **off (default)** — middleware runs for image requests; on Regional Functions
205
- the response goes through API Gateway buffered (6 MB cap); images share the
206
- server function's memory and concurrency; optimized images are cached via the
207
- configured `cacheHandler`.
208
- - **on** — dedicated Lambda with response streaming and independent sizing; no
209
- middleware for `_next/image`; caching relies on CloudFront.
210
- - Note that a future release will default this on once middleware support lands.
211
-
212
- ### 6. Examples and e2e
213
-
214
- Set `dedicatedImageFunction: true` in **`examples/global-functions/app.ts`** only.
215
- Leave `examples/regional-functions/app.ts` on the default. That way CI covers
216
- both paths: `glbl-fns` exercises the dedicated Lambda, `rgnl-fns` exercises the
217
- server-function path.
218
-
219
- `examples/e2e-tests/src/image-optimization.test.ts` should pass unchanged on both
220
- — including the `content-type: image/webp` assertion added in `40b7837`, since
221
- the server function re-encodes with sharp too (that's what Task 1 guarantees).
222
- If it fails on `rgnl-fns`, suspect the musl install, not the test.
223
-
224
- ### 7. Stretch — first construct test
225
-
226
- `test/` currently holds only a `tsconfig.json`; there are no CDK assertion tests.
227
- A worthwhile addition: with the flag off, assert the synthesized template has one
228
- Lambda and the `_next/image*` cache behavior targets the server origin; with it
229
- on, two Lambdas. The obstacle is that `NextjsBuild` runs `next build` in its
230
- constructor, so a test needs `skipBuild: true` plus a committed fixture `.next`
231
- directory. Skip if it balloons; it's not a Phase 0 blocker.
232
-
233
- ## Verification
234
-
235
- ```bash
236
- npx tsc --noEmit -p tsconfig.json
237
- npx jest src/ # 51 tests before this change
238
- ESLINT_USE_FLAT_CONFIG=false npx eslint --fix --ext .ts src/
239
- npx jsii --silence-warnings=reserved-word
240
- ```
241
-
242
- Then, because `NextjsBuildProps` changed, in this order (the build-order quirk in
243
- `CLAUDE.md`):
244
-
245
- ```bash
246
- pnpm compile # writes .jsii
247
- pnpm projen # regenerates src/generated-structs/OptionalNextjsBuildProps.ts from .jsii
248
- pnpm build
249
- npx jsii-docgen -o API.md # API.md is committed and generated from .jsii
250
- ```
251
-
252
- Notes:
253
-
254
- - `pnpm eslint` and `pnpm projen` shell out to `pnpm dlx projen`, which needs
255
- network access to `registry.npmjs.org`. In a sandboxed session that fails with
256
- `ERR_PNPM_FETCH_403`; the `npx` forms above work around it.
257
- - `src/image-optimization/handler.mts` cannot be linted — pre-existing, the
258
- ESLint project service only covers `.ts`/`.tsx`.
259
- - `API.md` is 11,973 lines and committed. Forgetting to regenerate it will show
260
- up as a CI self-mutation diff.
261
-
262
- ## Out of scope
263
-
264
- - Any middleware execution. That's the next phase: copy
265
- `ctx.outputs.middleware.filePath` + `assets` in `onBuildComplete`, match on
266
- `config.matchers[].sourceRegex` with `has`/`missing`, invoke the entrypoint's
267
- exported `handler(req, res, ctx)`. Node runtime only — edge is deprecated in
268
- Next.js 16.
269
- - Replacing the server function with adapter entrypoints.
270
- - Widening the image cache policy's `cookieBehavior` (currently `none()` at
271
- `nextjs-distribution.ts:338`). Only matters once middleware makes
272
- cookie-dependent decisions for image requests.
273
-
274
- ## Background: the bug that started this
275
-
276
- `next/dist/server/image-optimizer.js` reports a failed optimization by
277
- **returning** `{ buffer: upstreamBuffer, ..., error }` rather than throwing. The
278
- handler dropped `error`, so a broken sharp produced a valid HTTP 200 serving
279
- full-size originals forever, with clean logs. It got through four green e2e jobs
280
- and a live CloudFront endpoint that rendered all three test images correctly.
281
-
282
- It was caught by comparing bytes: the `_next/image` response was byte-identical
283
- to the raw S3 asset (31,283 bytes, md5 `ac38a73ecb20a904d52ebc9b28aa171f`),
284
- returned `content-type: image/png` despite `Accept: image/webp`, and carried
285
- S3's MD5 ETag rather than Next.js's sha256/base64url one.
286
-
287
- Root cause: `next build`'s output file tracing preserves the installer's layout,
288
- and under pnpm there is never a hoisted `node_modules/sharp` — only
289
- `node_modules/.pnpm/sharp@<version>/node_modules/sharp`. The old flat
290
- `existsSync` check never matched, so sharp was never bundled. Fixed in `40b7837`
291
- along with logging the swallowed `error`.
292
-
293
- The lesson for every later phase: the e2e suite asserts status codes, so it
294
- cannot see semantically wrong but syntactically fine responses. Assertions on
295
- bytes and headers are what caught this.