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.
- package/.jsii +821 -253
- package/.jsii.tabl.json +1 -1
- package/API.md +787 -18
- package/README.md +3 -3
- package/docs/breaking-changes.md +86 -0
- package/docs/caching-guide.md +6 -0
- package/docs/plans/adapter-runtime-release.md +362 -0
- package/docs/plans/phase-0-image-function-opt-in.md +295 -0
- package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +13 -0
- package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
- package/lib/image-optimization/handler-utils.d.ts +34 -0
- package/lib/image-optimization/handler-utils.js +83 -0
- package/lib/image-optimization/handler-utils.test.d.ts +1 -0
- package/lib/image-optimization/handler-utils.test.js +178 -0
- package/lib/image-optimization/handler.mjs +56126 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +4 -2
- package/lib/nextjs-api.d.ts +44 -0
- package/lib/nextjs-api.js +64 -20
- package/lib/nextjs-api.test.d.ts +1 -0
- package/lib/nextjs-api.test.js +144 -0
- package/lib/nextjs-build/nextjs-build.d.ts +60 -0
- package/lib/nextjs-build/nextjs-build.js +184 -27
- 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-compute/nextjs-image-function.d.ts +53 -0
- package/lib/nextjs-compute/nextjs-image-function.js +59 -0
- package/lib/nextjs-distribution.d.ts +33 -0
- package/lib/nextjs-distribution.js +39 -11
- package/lib/nextjs-post-deploy.js +1 -1
- package/lib/nextjs-static-assets.d.ts +18 -0
- package/lib/nextjs-static-assets.js +33 -6
- package/lib/nextjs-static-assets.test.d.ts +1 -0
- package/lib/nextjs-static-assets.test.js +58 -0
- package/lib/root-constructs/nextjs-base-construct.d.ts +62 -2
- package/lib/root-constructs/nextjs-base-construct.js +81 -3
- package/lib/root-constructs/nextjs-global-containers.d.ts +6 -1
- package/lib/root-constructs/nextjs-global-containers.js +11 -5
- package/lib/root-constructs/nextjs-global-functions.d.ts +16 -5
- package/lib/root-constructs/nextjs-global-functions.js +23 -21
- package/lib/root-constructs/nextjs-regional-containers.js +1 -1
- package/lib/root-constructs/nextjs-regional-functions.d.ts +11 -5
- package/lib/root-constructs/nextjs-regional-functions.js +17 -21
- package/lib/utils/base-path.d.ts +40 -0
- package/lib/utils/base-path.js +123 -0
- package/lib/utils/base-path.test.d.ts +1 -0
- package/lib/utils/base-path.test.js +163 -0
- package/lib/utils/experimental-flags.d.ts +24 -0
- package/lib/utils/experimental-flags.js +30 -0
- package/lib/utils/get-architecture.d.ts +10 -0
- package/lib/utils/get-architecture.js +17 -2
- 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`
|
|
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
|
|
package/docs/breaking-changes.md
CHANGED
|
@@ -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
|
package/docs/caching-guide.md
CHANGED
|
@@ -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."
|