cdk-nextjs 0.5.15 → 0.6.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 (53) hide show
  1. package/.jsii +821 -253
  2. package/.jsii.tabl.json +1 -1
  3. package/API.md +787 -18
  4. package/README.md +3 -3
  5. package/docs/breaking-changes.md +86 -0
  6. package/docs/caching-guide.md +6 -0
  7. package/docs/plans/adapter-runtime-release.md +362 -0
  8. package/docs/plans/phase-0-image-function-opt-in.md +295 -0
  9. package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +13 -0
  10. package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
  11. package/lib/image-optimization/handler-utils.d.ts +34 -0
  12. package/lib/image-optimization/handler-utils.js +83 -0
  13. package/lib/image-optimization/handler-utils.test.d.ts +1 -0
  14. package/lib/image-optimization/handler-utils.test.js +178 -0
  15. package/lib/image-optimization/handler.mjs +56126 -0
  16. package/lib/index.d.ts +2 -1
  17. package/lib/index.js +4 -2
  18. package/lib/nextjs-api.d.ts +44 -0
  19. package/lib/nextjs-api.js +64 -20
  20. package/lib/nextjs-api.test.d.ts +1 -0
  21. package/lib/nextjs-api.test.js +144 -0
  22. package/lib/nextjs-build/nextjs-build.d.ts +60 -0
  23. package/lib/nextjs-build/nextjs-build.js +184 -27
  24. package/lib/nextjs-cache.js +1 -1
  25. package/lib/nextjs-compute/nextjs-containers.js +1 -1
  26. package/lib/nextjs-compute/nextjs-functions.js +1 -1
  27. package/lib/nextjs-compute/nextjs-image-function.d.ts +53 -0
  28. package/lib/nextjs-compute/nextjs-image-function.js +59 -0
  29. package/lib/nextjs-distribution.d.ts +33 -0
  30. package/lib/nextjs-distribution.js +39 -11
  31. package/lib/nextjs-post-deploy.js +1 -1
  32. package/lib/nextjs-static-assets.d.ts +18 -0
  33. package/lib/nextjs-static-assets.js +33 -6
  34. package/lib/nextjs-static-assets.test.d.ts +1 -0
  35. package/lib/nextjs-static-assets.test.js +58 -0
  36. package/lib/root-constructs/nextjs-base-construct.d.ts +62 -2
  37. package/lib/root-constructs/nextjs-base-construct.js +81 -3
  38. package/lib/root-constructs/nextjs-global-containers.d.ts +6 -1
  39. package/lib/root-constructs/nextjs-global-containers.js +11 -5
  40. package/lib/root-constructs/nextjs-global-functions.d.ts +16 -5
  41. package/lib/root-constructs/nextjs-global-functions.js +23 -21
  42. package/lib/root-constructs/nextjs-regional-containers.js +1 -1
  43. package/lib/root-constructs/nextjs-regional-functions.d.ts +11 -5
  44. package/lib/root-constructs/nextjs-regional-functions.js +17 -21
  45. package/lib/utils/base-path.d.ts +40 -0
  46. package/lib/utils/base-path.js +123 -0
  47. package/lib/utils/base-path.test.d.ts +1 -0
  48. package/lib/utils/base-path.test.js +163 -0
  49. package/lib/utils/experimental-flags.d.ts +24 -0
  50. package/lib/utils/experimental-flags.js +30 -0
  51. package/lib/utils/get-architecture.d.ts +10 -0
  52. package/lib/utils/get-architecture.js +17 -2
  53. package/package.json +3 -1
package/README.md CHANGED
@@ -281,7 +281,7 @@ cdk-nextjs supports importing existing AWS resources instead of creating new one
281
281
  ### Resource Isolation
282
282
 
283
283
  - **Cache bucket and DynamoDB table** are isolated by `buildId` prefix. Multiple branches safely share one bucket/table with no conflicts.
284
- - **Static assets bucket** — Next.js includes content hashes in static asset filenames, so different branches deploying the same file will produce identical content. It's safe for branches to overwrite each other. If you're already using `basePath` for routing, assets will naturally be prefixed by it.
284
+ - **Static assets bucket** — Next.js includes content hashes in static asset filenames, so different branches deploying the same file will produce identical content. It's safe for branches to overwrite each other. If you're already using `basePath` for routing, assets will naturally be prefixed by it — `NextjsGlobalFunctions` and `NextjsGlobalContainers` read the `basePath` out of your Next.js build and use it as the S3 key prefix, since CloudFront serves assets from S3 by request path and the two can't differ. Set the `basePath` prop only if you want to be explicit about it; a value that disagrees with your app's fails at synth.
285
285
 
286
286
  ### Shared ALB and `removeAutoCreatedListener()`
287
287
 
