feat!: serve requests through the official Next.js adapter runtime - #271
Open
bestickley wants to merge 173 commits into
Open
bestickley wants to merge 173 commits into
bestickley wants to merge 173 commits into
Conversation
Splits config.supportsImmutableAssets out of the adapter runtime plan into its own branch and PR. No dependency on entrypoint invocation in either direction; the only overlap is a one-line conflict in modifyConfig.
Rewrites the plan as an implementation script rather than a design argument: adds a files-touched map, a commit order, and a concrete artifact contract (staging layout plus AdapterManifest types) so the build, dispatch, and runtime steps agree on filenames and path conventions. Resolves the two open questions in place — delete the dedicated image Lambda and its experimental flag, lazy-require entrypoints per dispatch — renumbers the phases into build order, and moves the rationale that must not be relitigated into a single Decisions section. Drops the duplicated packaging, standalone-fallout, and minimalMode passages.
Adds an EnterWorktree hook that runs pnpm install, so a fresh worktree is buildable without a manual step.
Next.js 16.3.5 no longer emits the root-params.d.ts reference. This file is generated by next build, so the checked-in copy was drifting dirty on every local build.
Step 1 of docs/plans/adapter-runtime-release.md. `onBuildComplete` now writes `.next/cdk-nextjs-adapter/`: a `manifest.json` holding `ctx.routing` verbatim, the route-template -> entrypoint map, config, pathnames and middleware, plus an `app/` tree staging the deduped union of every Node output's traced `assets` keyed by repo-root-relative path. That tree is the replacement for `output: "standalone"`, which is still produced -- nothing reads the manifest yet. Also: throw on any non-nodejs output rather than only on edge middleware, use `assetsHashes` to detect two outputs mapping one key to different content, and warn on the route config cdk-nextjs does not honor (`maxDuration`, `preferredRegion`). Symlinks in `assets` are recreated as symlinks, matching Next's own `copyTracedFiles`: under pnpm they are frequently directory symlinks into the store, which `copyFile` cannot handle and which triple the tree if dereferenced. `examples/pages-i18n` is a new minimal Pages Router app whose only purpose is to produce the `outputs.pages`/`pagesApi`/`config.i18n` shapes for the test fixtures; Next.js rejects `i18n` when an `app/` directory is present, so the App Router playground cannot cover them.
Adds `src/runtime/dispatch.ts`: `Dispatcher` calls `@next/routing`'s
`resolveRoutes` and returns a `DispatchResult` discriminated union — entrypoint,
static file, image optimization, redirect, external rewrite, middleware
responded, direct response, not found. It is pure decision-making, so it is unit
tested against the manifests step 1 builds from real `onBuildComplete` captures.
Notable findings this encodes:
- `resolvedPathname` is both basePath- and locale-prefixed, matching
`manifest.entrypoints` keys exactly. No normalization anywhere.
- `resolveRoutes` drops `MiddlewareResult.requestHeaders`, so dispatch captures
them from its own `invokeMiddleware` wrapper. Otherwise
`NextResponse.next({ request: { headers } })` breaks silently.
- Every redirect arrives as a bare `status` plus `location` in `resolvedHeaders`,
never as `ResolveRoutesResult.redirect`. `toRedirect` normalizes both shapes.
- `/_next/image` is not an adapter output type, so it is always unresolved and is
intercepted after middleware, which is where it has to run.
`buildAdapterManifest` now also maps dynamic *prerender* templates to their
owning route's entrypoint. Without them Pages Router ISR data URLs
(`/_next/data/<buildId>/fr/blog/hello.json`) 404, because the template they match
exists only as a prerender output. Concrete prerenders are deliberately excluded:
listing `/isr/1` makes it resolve to itself instead of `/isr/[id]` and loses the
`nxtPid` param.
The fixture capture script no longer trims dynamic prerender templates away, and
gains `--reuse-capture` to re-trim without a rebuild. All three fixtures
regenerated.
Adds MiddlewareRunner: the invokeMiddleware callback resolveRoutes calls, and nothing more. It builds a Request, invokes the built middleware handler, and hands the Response to @next/routing's responseToMiddlewareResult for the x-middleware-* translation. No matcher evaluator (resolveRoutes gates from routing.middlewareMatchers) and no hand-written header protocol. Loading awaits the require() result: Turbopack emits middleware as an async module whose module.exports is a Promise, so a synchronous require(...).handler is undefined. Both shapes are covered and tested. DispatchRequest gains a required method and MiddlewareInvoker takes it, because MiddlewareContext carries none and middleware needs one.
Adds the request path: a synthesized `IncomingMessage`/`ServerResponse` pair, a
`ResponseSink` that owns gzip, and `NextjsRuntime.handle(request, sink)` that
dispatches through `@next/routing` and invokes built entrypoints as
`handler(req, res, { waitUntil, requestMeta })`.
Two shells wrap that one core and do nothing but translate: `lambda.mts`
(`awslambda.streamifyResponse`, both the Function URL and API Gateway REST event
shapes) and `server.mts` (a `node:http` server). Containers gets the *same*
synthesized request objects as Lambda, so its e2e suite exercises the code Lambda
runs.
Also pins `.mts` under type-checking for the first time: the jsii `include` is
`src/**/*.ts`, so both shells would otherwise be unchecked. `pnpm compile` now
also runs `tsc -p tsconfig.esm.json`.
Nothing is wired into a construct yet — step 5.
`next` is external to both shell bundles, so esbuild turned every
`import ... from "next/dist/..."` in `static-files.ts` and `image.ts` into a
hoisted static import inside `cdk-nextjs-runtime/{lambda,server}.mjs`. Node
resolves those by walking `cdk-nextjs-runtime/node_modules` and then
`<deploymentRoot>/node_modules`, and under pnpm neither holds `next`: the only
`next` in the staged tree is the relative symlink at
`<projectDir>/node_modules/next`, one directory below the deployment root.
Because the imports are hoisted, this was not a degraded image route — the whole
shell would have failed to load.
`src/runtime/next-modules.ts` requires those modules through a `createRequire`
anchored inside the staged project dir, which is the same walk the traced
entrypoints do, for any package manager and any layout. `loadRuntime` points it
at the project dir right after it chdirs there. `handler-utils.ts` takes its two
`next` values as parameters so the dedicated image Lambda, which bundles `next`
in, keeps working from the same shared code.
`core.test.ts` now links this repo's `node_modules` into the staged project dir,
so `serveStatic` in the test resolves by the deployed walk rather than a
different one. Verified: no `import "next/..."` remains in either bundle.
`sendNotFound` pointed `req.url` at the not-found entrypoint's own pathname before invoking it. The App Router serializes the canonical URL into the RSC payload, so the client hydrates off a payload claiming it is at `/_not-found`: `usePathname()` returns the wrong value and the history entry is wrong. Render the module against the URL that was asked for instead, which is what `next start` does. `render404()` called from inside a route keeps `req.url` as it is — it is already the path that gave up, which is the path Next.js renders that 404 for.
`@next/routing` echoes the rewrite it followed back in `resolvedHeaders` as `x-middleware-rewrite`; Next's own router treats that as an internal signal and strips it. Forwarding it leaked the app's post-middleware paths to clients. `resolveRoutes` routes a middleware rewrite internally but never reports the rewritten URL, and `/_next/image` is not an adapter output type so it can never appear in `manifest.pathnames`. Capture the rewrite from that header and match `/_next/image` against it: the API Gateway examples rewrite `/_next/image` to `/<stage>/_next/image` to line `basePath` up, and comparing the URL as received 404s every optimized image behind such a rewrite.
All four deployment types now serve requests out of the deployment root the adapter stages during `next build`, with cdk-nextjs's own shell as the entrypoint. `output: "standalone"` is still set — it goes away in the next step — but nothing reads `.next/standalone` anymore. `NextjsBuild` reads and validates the adapter manifest (a missing one is now an error that names its own cause: `next.config` has no adapter registered), exposes `deploymentRootPath` and `relativeProjectDir`, and stages the one shell the deployment type runs into `cdk-nextjs-runtime/` beside a copy of the manifest. `sharp` handling moves with it: the staged tree is keyed by repo-root-relative path, so both the darwin-binary strip and the target install search by directory name rather than assuming one top-level `node_modules`, and the target is glibc for Functions, musl for Containers. `NextjsFunctions` drops `DockerImageFunction` for a zip `Function`. No Docker image, no Lambda Web Adapter; streaming is `awslambda.streamifyResponse` in the shell. `overrides.dockerImageFunctionProps` and `overrides.assetImageCodeProps` give way to `overrides.functionProps`, and `functions.Dockerfile` is deleted. The regional example no longer needs `ResponseTransferMode.BUFFERED`: API Gateway does not compress a streamed response, so the runtime gzips it itself. `NextjsContainers` and both container Dockerfiles `COPY` the deployment root and run `node cdk-nextjs-runtime/server.mjs`. A generated Dockerfile is now cdk-nextjs's to replace: every existing consumer has one that runs `node server.js`, which this version no longer produces, so keeping it makes the container crash-loop on MODULE_NOT_FOUND with nothing pointing at the cause. The `# ~~ Generated by cdk-nextjs ~~` first line marks ours, and says that deleting it takes ownership. Two adapter-side fixes this needed: - `hoistStoreOnlyPackages` picks the version with staged *code*, not the first by staging key. The trace stages `semver@6.3.1/package.json` for its metadata and the whole of `semver@7.8.5`; `6.3.1` sorted first, so hoisted `node_modules/semver` held one `package.json` and no modules, `sharp` failed to load, and every external `/_next/image` request 200ed with the unoptimized original — under next's misleading "Module `sharp` not found". - `addRuntimeNextClosure` traces what the image optimizer reaches. `next build` only traces what the app reaches, and no app reaches `next/dist/server/image-optimizer.js`, so `/_next/image` 500ed. Uses next's own vendored `@vercel/nft` rather than a new dependency or bundling, which would break the "always use the app's own next" invariant. Verified on AWS across all four deployment types against the reference stacks: byte-identical responses for App Router pages, ISR, SSR, SSG, route handlers, streaming, static files off disk, image optimization on both glibc and musl, and middleware rewrites. Packaging is 49 MB unzipped / 21 MB zipped for one function serving every route, 20% of the Lambda cap. Details in docs/plans/adapter-runtime-progress.md.
`removeExistingSharpBinaries` strips the traced host-platform `sharp` binaries
out of the staged tree before installing the target's. It matched on `sharp-`
and removed everything it found with `rmSync(path, { recursive: true, force:
true })`, in one pass, in `readdirSync` order.
pnpm's store directory names match that same substring
(`@img+sharp-darwin-arm64@0.35.4`) and several links point at one store
directory, so the walk regularly reached a store directory before the links
into it. That leaves dangling symlinks — and `rmSync` with `force` *silently
no-ops* on one, because `force` swallows the ENOENT its internal `rmdir` gets.
The second pass reported success and the link stayed. Four dangling
`@img/sharp-darwin-arm64` entries shipped in the deployment asset, which is a
latent ENOENT in anything that dereferences the tree — `cdk-assets` does, when
it zips.
Collect symlinks and directories separately and `unlinkSync` every symlink
first.
`onBuildComplete` stages the deployment root from the same NFT traces
`writeStandaloneDirectory` would have used, so standalone and the adapter are
alternatives, not layers. `next build` says as much itself, immediately above
the `onBuildComplete` call: "in the future `output: standalone` might not be
allowed if an adapter with `onBuildComplete` is configured". Since step 5
nothing read `.next/standalone`; this stops asking for it, and deletes the code
that only existed to serve that layout:
- `validateNextBuildOutput`, `findRelativePathToServerJs` and
`relativePathToPackage`, all keyed to `.next/standalone/server.js`.
- The dedicated image optimization Lambda — handler, construct, experimental
flag, `OptionalDockerImageFunctionProps`, the `imageFunctionUrl` /
`imageFunction` props and the CloudFront origin and API Gateway integration
built from them, plus `prepareImageOptimizationAssets` and its 19 MB second
asset. `/_next/image` is served by the runtime core, which is not a size
optimization: dispatch classifies a request as image optimization only after
middleware has had it, so `NextResponse.rewrite()` onto an image works now and
could not have worked against a separate function behind its own origin.
`_next/image*` keeps its own CloudFront behavior — its cache policy wants
`queryStringBehavior: all()` and `accept` in the key, which is wrong for
everything else — but points at the same origin as the rest. On API Gateway it
has no resource at all and falls through to `{proxy+}`.
`src/image-optimization/handler-utils.{ts,test.ts}` move to
`src/runtime/image-utils.{ts,test.ts}`; the directory is gone.
Verified on all four deployment types against the oracle stacks with
`.next/standalone` confirmed absent: byte-identical responses everywhere except
the oracle's own stale-canonical-URL 404 bug.
Adds an opt-in `functionGroups` prop to the two Functions root constructs. Its only purpose is staying under Lambda's 250 MB unzipped cap, which is now measured per group at synth rather than surfacing from CloudFormation minutes into a deploy. The split has to be decided twice in two processes — `onBuildComplete` stages one tree per group while `next build` is running, synth turns the same patterns into CloudFront behaviors or API Gateway resources — and a disagreement means CloudFront routing a request to a function whose zip lacks the entrypoint. So the rules live in one pure module both sides import, the resolved groups travel via `CDK_NEXTJS_FUNCTION_GROUPS`, and synth reads the assignment back off the manifest instead of recomputing it. Patterns are an exact path or a subtree and nothing else: CloudFront supports only `*`/`?`, so a dynamic segment could only deploy as a wildcard that also captures its siblings. Default behavior is unchanged — no prop, one function, one deployment root.
`fetchFromS3` used the app's `basePath` both to strip the href and to build the S3 key. Those are different things: `basePath` prefixes the *URL* the app is served at, while the key prefix is where `NextjsStaticAssets` uploaded the bytes. On the API Gateway deployment types the first is the stage name (`/prod`) and the second is empty, so every local `<Image>` asked S3 for `prod/_next/static/...`, got `NoSuchKey`, and 400'd with "The requested resource isn't a valid image." `NextjsStaticAssets.keyPrefix` is now a field with one definition, threaded through the compute props to `CDK_NEXTJS_STATIC_ASSETS_KEY_PREFIX` and read by the runtime image optimizer. `fetchFromS3` takes both values separately.
The S3 static integration neither forwarded `If-None-Match` / `If-Modified-Since` nor mapped a 304 response, so a client revalidating an immutable `_next/static` chunk re-downloaded it with a 200 every time. Both headers are now declared optional on the method and mapped into the integration request, and a `304` integration/method response pair passes back `Cache-Control`, `ETag` and `Last-Modified` with no body.
`ctx.outputs.prerenders[].pathname` carries the app's `basePath`, so with `basePath: "/prod"` the build seeded `<buildId>/prod/ssg/1.json` while the server - which sees the path only after Next.js has stripped `basePath` - asked for `<buildId>/ssg/1.json`. Every build-time prerender was a MISS that re-rendered on the first request and only became static from the second visit, which is what `ssg.test.ts` was catching on the API Gateway deployment types. `prerenderPathToCacheKey` strips `basePath` on a path boundary and maps the resulting root to `index`.
Step 8 of docs/plans/adapter-runtime-release.md. - `function-groups.test.ts` proves a grouped and an ungrouped route are served by different Lambdas, via a `/runtime-identity` page and API route that report the function name. `examples/global-functions` now declares a `/api/**` group permanently, so the split is exercised locally and in CI. - `middleware.test.ts` covers the plan's "middleware e2e proving interception on `_next/image`": `proxy.ts` 403s one committed fixture image and the test asserts the 403, which is only reachable if middleware runs for image requests. - `headers.test.ts` covers ETag shape, compression on and off, and a conditional GET. The compression pair is what replaces the LWA verification, since API Gateway does not compress a streamed response. - `isr` and `revalidation` now settle on a timestamp instead of trusting the first load after an invalidation, which could legitimately serve the expired entry while kicking off the background re-render. Docs: README gains a `functionGroups` section and corrects the Docker prerequisite to Containers-only; `breaking-changes.md` covers the adapter runtime, zip Lambdas and the removed image function; `next-build-output-guide.md` described standalone output and now describes the staged deployment root. All four deployment types are green: glbl-fns 32 passed, the other three 29 passed / 3 skipped (the split tests). `rgnl-fns` is red on `main` for causes the three preceding `fix:` commits repair.
Migrate `app-playground` to `cacheComponents: true` - what Next.js 16.3 folded
`experimental.ppr` into - and add `ppr.test.ts`, which asserts through a real
deployment that the prerendered shell goes out first and the request-dependent
half is resumed onto it in the same response.
Every prerender failure the flag surfaced fell into one of the three buckets
Next.js names in its own error message:
- `[cache]`: `'use cache'` on the data functions in `getCategories.ts` and
`getReviews.ts`, with `notFound()` and the throw to `error.js` left outside so
one upstream blip cannot be cached as a 404.
- `[stream]`: a `<Suspense>` boundary as deep as possible around anything that
reads the request - the client URL hooks, which suspend under
`cacheComponents`, and `params` in a dynamic route with no
`generateStaticParams`, which `'use cache'` cannot legalize. Two new shared
components, `ui/category-tab-group.tsx` and `ui/category-content.tsx`, carry
that for seven layouts and fourteen pages.
- `[block]`: `export const instant = false`, only in the streaming demo, whose
point is reading the cart cookie before there is anything to prerender.
`/runtime-identity` awaits `connection()` in place of `dynamic =
'force-dynamic'`, which `cacheComponents` rejects; without it the response is
prerendered and every function reports the same empty identity.
`/isr/[id]` keeps ISR by overriding only `cacheLife({ revalidate: 10 })`. A
`stale` shorter than the shell's own stale time cannot be baked into the shell,
so the build postponed the route and served it `private, no-store` with no
`x-nextjs-cache` - CDN caching and the `isr` suite's assertions both gone.
No runtime change was needed: with `minimalMode` unset, the per-route entrypoint
resumes the postponed render in-process and pipes it onto the shell, exactly as
`next start` does.
A dynamic app-page render in an app with no middleware crashed the Lambda with "Invariant: AsyncLocalStorage accessed in runtime where it is not available". `next/dist/server/app-render/async-local-storage.js` reads `globalThis.AsyncLocalStorage` at module scope and keeps whatever it saw, so if the global is not set yet every app-render storage in the process becomes Next's `FakeAsyncLocalStorage`, whose methods throw. Inside the precompiled `app-page-turbo.runtime.prod.js` the load order is against us: `work-async-storage.external.js` is required before `route-module.js` gets to the bootstrap that sets the global. Apps with middleware were accidentally fine, because `next/dist/build/templates/middleware.js` requires the same bootstrap and the runtime runs middleware before it loads a page entrypoint. Every example app has middleware, which is why no e2e caught this. `loadRuntime` now requires `next/dist/build/adapter/setup-node-env.external.js` right after pointing `next` resolution at the staged app. That is the bootstrap Next.js publishes for adapters; besides the globals it installs the `react`/`react-dom` require hook and the crypto polyfill, which were also reaching us only via middleware.
Adds the plumbing for running vercel/next.js's own e2e suite against a real cdk-nextjs deployment, plus an explicit three-file slice to run it on: app-static, app-action and middleware-rewrites. - scripts/e2e-deploy.sh, e2e-logs.sh, e2e-cleanup.sh implement the three NEXT_TEST_*_SCRIPT_PATH hooks; e2e-sweep.sh cleans up orphans. - scripts/e2e-harness/app.js deploys NextjsRegionalFunctions and reports a Lambda Function URL. The harness's getFullUrl discards any prefix in the deployment URL, so API Gateway's mandatory stage prefix cannot be used: every absolute path would miss /prod and 404. Same Lambda, same adapter output, different front door. - scripts/e2e-harness/stage-static.js copies _next/static and public/ into the deployment package, which the product deliberately leaves out because CloudFront and API Gateway route those prefixes to S3. A bare Function URL has no S3 integration in front of it. - test/deploy-tests-manifest.json is a v2 filter manifest; its per-file excluded cases are copied from next.js's own manifest at v16.3.5, i.e. cases that fail on Vercel too. - .github/workflows/e2e-harness.yml runs it nightly and on demand, then sweeps. Every harness stack carries a cdk-nextjs:harness tag and an hrns- prefix, and both delete paths refuse a stack without the tag. The sweeper additionally requires the stack to be older than HARNESS_SWEEP_MAX_AGE_HOURS (default 6) so it cannot delete a running test's stack. The official test files have not been run: that needs a built vercel/next.js checkout, and installing one was refused in the authoring environment. The scripts are proven end to end against real AWS with a hand-written fixture shaped like a harness temp app. Details in docs/plans/adapter-runtime-progress.md.
The compatibility harness previously stood up a NextjsRegionalFunctions stack per test file and exposed it with a FunctionUrlAuthType.NONE Function URL. That URL was world accessible, which Palisade flagged and Epoxy auto-mitigated. Nothing in the published library was affected -- the Function URL was created by harness code only. Deploy NextjsGlobalFunctions instead: CloudFront in front of the product's own AWS_IAM Function URL with OAC, so there is no unauthenticated endpoint and `_next/static` / `public/` are served from the NextjsStaticAssets bucket exactly as in production. That also lets stage-static.js go away. All test files now share one stack (`hrns-shared`) deployed with `--hotswap-fallback`, so only the first file of a run pays ~12 minutes for a distribution. Measured against two built fixtures, five resources differ and two of them are not hotswappable (the cache bucket's `aws-cdk:cr-owned` tag, and the distribution's per-`public/` cache behaviors), so most files take the CloudFormation fallback -- still a couple of minutes rather than ~13. See scripts/e2e-harness/README.md. Sharing the stack requires: `-c 1` (concurrent deploys into one stack would race), e2e-cleanup.sh keeping the stack for the next file with e2e-sweep.sh deleting it after the run, and e2e-deploy.sh invalidating the distribution itself -- a hotswap never runs CloudFormation, so the post-deploy custom resource never fires. Its properties are pinned in app.js for that same reason.
`EnterWorktree` is not a hook event, so Claude Code ignored the whole block and the `pnpm install` never ran on worktree creation. The valid event is `WorktreeCreate`. Also drops `matcher`, which only filters tool-scoped events (PreToolUse/PostToolUse) by tool name and matches nothing here.
`assertNodeRuntimes` folded middleware in with the routes, and middleware's `sourcePage` is `/`. An app whose only edge artifact was a legacy `middleware.ts` was therefore told to remove `export const runtime = "edge"` from `/` -- a page that does not set it and need not exist. Report the two separately, naming middleware by its file path and pointing at Next.js 16's Node-runtime `proxy.ts`. Found by running vercel/next.js's own e2e suite through the compatibility harness: three of its fixtures use legacy edge middleware.
First step in which vercel/next.js's own e2e suite actually ran against a cdk-nextjs deployment, which is step 8's exit criterion 2. Both manifest files pass on attempt 0, exit code 0. Three things that only showed up by running it: 1. All three files in the previous manifest were unbuildable, not merely failing. `assertNodeRuntimes` throws during `next build` on any output whose runtime is not `nodejs`, and two fixtures ship a legacy edge `middleware.js` while app-static has ~10 `*-edge` routes. Edge is a deliberate non-goal -- deprecated upstream, and cdk-nextjs supports Next 16's Node-runtime `proxy.ts` -- so they are excluded with reasons in `excluded-notes`. ~522 e2e files are edge-free. Replacements were chosen by running them, which the manifest now states as a rule. 2. The first test file of every run failed and only passed on retry: a cold stack create is ~240s, `createNext` runs in jest's `beforeAll`, and NEXT_E2E_TEST_TIMEOUT is 240000. `scripts/e2e-warm.sh` creates the stack up front through `e2e-deploy.sh` itself, so a successful warm-up proves the path the test files take. Verified cold. 3. `deployment-skew` fails for real: an `RSC: 1` request gets `text/html` rather than `text/x-component`. Not the header-quota caveat -- `rsc` is allowlisted. Excluded pending its own fix, recorded as a bug rather than an inapplicable test. Also corrects every '~12 minutes' distribution-create claim to the ~240s actually measured.
Final entry in the progress log: every exit criterion from docs/plans/adapter-runtime-release.md with its state, the one documented deviation (the harness runs on NextjsGlobalFunctions, not Regional, because next.js's getFullUrl drops the API Gateway stage prefix), and the five items carried past the PR.
cdklabs-automation
enabled auto-merge
September 22, 2026 12:13
…plan # Conflicts: # README.md # src/image-optimization/handler.mts # src/nextjs-build/nextjs-build.ts # src/nextjs-compute/nextjs-image-function.ts # src/nextjs-static-assets.ts # src/root-constructs/nextjs-base-construct.ts # src/root-constructs/nextjs-global-functions.ts # src/root-constructs/nextjs-regional-functions.ts # src/runtime/image-utils.test.ts # src/runtime/image-utils.ts
Contributor
Dependency ReviewThe following issues were found:
License Issues.github/workflows/zero-config-build.yml
OpenSSF ScorecardScorecard details
Scanned Files
|
A top-level `public/` entry is matched by name rather than by an existing wildcard, so on the Global types it becomes its own CloudFront behaviour - whose path pattern `toPathPattern` has to write with `?` for the space - and on `NextjsRegionalFunctions` it is skipped by design, because an API Gateway resource path cannot express it. The spec asserts both: 200 on three types, 404 on API Gateway. Its own commit because the first push into an existing Global stack adds a CloudFront behaviour, which should show up in CI timings on its own.
…xpansion docs/adapter-runtime-plan took defect numbers 34 and 35 while this branch was open, so the redirect-drops-the-API-Gateway-stage defect is renumbered to 36, in docs/harness-coverage.md and in the url-normalization gates. It now sits under a "Bug — not yet fixed" section after 35, since the target moved the path-traversal entry that section used to hold to Upstream.
… when basePath carries the stage
…the prefix API Gateway strips
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…nd match it by wildcard
…tead of failing on it
…loudFront's cache
…e-Control The dynamic cache policy inherited CDK's one-day default TTL, so a route handler without Cache-Control was cached at the edge for 24 hours.
Listing public/ in the manifest at onBuildComplete missed postbuild output, keyed names only in their encodeURIComponent spelling, never matched in an i18n app, dropped symlinks, lost to app routes at the same path, and bloated every Lambda manifest. The container runtime now lists public/ itself and the Dispatcher builds its routing table once per cold start. Also: call send with an encoded path under a root (files with % in the name), pass generateEtags through, answer 405 to non-GET/HEAD for public/ and _next/static, match _next/static chunks requested percent-encoded, and render a missing static file's 404 with middleware's request headers.
…: 0 deployable A top-level public/ name CloudFront cannot spell became a pattern of wildcards alone that captured app routes of the same length; it now gets no behavior and a synth warning. The 255-character limit is checked on the final pattern, and a dynamic cache policy with maxTtl 0 drops its cache key, which CloudFront requires.
…uard the zero default TTL
…r header it was cached with Since dynamic responses stopped being cached by default, /api/revalidate reaches the app, so the baseline is the blocking REVALIDATED render. CloudFront caches it for s-maxage=10 and serves it stale under stale-while-revalidate, and the 11-second reload saw that cached header. An edge hit is not a re-render, which is what the assertion exists to catch.
…ve text /[0-2]s ago/ also matched "12s ago" and "20s ago", and the text ticks client-side. The render timestamp in the title attribute is what the cache actually keeps.
The working logs were stale on arrival. adapter-runtime.md keeps the map: build and request paths, invariants, knowing divergences from next start, and per-type limitations. harness-coverage.md shrinks to verdicts, a defect index and debugging technique. The README no longer claims revalidation leaves CloudFront stale on the Global types; it invalidates for you.
…drain on shutdown - treat each Set-Cookie entry as one cookie instead of comma-splitting - drop the failed render's Content-Length/ETag before every error fallback - send no-store on a 404 for a manifest chunk missing from disk - wait for in-flight handle()/waitUntil work on SIGTERM - respect backpressure on proxied web responses; stop reading on disconnect - forward x-forwarded-host/proto on in-process revalidation - check segment boundaries when stripping basePath
…m the rendering output - stage /_not-found, /404, /500 and /_error in every function group - map /index to / only for Pages Router outputs - key group ownership on the loaded file so Pages data routes follow their page - take each prerender's cache kind from its parent output (catch-alls, Pages home) - reject /_next group patterns and non-string routes - read staged file links with bounded concurrency
…d group routes - only drop the cache key when every resolved TTL is 0 - route method-less group parent resources to the default function - reject group routes API Gateway path parts cannot hold, naming the group - throw when a basePath prop repeats the stage; warn on non-root domain mappings - back up a customized generated Dockerfile before replacing it - remove only @img/sharp-* binaries; find staged sharp in the cleanup walk - prune only <prefix>/_next/ so apps sharing a bucket are left alone - replace Partial<CachePolicyProps> with an explicit interface
…idation - validate memory hits against DynamoDB markers so other instances' revalidations apply - send a single CloudFront invalidation per revalidateTag, collapsing to /* past quota - invalidate the whole app when the tag-mapping query is truncated - honor revalidateTag durations (stale-while-revalidate via Next's tag manifest) - ignore mapping rows from tags that only share a # prefix - stop deleting revalidated fetch entries on get - skip mapping rows without a distribution; batch marker reads - round init-cache ephemeral storage so handler identity is stable; keep staging dir when staging is disabled
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
docs/plans/adapter-runtime-release.md: cdk-nextjs stops deployingNext.js's
output: "standalone"server and instead serves requests through theofficial Next.js adapter API —
onBuildCompletebuild outputs,@next/routingfor dispatch, and one runtime core behind two shells (Lambda and container).
docs/plans/adapter-runtime-progress.mdis the per-step record: what landed, whatwas decided, what was measured, and — in its "Final — exit criteria roll-up"
section — every plan exit criterion with its state.
Why
The standalone server was a black box we shipped and then worked around. The
adapter API gives us the build outputs directly, so routing, middleware, image
optimization and caching are things this library resolves rather than things it
proxies to a server it cannot see into. Concretely that bought:
2061 ms → 342 ms; end-to-end cold path ~2.5 s → ~1.4 s (measured against the
main-rgnl-fnsoracle, read-only from CloudWatch Logs).optimization is served by the same runtime core.
functionGroups— route-group splitting, as the documented answer to a250 MB Lambda size error.
rgnl-fnsisred on
mainfor two of them (run 35591295859), so this branch repairs astanding failure rather than introducing one.
Breaking changes
See
docs/breaking-changes.md. Summary:CDK_NEXTJS_EXPERIMENTAL_DEDICATED_IMAGE_FUNCTIONremoved, along with the imageconstruct it gated.
nextjsFunctionsProps.dockerImageFunctionProps→functionProps. The Dockerimage override keys are meaningless once Functions are zip Lambdas.
Verification
Unit:
pnpm compile0 errors,pnpm eslintclean, 17 suites / 268 tests.e2e (
examples/e2e-tests) against four deployed stacks on this branch:glbl-fns(with theapisplit)glbl-cntnrsrgnl-cntnrsrgnl-fnsThe skips are the
function-groupstests, which needE2E_FUNCTION_GROUPS.New coverage the plan called for: middleware intercepting
_next/image, PPR undercacheComponents(which also settled that no manual resume code is needed),response compression on both Functions types, conditional GETs, and a split
configuration proving a grouped and an ungrouped route come from different
Lambdas.
Next.js's own e2e suite
scripts/e2e-warm.sh+scripts/e2e-deploy.sh/e2e-logs.sh/e2e-cleanup.sh/e2e-sweep.shimplement the adapter testingharness contract, so vercel/next.js's e2e tests can run against a real
cdk-nextjs deployment. Nightly
workflow_dispatchworkflow, one shared stack,serialized.
scripts/e2e-harness/README.mdis the guide.Current state, stated plainly: two test files, both green on attempt 0, exit
code 0. That is the "plumbing + small slice" scope, not a broad slice.
Two deliberate deviations, both documented:
NextjsGlobalFunctions, notNextjsRegionalFunctionsas the plansays. next.js's
getFullUrlassignsparsedUrl.pathnameoutright, so themandatory API Gateway
/<stage>prefix is discarded and every absolute path404s. Not fixable from our side.
assertNodeRuntimesthrowsduring
next buildon any non-nodejsoutput, so such a fixture fails to buildand a per-case skip cannot rescue it. Deliberate: the edge runtime is deprecated
upstream and cdk-nextjs supports Next.js 16's Node-runtime
proxy.ts. ~522 ofnext.js's e2e files are edge-free. Every exclusion has a reason in
test/deploy-tests-manifest.json'sexcluded-notes.Harness stacks are tag-gated (
hrns-*andcdk-nextjs:harness=1, re-checkedimmediately before each delete), age-floored, and dry-run unless
--apply. Theshared stack from the final run was swept.
Known open items
One left, named rather than hidden; it wants its own change:
deployment-skew's RSC content-type bug — anRSC: 1request getstext/html; charset=utf-8instead oftext/x-component. Characterized, notyet diagnosed to edge vs. adapter. Excluded with a note saying it should come
back as a regression test.
The other two are now fixed in this branch:
healthCheckPathmoved offNextjsBasePropsonto the two Containers constructs (required where it isactually read, a type error where it did nothing), and cache-handler
invalidation now prefixes the app's
basePathonto the routes it derives, so aGlobal type with a
basePathinvalidates the URI CloudFront cached.