cdk-nextjs 0.6.2 → 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.
- package/.jsii +2404 -2723
- package/.jsii.tabl.json +1 -1
- package/API.md +1681 -1930
- package/CLAUDE.md +2 -1
- package/README.md +429 -130
- package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +148 -169
- package/assets/lambdas/redeploy-stage/redeploy-stage.lambda/index.js +875 -0
- package/docs/adapter-runtime.md +185 -0
- package/docs/breaking-changes.md +667 -4
- package/docs/caching-guide.md +156 -112
- package/docs/cdk-nextjs-cfn-exec-policy.json +2 -1
- package/docs/development-guide.md +1 -1
- package/docs/harness-coverage.md +598 -0
- package/docs/init-cache-deployment.md +32 -1
- package/docs/next-build-output-guide.md +30 -20
- package/examples/README.md +5 -1
- package/lib/adapter/__fixtures__/app-playground-base-path.json +4926 -0
- package/lib/adapter/__fixtures__/app-playground.json +4917 -0
- package/lib/adapter/__fixtures__/pages-i18n.json +1728 -0
- package/lib/adapter/adapter.mjs +1548 -185
- package/lib/adapter/adapter.test.js +106 -0
- package/lib/adapter/aws-cache-store.d.ts +489 -0
- package/lib/adapter/aws-cache-store.js +1004 -0
- package/lib/adapter/aws-cache-store.test.d.ts +1 -0
- package/lib/adapter/aws-cache-store.test.js +361 -0
- package/lib/adapter/build-outputs.d.ts +80 -0
- package/lib/adapter/build-outputs.js +852 -0
- package/lib/adapter/build-outputs.test.d.ts +1 -0
- package/lib/adapter/build-outputs.test.js +861 -0
- package/lib/adapter/cache-handler.d.ts +9 -2
- package/lib/adapter/cache-handler.js +72 -96
- package/lib/adapter/cache-handler.mjs +18956 -17774
- package/lib/adapter/cache-handler.test.js +263 -31
- package/lib/adapter/cache-kinds.d.ts +32 -0
- package/lib/adapter/cache-kinds.js +35 -0
- package/lib/adapter/cache-kinds.test.d.ts +1 -0
- package/lib/adapter/cache-kinds.test.js +69 -0
- package/lib/adapter/cache-utils.d.ts +90 -0
- package/lib/adapter/cache-utils.js +199 -4
- package/lib/adapter/cache-utils.test.d.ts +1 -0
- package/lib/adapter/cache-utils.test.js +179 -0
- package/lib/adapter/function-groups.d.ts +215 -0
- package/lib/adapter/function-groups.js +1099 -0
- package/lib/adapter/function-groups.test.d.ts +1 -0
- package/lib/adapter/function-groups.test.js +743 -0
- package/lib/adapter/init-cache.d.ts +6 -0
- package/lib/adapter/init-cache.js +245 -0
- package/lib/adapter/init-cache.test.d.ts +1 -0
- package/lib/adapter/init-cache.test.js +137 -0
- package/lib/adapter/local-file-cache-handler.d.ts +23 -0
- package/lib/adapter/local-file-cache-handler.js +52 -1
- package/lib/adapter/memory-cache-handler.d.ts +26 -17
- package/lib/adapter/memory-cache-handler.js +32 -50
- package/lib/adapter/memory-cache-handler.test.js +105 -85
- package/lib/adapter/s3-cache-handler.d.ts +179 -40
- package/lib/adapter/s3-cache-handler.js +807 -277
- package/lib/adapter/s3-cache-handler.test.js +1186 -31
- package/lib/adapter/use-cache-common.d.ts +90 -0
- package/lib/adapter/use-cache-common.js +250 -0
- package/lib/adapter/use-cache-common.test.d.ts +1 -0
- package/lib/adapter/use-cache-common.test.js +646 -0
- package/lib/adapter/use-cache-default-handler.d.ts +24 -0
- package/lib/adapter/use-cache-default-handler.js +90 -0
- package/lib/adapter/use-cache-default-handler.mjs +38645 -0
- package/lib/adapter/use-cache-handlers.test.d.ts +1 -0
- package/lib/adapter/use-cache-handlers.test.js +349 -0
- package/lib/adapter/use-cache-remote-handler.d.ts +26 -0
- package/lib/adapter/use-cache-remote-handler.js +186 -0
- package/lib/{image-optimization/handler.mjs → adapter/use-cache-remote-handler.mjs} +33572 -38798
- package/lib/generated-structs/OptionalNextjsApiProps.d.ts +59 -0
- package/lib/generated-structs/OptionalNextjsApiProps.js +3 -0
- package/lib/generated-structs/OptionalNextjsCacheProps.d.ts +15 -6
- package/lib/generated-structs/OptionalNextjsCacheProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsContainersProps.d.ts +25 -11
- package/lib/generated-structs/OptionalNextjsContainersProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +31 -9
- package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsPostDeployProps.d.ts +8 -0
- package/lib/generated-structs/OptionalNextjsPostDeployProps.js +1 -1
- package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.d.ts +6 -4
- package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.js +1 -1
- package/lib/index.d.ts +6 -7
- package/lib/index.js +2 -4
- package/lib/lambdas/post-deploy/prune-cache-bucket.d.ts +7 -3
- package/lib/lambdas/post-deploy/prune-cache-bucket.js +57 -71
- package/lib/lambdas/post-deploy/prune-cache-bucket.test.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-cache-bucket.test.js +114 -0
- package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-revalidation-table.js +23 -31
- package/lib/lambdas/post-deploy/prune-revalidation-table.test.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +71 -0
- package/lib/lambdas/post-deploy/prune-s3.d.ts +14 -5
- package/lib/lambdas/post-deploy/prune-s3.js +41 -12
- package/lib/lambdas/post-deploy/prune-s3.test.js +97 -18
- package/lib/lambdas/redeploy-stage/redeploy-stage-function.d.ts +13 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage-function.js +55 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.d.ts +24 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.js +68 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.d.ts +20 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.js +24 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.test.d.ts +1 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.test.js +93 -0
- package/lib/nextjs-api.d.ts +102 -18
- package/lib/nextjs-api.js +338 -38
- package/lib/nextjs-api.test.js +537 -1
- package/lib/nextjs-build/global-containers.Dockerfile +15 -10
- package/lib/nextjs-build/nextjs-build.d.ts +339 -63
- package/lib/nextjs-build/nextjs-build.js +684 -327
- package/lib/nextjs-build/nextjs-build.test.d.ts +1 -0
- package/lib/nextjs-build/nextjs-build.test.js +412 -0
- package/lib/nextjs-build/patch-fetch.js +1 -1
- package/lib/nextjs-build/patch-fetch.test.js +166 -11
- package/lib/nextjs-build/regional-containers.Dockerfile +17 -10
- package/lib/nextjs-cache.d.ts +82 -4
- package/lib/nextjs-cache.js +186 -10
- package/lib/nextjs-cache.test.js +176 -1
- package/lib/nextjs-compute/nextjs-compute-base-props.d.ts +21 -8
- package/lib/nextjs-compute/nextjs-compute-base-props.js +41 -1
- package/lib/nextjs-compute/nextjs-compute-base-props.test.d.ts +1 -0
- package/lib/nextjs-compute/nextjs-compute-base-props.test.js +44 -0
- package/lib/nextjs-compute/nextjs-containers.d.ts +19 -0
- package/lib/nextjs-compute/nextjs-containers.js +57 -20
- package/lib/nextjs-compute/nextjs-functions.d.ts +122 -7
- package/lib/nextjs-compute/nextjs-functions.js +113 -67
- package/lib/nextjs-compute/nextjs-functions.test.d.ts +1 -0
- package/lib/nextjs-compute/nextjs-functions.test.js +99 -0
- package/lib/nextjs-distribution.d.ts +194 -23
- package/lib/nextjs-distribution.js +535 -62
- package/lib/nextjs-distribution.test.d.ts +1 -0
- package/lib/nextjs-distribution.test.js +697 -0
- package/lib/nextjs-post-deploy.d.ts +13 -5
- package/lib/nextjs-post-deploy.js +13 -10
- package/lib/nextjs-post-deploy.test.d.ts +1 -0
- package/lib/nextjs-post-deploy.test.js +34 -0
- package/lib/nextjs-static-assets.d.ts +17 -4
- package/lib/nextjs-static-assets.js +22 -5
- package/lib/root-constructs/nextjs-base-construct.d.ts +102 -58
- package/lib/root-constructs/nextjs-base-construct.js +139 -47
- package/lib/root-constructs/nextjs-global-containers.d.ts +18 -20
- package/lib/root-constructs/nextjs-global-containers.js +20 -71
- package/lib/root-constructs/nextjs-global-functions.d.ts +26 -28
- package/lib/root-constructs/nextjs-global-functions.js +52 -70
- package/lib/root-constructs/nextjs-regional-containers.d.ts +18 -5
- package/lib/root-constructs/nextjs-regional-containers.js +8 -15
- package/lib/root-constructs/nextjs-regional-functions.d.ts +22 -18
- package/lib/root-constructs/nextjs-regional-functions.js +8 -26
- package/lib/runtime/api-gateway-path.d.ts +34 -0
- package/lib/runtime/api-gateway-path.js +71 -0
- package/lib/runtime/api-gateway-path.test.d.ts +1 -0
- package/lib/runtime/api-gateway-path.test.js +65 -0
- package/lib/runtime/case-redirect.d.ts +22 -0
- package/lib/runtime/case-redirect.js +63 -0
- package/lib/runtime/case-redirect.test.d.ts +1 -0
- package/lib/runtime/case-redirect.test.js +32 -0
- package/lib/runtime/core.d.ts +234 -0
- package/lib/runtime/core.js +1213 -0
- package/lib/runtime/core.test.d.ts +1 -0
- package/lib/runtime/core.test.js +1678 -0
- package/lib/runtime/deployment-root.d.ts +13 -0
- package/lib/runtime/deployment-root.js +29 -0
- package/lib/runtime/dispatch.d.ts +331 -0
- package/lib/runtime/dispatch.js +751 -0
- package/lib/runtime/dispatch.test.d.ts +1 -0
- package/lib/runtime/dispatch.test.js +853 -0
- package/lib/runtime/entrypoints.d.ts +57 -0
- package/lib/runtime/entrypoints.js +115 -0
- package/lib/runtime/http/abort-signal.d.ts +6 -0
- package/lib/runtime/http/abort-signal.js +19 -0
- package/lib/runtime/http/abort-signal.test.d.ts +1 -0
- package/lib/runtime/http/abort-signal.test.js +49 -0
- package/lib/runtime/http/node-server.d.ts +38 -0
- package/lib/runtime/http/node-server.js +99 -0
- package/lib/runtime/http/node-server.test.d.ts +1 -0
- package/lib/runtime/http/node-server.test.js +96 -0
- package/lib/runtime/http/node-sink.d.ts +16 -0
- package/lib/runtime/http/node-sink.js +48 -0
- package/lib/runtime/http/node-sink.test.d.ts +1 -0
- package/lib/runtime/http/node-sink.test.js +66 -0
- package/lib/runtime/http/request.d.ts +62 -0
- package/lib/runtime/http/request.js +134 -0
- package/lib/runtime/http/request.test.d.ts +1 -0
- package/lib/runtime/http/request.test.js +103 -0
- package/lib/runtime/http/response.d.ts +77 -0
- package/lib/runtime/http/response.js +229 -0
- package/lib/runtime/http/response.test.d.ts +1 -0
- package/lib/runtime/http/response.test.js +178 -0
- package/lib/runtime/http/sink.d.ts +73 -0
- package/lib/runtime/http/sink.js +188 -0
- package/lib/runtime/http/sink.test.d.ts +1 -0
- package/lib/runtime/http/sink.test.js +257 -0
- package/lib/runtime/image-utils.d.ts +78 -0
- package/lib/runtime/image-utils.js +129 -0
- package/lib/runtime/image-utils.test.d.ts +1 -0
- package/lib/runtime/image-utils.test.js +237 -0
- package/lib/runtime/image.d.ts +76 -0
- package/lib/runtime/image.js +296 -0
- package/lib/runtime/image.test.d.ts +1 -0
- package/lib/runtime/image.test.js +405 -0
- package/lib/runtime/lambda-event.d.ts +4 -0
- package/lib/runtime/lambda-event.js +113 -0
- package/lib/runtime/lambda-event.test.d.ts +1 -0
- package/lib/runtime/lambda-event.test.js +119 -0
- package/lib/runtime/lambda.mjs +41595 -0
- package/lib/runtime/load-module.d.ts +8 -0
- package/lib/runtime/load-module.js +35 -0
- package/lib/runtime/manifest.d.ts +215 -0
- package/lib/runtime/manifest.js +118 -0
- package/lib/runtime/middleware.d.ts +43 -0
- package/lib/runtime/middleware.js +113 -0
- package/lib/runtime/middleware.test.d.ts +1 -0
- package/lib/runtime/middleware.test.js +259 -0
- package/lib/runtime/next-modules.d.ts +55 -0
- package/lib/runtime/next-modules.js +124 -0
- package/lib/runtime/next-modules.test.d.ts +1 -0
- package/lib/runtime/next-modules.test.js +34 -0
- package/lib/runtime/public-files.d.ts +32 -0
- package/lib/runtime/public-files.js +117 -0
- package/lib/runtime/public-files.test.d.ts +1 -0
- package/lib/runtime/public-files.test.js +97 -0
- package/lib/runtime/server.mjs +41588 -0
- package/lib/runtime/static-files.d.ts +50 -0
- package/lib/runtime/static-files.js +266 -0
- package/lib/runtime/static-files.test.d.ts +1 -0
- package/lib/runtime/static-files.test.js +118 -0
- package/lib/runtime/util.d.ts +28 -0
- package/lib/runtime/util.js +79 -0
- package/lib/utils/base-path.d.ts +106 -13
- package/lib/utils/base-path.js +254 -41
- package/lib/utils/base-path.test.js +219 -51
- package/lib/utils/get-architecture.d.ts +7 -8
- package/lib/utils/get-architecture.js +12 -13
- package/lib/utils/zip-directory.d.ts +14 -0
- package/lib/utils/zip-directory.js +121 -0
- package/lib/utils/zip-directory.test.d.ts +1 -0
- package/lib/utils/zip-directory.test.js +99 -0
- package/package.json +22 -3
- package/scripts/capture-adapter-fixture.mjs +325 -0
- package/scripts/e2e-deploy.sh +398 -0
- package/scripts/e2e-harness/README.md +707 -0
- package/scripts/e2e-harness/app.js +370 -0
- package/scripts/e2e-harness/common.sh +249 -0
- package/scripts/e2e-harness/screen.mjs +302 -0
- package/scripts/e2e-harness/stage-proxy.mjs +145 -0
- package/scripts/e2e-harness/type-filters.mjs +47 -0
- package/scripts/e2e-logs.sh +75 -0
- package/scripts/e2e-offline.sh +91 -0
- package/scripts/e2e-sweep.sh +370 -0
- package/scripts/e2e-warm.sh +114 -0
- package/scripts/zero-config-build.mjs +418 -0
- package/docs/1k-concurrent-users-executions.png +0 -0
- package/docs/plans/adapter-runtime-release.md +0 -362
- package/docs/plans/phase-0-image-function-opt-in.md +0 -295
- package/lib/generated-structs/OptionalDockerImageFunctionProps.d.ts +0 -425
- package/lib/generated-structs/OptionalDockerImageFunctionProps.js +0 -3
- package/lib/image-optimization/handler-utils.d.ts +0 -34
- package/lib/image-optimization/handler-utils.js +0 -83
- package/lib/image-optimization/handler-utils.test.js +0 -178
- package/lib/nextjs-build/functions.Dockerfile +0 -31
- package/lib/nextjs-compute/nextjs-image-function.d.ts +0 -53
- package/lib/nextjs-compute/nextjs-image-function.js +0 -59
- package/lib/utils/experimental-flags.d.ts +0 -24
- package/lib/utils/experimental-flags.js +0 -30
- /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.
|