@@ -344,8 +344,8 @@ Note: CloudFront distributions take several minutes to create/update, so this ar
344
344
  - If using `NextjsGlobalFunctions` or `NextjsGlobalContainers` (which use CloudFront), the number of top level files/directories cannot exceed 25, the max number of behaviors a CloudFront Distribution supports. We recommend you put all of your public assets into one top level directory (i.e. public/static) so you don't reach this limit. See [CloudFront Quotas](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html) for more information.
345
345
  - If using `NextjsGlobalFunctions`, when [revalidating data in Next.js](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#on-demand-revalidation) (i.e. [revalidatePath](https://nextjs.org/docs/app/api-reference/functions/revalidatePath)), the CloudFront Cache will still hold stale data. You'll need to use AWS SDK JS V3 [CreateInvalidationCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-cloudfront/Class/CreateInvalidationCommand/) to manually invalidate the path in CloudFront. See more [here](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html).
346
346
  - If using `NextjsGlobalFunctions`, setting an Authorization header won't work by default because of Lambda Function URL with IAM Auth is already using the Authorization header. You can use the `AWS_LWA_AUTHORIZATION_SOURCE` environment variable of [AWS Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter) to set an alternative Authorization header in the client which will then be set to the Authorization header when it reaches your app.
347
- - `NextjsRegionalFunctions` doesn't support streaming because API Gateway doesn't support streaming yet.
348
- - If using `NextjsRegionalFunctions` without a custom domain, API Gateway REST APIs require a [stage name](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-stages.html) (default: `/prod`) to be specified. This causes links to pages and static assets to break because they're not prefixed with the stage name. You can work around this issue by specifying [basePath](https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath) in next.config.js as your stage name. Additionally, you'll need to add middleware logic to rewrite requests to include the stage name because API Gateway does not include the stage name in the path passed to Lambda. See [examples/app-playground/middleware.ts](./examples/app-playground/middleware.ts).
347
+ - `NextjsRegionalFunctions` streaming depends on whether your server Lambda supports response streaming (e.g. [AWS Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter)'s `AWS_LWA_INVOKE_MODE`) — override `overrides.nextjsApi.dynamicIntegrationProps.responseTransferMode` to `BUFFERED` if it doesn't, otherwise API Gateway returns a 500 for every request.
348
+ - If using `NextjsRegionalFunctions` without a custom domain, API Gateway REST APIs require a [stage name](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-stages.html) (default: `/prod`) to be specified. This causes links to pages and static assets to break because they're not prefixed with the stage name. You can work around this issue by specifying [basePath](https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath) in next.config.js as your stage name — leave the construct's `basePath` prop unset when you do, since API Gateway strips the stage before matching resources. Additionally, you'll need to add middleware logic to rewrite requests to include the stage name because API Gateway does not include the stage name in the path passed to Lambda. See [examples/app-playground/middleware.ts](./examples/app-playground/middleware.ts). Mapping a custom domain at the root avoids all of this — see [examples/regional-functions/README.md](./examples/regional-functions/README.md#with-a-custom-domain-no-stage-workarounds).
349
349
 
350
350
  ## Additional Security Recommendations
351
351
 
@@ -1,5 +1,91 @@
1
1
  # Breaking Changes
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### `basePath` is reconciled with your Next.js app's own `basePath`
6
+
7
+ The `basePath` prop and the `basePath` in your app's `next.config.js` are two
8
+ different things — where the infrastructure serves the app from (and which S3 key
9
+ prefix the static assets land under) versus the prefix the app emits its own
10
+ links and asset hrefs under. They used to be set independently, which silently
11
+ 404'd static assets when they disagreed. They're now checked against each other
12
+ at synth, and derived from the app where there's only one value that can work.
13
+
14
+ - `NextjsGlobalFunctions` / `NextjsGlobalContainers`: leave the prop unset and it
15
+ now follows your app's `basePath`. CloudFront hands its S3 origin the request
16
+ path verbatim as the object key, so the two can't differ — a prop that
17
+ disagrees with your app throws at synth.
18
+ - `NextjsRegionalFunctions`: the prop is never derived (API Gateway strips the
19
+ stage before matching resources, so an app served at the `prod` stage sets
20
+ `basePath: "/prod"` and leaves the prop unset). If you do set it, your app's
21
+ `basePath` must end with it — either equal to it, or prefixed by the stage or
22
+ base path mapping API Gateway strips (`basePath: "/prod/base"` with the prop
23
+ set to `"/base"`). Anything else throws.
24
+ - `NextjsRegionalContainers`: unchanged. The ALB forwards every path to the
25
+ container, which serves its own static assets, so the prop only namespaces the
26
+ S3 bucket and is unconstrained.
27
+ - Surrounding slashes on the prop are now normalized away, so `"/base"`,
28
+ `"base"` and `"/base/"` all behave identically. Previously the prop was passed
29
+ through to `NextjsDistribution` as written, so a trailing slash produced cache
30
+ behaviors like `/base//_next/static*` against keys uploaded under
31
+ `base/_next/...`, and a missing leading slash produced `base/_next/static*`.
32
+ **If you passed `basePath` without a leading slash**, your distribution's cache
33
+ behavior path patterns change from `base/…` to `/base/…` on the next deploy.
34
+ This is an in-place update of the existing distribution, not a replacement.
35
+ - On the Global constructs, an
36
+ `overrides.nextjsStaticAssets.bucketDeploymentProps.destinationKeyPrefix` that
37
+ doesn't match `basePath` now throws: CloudFront's S3 origin has no way to ask
38
+ for the objects anywhere but the request path, so a different prefix 404s every
39
+ static request.
40
+ - `NextjsGlobalFunctions.url` and `NextjsGlobalContainers.url` now include
41
+ `basePath`, and `NextjsRegionalFunctions.url` reports the custom domain and
42
+ base path mapping when one is configured. All three previously returned a URL
43
+ the app answers with a 404 under these setups.
44
+
45
+ - **Migration:** If your app sets `basePath` and you deploy a Global construct,
46
+ either drop the `basePath` prop or set it to the same value your app uses. The
47
+ first deploy after upgrading uploads the static assets under `<basePath>/` in
48
+ S3 and adds `<basePath>/_next/static*` (and `<basePath>/*`) cache behaviors to
49
+ the distribution. If you were reading `url` and appending `basePath` yourself,
50
+ remove that — it's included now.
51
+
52
+ Note: when `.next/required-server-files.json` can't be read, cdk-nextjs assumes
53
+ your app sets no `basePath` and now warns rather than staying silent, since that
54
+ fallback is otherwise indistinguishable from an app that genuinely sets none.
55
+
56
+ ## 0.5.16
57
+
58
+ ### Functions-only overrides moved off the Containers constructs
59
+
60
+ `nextjsFunctionsProps` and `nextjsImageFunctionProps` moved from
61
+ `NextjsBaseConstructOverrides` to the new `NextjsFunctionsConstructOverrides`,
62
+ which only the two Functions root constructs extend. They were previously
63
+ accepted by `NextjsGlobalContainers` / `NextjsRegionalContainers` and silently
64
+ ignored there.
65
+
66
+ - **Migration:** Remove those keys from Containers construct overrides. If you
67
+ were setting them and expecting an effect, there wasn't one.
68
+
69
+ ### New (non-breaking): lower-level image optimization building blocks
70
+
71
+ `_next/image` behavior is unchanged — the Next.js server function still serves
72
+ it on every `NextjsType`, so Next.js middleware still runs for image requests.
73
+
74
+ These additive, optional props let you wire your own image optimization Lambda
75
+ if you want one:
76
+
77
+ - `NextjsApiProps.imageFunction` — when omitted, `_next/image` falls through to
78
+ `serverFunction` via the `{proxy+}` catch-all, as before.
79
+ - `NextjsDistributionProps.imageFunctionUrl` — when omitted, the `_next/image*`
80
+ behavior points at the dynamic origin, as before.
81
+ - The `NextjsImageFunction` construct is exported for direct use.
82
+
83
+ `NextjsGlobalFunctions.nextjsImageFunction` and
84
+ `NextjsRegionalFunctions.nextjsImageFunction` are `undefined` unless the
85
+ dedicated image Lambda is enabled, which no supported prop does yet. A future
86
+ release routes `_next/image` through the adapter runtime after middleware; see
87
+ `docs/plans/adapter-runtime-release.md`.
88
+
3
89
  ## 0.5.0
4
90
 
5
91
  ### Architecture Changes
@@ -56,6 +56,12 @@ Next.js uses multiple caching layers, each with a specific `CachedRouteKind` tha
56
56
  - **Optimization**: Cached resized, format-converted images
57
57
  - **Revalidation**: Time-based or on-demand revalidation
58
58
 
59
+ This applies on every `NextjsType`, since `_next/image` is served by the Next.js
60
+ server. If you replace that route with your own image optimization Lambda (via
61
+ `NextjsApiProps.imageFunction` or `NextjsDistributionProps.imageFunctionUrl`),
62
+ this cache kind is no longer written and you rely on HTTP caching instead
63
+ (`Cache-Control`/`ETag` response headers, browser/CDN-cached).
64
+
59
65
  ### 5. Redirect Cache (REDIRECT)
60
66
 
61
67
  **What**: Cached redirect configurations for dynamic redirects.
@@ -0,0 +1,362 @@
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."