Repository navigation
feat!: require Node.js 22.2+ and drop the Node 20 workarounds - #318
Open
bestickley wants to merge 9 commits into
Open
bestickley wants to merge 9 commits into
bestickley wants to merge 9 commits into
Conversation
Contributor
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files
|
bestickley
marked this pull request as ready for review
October 15, 2026 20:34
cdklabs-automation
enabled auto-merge
October 15, 2026 20:34
auto-merge was automatically disabled
October 16, 2026 15:01
Merge commits are not allowed on this repository
Fixes #321. On a gzipped streamed response, a `<Suspense>` fallback arrived only with the slow boundary it stood in for. `pipeToSink` forwarded Next.js's per-chunk `res.flush()` to `compressor.flush(Z_SYNC_FLUSH)`, but under backpressure the chunk Next.js had just written was still in `ShimServerResponse`'s buffer, so the flush was queued in zlib *ahead of* it. The last chunk before a pause then sat in zlib until the next write. With ~100 KB written before a pause, the client could decode only 70-80 KB, matching the 73 KB vs ~100 KB (identity) in the issue. The gzip stage now uses zlib's per-write `flush: Z_SYNC_FLUSH`, which is ordered with the data. `ShimServerResponse.flush` and the matching call in `sendWebResponse` are removed: nothing needs them now, and without the property Next.js skips its own flush call. The new `sink.test.ts` case runs Next.js's write → flush → wait-for-drain loop against a slow sink. It fails on the old forwarding and passes here.
Follow-up to #323, from its review. #323 removed `ShimServerResponse.flush`, so app code that calls `res.flush()` now throws `TypeError: res.flush is not a function`. An example is `res.write(event); res.flush()` in a Pages API route, which the `compression` middleware supports under `next start`. The method is back as a no-op: the sink's gzip stage already sync-flushes every chunk, so there is nothing left for it to do. Next.js's own `pipe-readable` now calls it after every chunk, which is harmless. Also: - The backpressure test's comment now says how hex actually compresses (about 2:1). What makes each write meet backpressure is `res`'s own 16 KiB buffer. - In `sendWebResponse`, the drain comment is back next to the drain check. - The `res.flush()` entry in `docs/nextjs-private-apis.md` is updated. Accepted trade-off from #323, noted here for the record: every write to a gzipped response is now sync-flushed. The Next.js render path already flushed every chunk, and 64 KB static-file chunks barely change. Many tiny writes without a flush cost more: 10,000 writes of 40 bytes grow from 59.5 KB to 161 KB. Delaying each flush until the buffered bytes reach zlib would avoid that, but it needs byte accounting across Transform buffers, which risks reintroducing #321's ordering bug.
) ## Summary - **Container images:** the generated Global/Regional Containers Dockerfiles build on `node:24-alpine` but never upgrade OS packages. The current upstream image (Alpine 3.24.2) still ships `zlib-1.3.2-r0` (CVE-2026-85091), while `1.3.2-r1` is already in the Alpine repo. Both Dockerfiles now run `apk upgrade --no-cache` before `apk add`, so images pick up security fixes released after the base image was published. User Dockerfiles carrying the generated header are rewritten automatically. - **Dependabot:** adds a `handlebars: ^4.7.10` pnpm override (via `.projenrc.ts`) to resolve alerts #436, #437, #438. handlebars is transitive (ts-jest, conventional-changelog-writer); the lockfile diff touches only handlebars. ## Testing - `docker run node:24-alpine sh -c 'apk upgrade --no-cache && apk add --no-cache libc6-compat && apk info -v | grep ^zlib'` → `zlib-1.3.2-r1`, node v24.22.0. - `pnpm compile`, `pnpm projen`, `pnpm jest src/nextjs-build/nextjs-build.test.ts` (52 passed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
… hanging, and extract pure helpers out of the constructs (#319) Closes #313, and extends it to the other constructs. ## Extracted (internal, none exported from `src/index.ts`, each with unit tests) | Module | From | What | |---|---|---| | `utils/path-patterns.ts` | NextjsDistribution | `public/` path patterns (UTF-8 `?` rule, 255-char limit after basePath), `getPathPattern`, public/group collisions | | `utils/behavior-budget.ts` | NextjsDistribution | `assertBehaviorBudget`, `DEFAULT_MAX_CACHE_BEHAVIORS` | | `utils/cache-keys.ts` | NextjsDistribution | `isCachingDisabled`, dynamic/disabled cache keys | | `utils/viewer-request-functions.ts` | NextjsDistribution | `checkFunctionCodeSize`, `assertViewerRequestUnclaimed` | | `utils/api-resource-paths.ts` | NextjsApi | function-group resource paths (`{proxy+}`, `_next/data/{buildId}`), parent ownership, routability and `public/` collision checks | | `utils/cache-deployment-sizing.ts` | NextjsCache | deployment Lambda storage steps, cap and memory | | `utils/dockerfile.ts` | NextjsContainers | keep / unchanged / create / replace decision | | `utils/api-gateway-prefix.ts`, `assertStaticAssetsKeyPrefix` in `utils/base-path.ts` | NextjsBaseConstruct | API Gateway stage/domain prefix, static assets key prefix check | | `utils/remove-staging-dirs.ts`, `utils/secure-bucket-props.ts` | NextjsCache + NextjsStaticAssets | shared staging cleanup and bucket defaults | | `lambdaSizeError`, `isClientEntrypointChunk` in `nextjs-build.ts` | NextjsBuild | 250 MB error, patch-fetch chunk filter | | `splitGroupRoute`, `assertKnownFunctionGroups` in `adapter/function-groups.ts` | NextjsApi + NextjsDistribution | the route split was written out three times; the unknown-group check was in both | | `isFunctionsType`, `isGlobalType` in `constants.ts` | several | replace hand-written type checks | In the distribution, the four response headers policies (static, dynamic, image, service worker) share one builder, and `getComment(x)` reads the stack name itself. `nextjs-distribution.ts` went from 1271 to 966 lines, and `nextjs-api.ts` from 832 to 718. ## Behaviour changes - **fix:** the fetch patch was also prepended to `main-app-*.js.map` when browser source maps are on. - **fix:** sizing the init cache used `readdirSync({ recursive: true })`, which never returns on a symlink cycle under Node 24. It now walks the tree with `listTree`. - **fix:** the cache bucket now denies requests below TLS 1.2, as the static assets bucket already did. This is the only template change in normal configurations. - A CloudFront comment of exactly 128 characters now keeps the stack name (`<` became `<=`). - Containers derive their CPU architecture from `getLambdaArchitecture()`. x64 and arm64 hosts are unchanged. A 32-bit `arm` host now gets ARM64, matching what NextjsBuild stages. Other unusual hosts now set X86_64 explicitly instead of leaving it to Fargate's default, which is also X86_64. ## Verification - `pnpm compile` and `pnpm test` (eslint included) pass: 56 suites, 1302 tests, rebased on main at #324. This branch doesn't change `src/nextjs-distribution.test.ts` or `src/nextjs-api.test.ts`; the tests #320 added to them pass against the refactor. - Rebased on main: #320's `Service-Worker-Allowed` behavior is ported into the refactor. Its policy-name constants stay in `nextjs-distribution.ts`, and `assertBehaviorBudget` counts the service-worker behavior (`serviceWorker`), with a unit test. - I compared templates before and after with throwaway harnesses (`Template.fromStack(...).toJSON()` plus warnings and error messages): - NextjsDistribution, 7 configs (basePath, assetPrefix, functionGroups, unicode `public/`, viewerRequestFunctionCode, containers, long stack name): identical. - NextjsApi, 4 configs and 6 error paths: identical. - Root constructs, 9 configs across all four types: identical. - NextjsCache, 6 configs: only the TLS 1.2 deny statement is added. - NextjsStaticAssets: identical. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
Until 1.0, breaking changes should only ship in a `0.X.0` release. `feat`/`fix` release as patches, and users on `^0.7` get those automatically. - `docs/breaking-changes.md`: the `0.7.12` section gets a note that it shipped in a patch by mistake. It and the `0.5.16` sections are folded into `0.7.0` and `0.5.0`. Each moved section starts with a _Shipped in 0.X.Y._ line, so readers on an earlier patch can tell what applies to them. The text is otherwise unchanged. - `CLAUDE.md`: a new Key Conventions rule: - patches must not break anyone; warn or deprecate instead; - breaking PRs take a `!` title (`fix!:`/`feat!:`), which projen bumps to the next minor, and target the unreleased `next` branch, which is squash-merged into `main` as one `feat!:` commit when the minor ships; - `docs/breaking-changes.md` has no patch headers. Docs only. No release is triggered.
Node 20 is end-of-life. Declare engines.node >= 22.2.0 (the first 22.x with zlib.crc32), use zlib.crc32 in place of the hand-written table, and drop the Dirent.path fallback for Node 20.9-20.11. Closes #305
- R1-3: Node.js requirement no longer calls 22 an active LTS; newer LTS lines are supported
- R2-1: listFiles comment no longer ties directory-link following to Node 24 - R2-3: sizeOfDirectory docstring says it counts through directory links, as cdk-assets zips them - R2-4: bundles loaded by the user's next build target minNodeVersion (node22.2.0) - R2-5: require() of the ESM bundles noted as Node 22.12+
bestickley
force-pushed
the
feat/require-node-22
branch
from
October 20, 2026 13:22
fb36ff1 to
48c4230
Compare
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.
Closes #305.
.projenrc.ts:minNodeVersion: "22.2.0"(replacingsetNodeEngineVersion: false), sopackage.jsongetsengines.node: ">= 22.2.0". 22.2 is the first 22.x withzlib.crc32. CI workflows are unchanged.src/utils/zip-directory.ts: usezlib.crc32; the hand-written table and its test are gone.src/nextjs-cache.ts: drop theentry.parentPath ?? entry.pathfallback.src/nextjs-static-assets.ts: drop the Node 20.12 mention fromlistFiles's comment (the explicit walk stays for symlinks).next build(adapter, cache-handler, use-cache handlers) targetnode${minNodeVersion}instead ofnode24; the Lambda runtime shells stay onnode24.docs/breaking-changes.md(under 0.7.14) state the requirement.Not
!: Node 20 is EOL, andenginesis advisory for a library (npm warns; pnpm errors only withengine-strict).Verified:
pnpm test(47 suites, 1175 tests) andpnpm eslintpass.