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.
Files changed (263) hide show
  1. package/.jsii +2404 -2723
  2. package/.jsii.tabl.json +1 -1
  3. package/API.md +1681 -1930
  4. package/CLAUDE.md +2 -1
  5. package/README.md +429 -130
  6. package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +148 -169
  7. package/assets/lambdas/redeploy-stage/redeploy-stage.lambda/index.js +875 -0
  8. package/docs/adapter-runtime.md +185 -0
  9. package/docs/breaking-changes.md +667 -4
  10. package/docs/caching-guide.md +156 -112
  11. package/docs/cdk-nextjs-cfn-exec-policy.json +2 -1
  12. package/docs/development-guide.md +1 -1
  13. package/docs/harness-coverage.md +598 -0
  14. package/docs/init-cache-deployment.md +32 -1
  15. package/docs/next-build-output-guide.md +30 -20
  16. package/examples/README.md +5 -1
  17. package/lib/adapter/__fixtures__/app-playground-base-path.json +4926 -0
  18. package/lib/adapter/__fixtures__/app-playground.json +4917 -0
  19. package/lib/adapter/__fixtures__/pages-i18n.json +1728 -0
  20. package/lib/adapter/adapter.mjs +1548 -185
  21. package/lib/adapter/adapter.test.js +106 -0
  22. package/lib/adapter/aws-cache-store.d.ts +489 -0
  23. package/lib/adapter/aws-cache-store.js +1004 -0
  24. package/lib/adapter/aws-cache-store.test.d.ts +1 -0
  25. package/lib/adapter/aws-cache-store.test.js +361 -0
  26. package/lib/adapter/build-outputs.d.ts +80 -0
  27. package/lib/adapter/build-outputs.js +852 -0
  28. package/lib/adapter/build-outputs.test.d.ts +1 -0
  29. package/lib/adapter/build-outputs.test.js +861 -0
  30. package/lib/adapter/cache-handler.d.ts +9 -2
  31. package/lib/adapter/cache-handler.js +72 -96
  32. package/lib/adapter/cache-handler.mjs +18956 -17774
  33. package/lib/adapter/cache-handler.test.js +263 -31
  34. package/lib/adapter/cache-kinds.d.ts +32 -0
  35. package/lib/adapter/cache-kinds.js +35 -0
  36. package/lib/adapter/cache-kinds.test.d.ts +1 -0
  37. package/lib/adapter/cache-kinds.test.js +69 -0
  38. package/lib/adapter/cache-utils.d.ts +90 -0
  39. package/lib/adapter/cache-utils.js +199 -4
  40. package/lib/adapter/cache-utils.test.d.ts +1 -0
  41. package/lib/adapter/cache-utils.test.js +179 -0
  42. package/lib/adapter/function-groups.d.ts +215 -0
  43. package/lib/adapter/function-groups.js +1099 -0
  44. package/lib/adapter/function-groups.test.d.ts +1 -0
  45. package/lib/adapter/function-groups.test.js +743 -0
  46. package/lib/adapter/init-cache.d.ts +6 -0
  47. package/lib/adapter/init-cache.js +245 -0
  48. package/lib/adapter/init-cache.test.d.ts +1 -0
  49. package/lib/adapter/init-cache.test.js +137 -0
  50. package/lib/adapter/local-file-cache-handler.d.ts +23 -0
  51. package/lib/adapter/local-file-cache-handler.js +52 -1
  52. package/lib/adapter/memory-cache-handler.d.ts +26 -17
  53. package/lib/adapter/memory-cache-handler.js +32 -50
  54. package/lib/adapter/memory-cache-handler.test.js +105 -85
  55. package/lib/adapter/s3-cache-handler.d.ts +179 -40
  56. package/lib/adapter/s3-cache-handler.js +807 -277
  57. package/lib/adapter/s3-cache-handler.test.js +1186 -31
  58. package/lib/adapter/use-cache-common.d.ts +90 -0
  59. package/lib/adapter/use-cache-common.js +250 -0
  60. package/lib/adapter/use-cache-common.test.d.ts +1 -0
  61. package/lib/adapter/use-cache-common.test.js +646 -0
  62. package/lib/adapter/use-cache-default-handler.d.ts +24 -0
  63. package/lib/adapter/use-cache-default-handler.js +90 -0
  64. package/lib/adapter/use-cache-default-handler.mjs +38645 -0
  65. package/lib/adapter/use-cache-handlers.test.d.ts +1 -0
  66. package/lib/adapter/use-cache-handlers.test.js +349 -0
  67. package/lib/adapter/use-cache-remote-handler.d.ts +26 -0
  68. package/lib/adapter/use-cache-remote-handler.js +186 -0
  69. package/lib/{image-optimization/handler.mjs → adapter/use-cache-remote-handler.mjs} +33572 -38798
  70. package/lib/generated-structs/OptionalNextjsApiProps.d.ts +59 -0
  71. package/lib/generated-structs/OptionalNextjsApiProps.js +3 -0
  72. package/lib/generated-structs/OptionalNextjsCacheProps.d.ts +15 -6
  73. package/lib/generated-structs/OptionalNextjsCacheProps.js +1 -1
  74. package/lib/generated-structs/OptionalNextjsContainersProps.d.ts +25 -11
  75. package/lib/generated-structs/OptionalNextjsContainersProps.js +1 -1
  76. package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +31 -9
  77. package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
  78. package/lib/generated-structs/OptionalNextjsPostDeployProps.d.ts +8 -0
  79. package/lib/generated-structs/OptionalNextjsPostDeployProps.js +1 -1
  80. package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.d.ts +6 -4
  81. package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.js +1 -1
  82. package/lib/index.d.ts +6 -7
  83. package/lib/index.js +2 -4
  84. package/lib/lambdas/post-deploy/prune-cache-bucket.d.ts +7 -3
  85. package/lib/lambdas/post-deploy/prune-cache-bucket.js +57 -71
  86. package/lib/lambdas/post-deploy/prune-cache-bucket.test.d.ts +1 -0
  87. package/lib/lambdas/post-deploy/prune-cache-bucket.test.js +114 -0
  88. package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +1 -0
  89. package/lib/lambdas/post-deploy/prune-revalidation-table.js +23 -31
  90. package/lib/lambdas/post-deploy/prune-revalidation-table.test.d.ts +1 -0
  91. package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +71 -0
  92. package/lib/lambdas/post-deploy/prune-s3.d.ts +14 -5
  93. package/lib/lambdas/post-deploy/prune-s3.js +41 -12
  94. package/lib/lambdas/post-deploy/prune-s3.test.js +97 -18
  95. package/lib/lambdas/redeploy-stage/redeploy-stage-function.d.ts +13 -0
  96. package/lib/lambdas/redeploy-stage/redeploy-stage-function.js +55 -0
  97. package/lib/lambdas/redeploy-stage/redeploy-stage.d.ts +24 -0
  98. package/lib/lambdas/redeploy-stage/redeploy-stage.js +68 -0
  99. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.d.ts +20 -0
  100. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.js +24 -0
  101. package/lib/lambdas/redeploy-stage/redeploy-stage.test.d.ts +1 -0
  102. package/lib/lambdas/redeploy-stage/redeploy-stage.test.js +93 -0
  103. package/lib/nextjs-api.d.ts +102 -18
  104. package/lib/nextjs-api.js +338 -38
  105. package/lib/nextjs-api.test.js +537 -1
  106. package/lib/nextjs-build/global-containers.Dockerfile +15 -10
  107. package/lib/nextjs-build/nextjs-build.d.ts +339 -63
  108. package/lib/nextjs-build/nextjs-build.js +684 -327
  109. package/lib/nextjs-build/nextjs-build.test.d.ts +1 -0
  110. package/lib/nextjs-build/nextjs-build.test.js +412 -0
  111. package/lib/nextjs-build/patch-fetch.js +1 -1
  112. package/lib/nextjs-build/patch-fetch.test.js +166 -11
  113. package/lib/nextjs-build/regional-containers.Dockerfile +17 -10
  114. package/lib/nextjs-cache.d.ts +82 -4
  115. package/lib/nextjs-cache.js +186 -10
  116. package/lib/nextjs-cache.test.js +176 -1
  117. package/lib/nextjs-compute/nextjs-compute-base-props.d.ts +21 -8
  118. package/lib/nextjs-compute/nextjs-compute-base-props.js +41 -1
  119. package/lib/nextjs-compute/nextjs-compute-base-props.test.d.ts +1 -0
  120. package/lib/nextjs-compute/nextjs-compute-base-props.test.js +44 -0
  121. package/lib/nextjs-compute/nextjs-containers.d.ts +19 -0
  122. package/lib/nextjs-compute/nextjs-containers.js +57 -20
  123. package/lib/nextjs-compute/nextjs-functions.d.ts +122 -7
  124. package/lib/nextjs-compute/nextjs-functions.js +113 -67
  125. package/lib/nextjs-compute/nextjs-functions.test.d.ts +1 -0
  126. package/lib/nextjs-compute/nextjs-functions.test.js +99 -0
  127. package/lib/nextjs-distribution.d.ts +194 -23
  128. package/lib/nextjs-distribution.js +535 -62
  129. package/lib/nextjs-distribution.test.d.ts +1 -0
  130. package/lib/nextjs-distribution.test.js +697 -0
  131. package/lib/nextjs-post-deploy.d.ts +13 -5
  132. package/lib/nextjs-post-deploy.js +13 -10
  133. package/lib/nextjs-post-deploy.test.d.ts +1 -0
  134. package/lib/nextjs-post-deploy.test.js +34 -0
  135. package/lib/nextjs-static-assets.d.ts +17 -4
  136. package/lib/nextjs-static-assets.js +22 -5
  137. package/lib/root-constructs/nextjs-base-construct.d.ts +102 -58
  138. package/lib/root-constructs/nextjs-base-construct.js +139 -47
  139. package/lib/root-constructs/nextjs-global-containers.d.ts +18 -20
  140. package/lib/root-constructs/nextjs-global-containers.js +20 -71
  141. package/lib/root-constructs/nextjs-global-functions.d.ts +26 -28
  142. package/lib/root-constructs/nextjs-global-functions.js +52 -70
  143. package/lib/root-constructs/nextjs-regional-containers.d.ts +18 -5
  144. package/lib/root-constructs/nextjs-regional-containers.js +8 -15
  145. package/lib/root-constructs/nextjs-regional-functions.d.ts +22 -18
  146. package/lib/root-constructs/nextjs-regional-functions.js +8 -26
  147. package/lib/runtime/api-gateway-path.d.ts +34 -0
  148. package/lib/runtime/api-gateway-path.js +71 -0
  149. package/lib/runtime/api-gateway-path.test.d.ts +1 -0
  150. package/lib/runtime/api-gateway-path.test.js +65 -0
  151. package/lib/runtime/case-redirect.d.ts +22 -0
  152. package/lib/runtime/case-redirect.js +63 -0
  153. package/lib/runtime/case-redirect.test.d.ts +1 -0
  154. package/lib/runtime/case-redirect.test.js +32 -0
  155. package/lib/runtime/core.d.ts +234 -0
  156. package/lib/runtime/core.js +1213 -0
  157. package/lib/runtime/core.test.d.ts +1 -0
  158. package/lib/runtime/core.test.js +1678 -0
  159. package/lib/runtime/deployment-root.d.ts +13 -0
  160. package/lib/runtime/deployment-root.js +29 -0
  161. package/lib/runtime/dispatch.d.ts +331 -0
  162. package/lib/runtime/dispatch.js +751 -0
  163. package/lib/runtime/dispatch.test.d.ts +1 -0
  164. package/lib/runtime/dispatch.test.js +853 -0
  165. package/lib/runtime/entrypoints.d.ts +57 -0
  166. package/lib/runtime/entrypoints.js +115 -0
  167. package/lib/runtime/http/abort-signal.d.ts +6 -0
  168. package/lib/runtime/http/abort-signal.js +19 -0
  169. package/lib/runtime/http/abort-signal.test.d.ts +1 -0
  170. package/lib/runtime/http/abort-signal.test.js +49 -0
  171. package/lib/runtime/http/node-server.d.ts +38 -0
  172. package/lib/runtime/http/node-server.js +99 -0
  173. package/lib/runtime/http/node-server.test.d.ts +1 -0
  174. package/lib/runtime/http/node-server.test.js +96 -0
  175. package/lib/runtime/http/node-sink.d.ts +16 -0
  176. package/lib/runtime/http/node-sink.js +48 -0
  177. package/lib/runtime/http/node-sink.test.d.ts +1 -0
  178. package/lib/runtime/http/node-sink.test.js +66 -0
  179. package/lib/runtime/http/request.d.ts +62 -0
  180. package/lib/runtime/http/request.js +134 -0
  181. package/lib/runtime/http/request.test.d.ts +1 -0
  182. package/lib/runtime/http/request.test.js +103 -0
  183. package/lib/runtime/http/response.d.ts +77 -0
  184. package/lib/runtime/http/response.js +229 -0
  185. package/lib/runtime/http/response.test.d.ts +1 -0
  186. package/lib/runtime/http/response.test.js +178 -0
  187. package/lib/runtime/http/sink.d.ts +73 -0
  188. package/lib/runtime/http/sink.js +188 -0
  189. package/lib/runtime/http/sink.test.d.ts +1 -0
  190. package/lib/runtime/http/sink.test.js +257 -0
  191. package/lib/runtime/image-utils.d.ts +78 -0
  192. package/lib/runtime/image-utils.js +129 -0
  193. package/lib/runtime/image-utils.test.d.ts +1 -0
  194. package/lib/runtime/image-utils.test.js +237 -0
  195. package/lib/runtime/image.d.ts +76 -0
  196. package/lib/runtime/image.js +296 -0
  197. package/lib/runtime/image.test.d.ts +1 -0
  198. package/lib/runtime/image.test.js +405 -0
  199. package/lib/runtime/lambda-event.d.ts +4 -0
  200. package/lib/runtime/lambda-event.js +113 -0
  201. package/lib/runtime/lambda-event.test.d.ts +1 -0
  202. package/lib/runtime/lambda-event.test.js +119 -0
  203. package/lib/runtime/lambda.mjs +41595 -0
  204. package/lib/runtime/load-module.d.ts +8 -0
  205. package/lib/runtime/load-module.js +35 -0
  206. package/lib/runtime/manifest.d.ts +215 -0
  207. package/lib/runtime/manifest.js +118 -0
  208. package/lib/runtime/middleware.d.ts +43 -0
  209. package/lib/runtime/middleware.js +113 -0
  210. package/lib/runtime/middleware.test.d.ts +1 -0
  211. package/lib/runtime/middleware.test.js +259 -0
  212. package/lib/runtime/next-modules.d.ts +55 -0
  213. package/lib/runtime/next-modules.js +124 -0
  214. package/lib/runtime/next-modules.test.d.ts +1 -0
  215. package/lib/runtime/next-modules.test.js +34 -0
  216. package/lib/runtime/public-files.d.ts +32 -0
  217. package/lib/runtime/public-files.js +117 -0
  218. package/lib/runtime/public-files.test.d.ts +1 -0
  219. package/lib/runtime/public-files.test.js +97 -0
  220. package/lib/runtime/server.mjs +41588 -0
  221. package/lib/runtime/static-files.d.ts +50 -0
  222. package/lib/runtime/static-files.js +266 -0
  223. package/lib/runtime/static-files.test.d.ts +1 -0
  224. package/lib/runtime/static-files.test.js +118 -0
  225. package/lib/runtime/util.d.ts +28 -0
  226. package/lib/runtime/util.js +79 -0
  227. package/lib/utils/base-path.d.ts +106 -13
  228. package/lib/utils/base-path.js +254 -41
  229. package/lib/utils/base-path.test.js +219 -51
  230. package/lib/utils/get-architecture.d.ts +7 -8
  231. package/lib/utils/get-architecture.js +12 -13
  232. package/lib/utils/zip-directory.d.ts +14 -0
  233. package/lib/utils/zip-directory.js +121 -0
  234. package/lib/utils/zip-directory.test.d.ts +1 -0
  235. package/lib/utils/zip-directory.test.js +99 -0
  236. package/package.json +22 -3
  237. package/scripts/capture-adapter-fixture.mjs +325 -0
  238. package/scripts/e2e-deploy.sh +398 -0
  239. package/scripts/e2e-harness/README.md +707 -0
  240. package/scripts/e2e-harness/app.js +370 -0
  241. package/scripts/e2e-harness/common.sh +249 -0
  242. package/scripts/e2e-harness/screen.mjs +302 -0
  243. package/scripts/e2e-harness/stage-proxy.mjs +145 -0
  244. package/scripts/e2e-harness/type-filters.mjs +47 -0
  245. package/scripts/e2e-logs.sh +75 -0
  246. package/scripts/e2e-offline.sh +91 -0
  247. package/scripts/e2e-sweep.sh +370 -0
  248. package/scripts/e2e-warm.sh +114 -0
  249. package/scripts/zero-config-build.mjs +418 -0
  250. package/docs/1k-concurrent-users-executions.png +0 -0
  251. package/docs/plans/adapter-runtime-release.md +0 -362
  252. package/docs/plans/phase-0-image-function-opt-in.md +0 -295
  253. package/lib/generated-structs/OptionalDockerImageFunctionProps.d.ts +0 -425
  254. package/lib/generated-structs/OptionalDockerImageFunctionProps.js +0 -3
  255. package/lib/image-optimization/handler-utils.d.ts +0 -34
  256. package/lib/image-optimization/handler-utils.js +0 -83
  257. package/lib/image-optimization/handler-utils.test.js +0 -178
  258. package/lib/nextjs-build/functions.Dockerfile +0 -31
  259. package/lib/nextjs-compute/nextjs-image-function.d.ts +0 -53
  260. package/lib/nextjs-compute/nextjs-image-function.js +0 -59
  261. package/lib/utils/experimental-flags.d.ts +0 -24
  262. package/lib/utils/experimental-flags.js +0 -30
  263. /package/lib/{image-optimization/handler-utils.test.d.ts → adapter/adapter.test.d.ts} +0 -0
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Screen vercel/next.js's e2e suite for harness candidates, and keep the
4
+ * `screening` block in `test/deploy-tests-manifest.json` honest.
5
+ *
6
+ * node scripts/e2e-harness/screen.mjs --next ../next.js # print the funnel
7
+ * node scripts/e2e-harness/screen.mjs --next ../next.js --check # fail if stale
8
+ * node scripts/e2e-harness/screen.mjs --next ../next.js --write # update the manifest
9
+ * node scripts/e2e-harness/screen.mjs --next ../next.js --list cache
10
+ *
11
+ * Why this exists rather than a number typed into a doc: the funnel moves every
12
+ * time next.js is upgraded, and a stale count is indistinguishable from a
13
+ * measured one. `--check` is what makes the recorded overview trustworthy.
14
+ *
15
+ * The disqualifying screens are the ones in this directory's README, in the
16
+ * order that costs least to apply:
17
+ *
18
+ * - **edge / middleware** — cdk-nextjs throws during `next build` for any
19
+ * non-`nodejs` runtime (`assertNodeRuntimes`), so the fixture is unbuildable
20
+ * and no per-case `failed` entry can rescue it.
21
+ * - **skipped-upstream** — `describe.skip`, so the file reports "passing" in
22
+ * seconds without deploying anything. Adding it claims coverage that does not
23
+ * exist. This one over-reports, because the match is textual and some skips are
24
+ * conditional: `incremental-cache-path-traversal` writes
25
+ * `(process.env.__NEXT_CACHE_COMPONENTS ? describe.skip : describe)`, which the
26
+ * harness does not set, so the file does run and did find a real defect. A file
27
+ * disqualified only by this screen is worth re-reading before it is dismissed.
28
+ * - **skipDeployment** — the same trap, spelled in the `nextTestSetup` call:
29
+ * next.js replaces the whole file with `it.only('should skip next deploy')`.
30
+ * The costliest screen to have been missing — it covers 236 of what were
31
+ * otherwise counted as candidates, and two files had already been added to
32
+ * `rules.include` on the strength of a 4s "pass".
33
+ * - **mode-gated** — the third spelling of the same trap, and the one that cost a
34
+ * deploy slot to find: the file's real cases sit inside `if (isNextStart)` or
35
+ * `if (isNextDev)`, and every other mode gets an empty
36
+ * `it('should skip …', () => {})`. `app-fetch-deduping` reported a pass in 5.9s
37
+ * having deployed nothing. A stub whose title names *dev* is the opposite case
38
+ * — the file runs everywhere but dev, `app-prefetch-static` being one — so
39
+ * those are left in.
40
+ * - **isNextDeploy-gate** — usually next.js itself gating out what cannot work
41
+ * behind a CDN. Not always disqualifying; read the gate before dismissing it.
42
+ * - **output-export** — a static export is not what any `NextjsType` deploys.
43
+ * - **scaffold** — `test-template/{{ toFileName name }}`, a `pnpm new-test`
44
+ * template rather than a test.
45
+ * - **verdict** — the file already has a written verdict in the manifest's
46
+ * `excluded-notes`, so it has been deployed (or read) and deliberately left out.
47
+ * Without this screen a decided file stays a candidate forever and gets picked
48
+ * for another batch, which is how the `next-config-ts-native-ts` family would
49
+ * have burned 18 deploy slots twice.
50
+ *
51
+
52
+ * "Clean" means only that none of those apply. It is a candidate list, not a
53
+ * prediction: a clean file still has to be deployed and watched before it goes
54
+ * into `rules.include`.
55
+ */
56
+ import {
57
+ existsSync,
58
+ readdirSync,
59
+ readFileSync,
60
+ statSync,
61
+ writeFileSync,
62
+ } from "node:fs";
63
+ import { basename, dirname, join, resolve } from "node:path";
64
+ import { fileURLToPath } from "node:url";
65
+
66
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
67
+ const MANIFEST = join(repoRoot, "test", "deploy-tests-manifest.json");
68
+ const TEST_ROOT = "test/e2e";
69
+ const SOURCE_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
70
+ const TEST_RE = /\.test\.[tj]sx?$/;
71
+ /**
72
+ * Both spellings that pin a route to the edge runtime: app router's
73
+ * `export const runtime = 'edge'` and pages router's
74
+ * `export const config = { runtime: 'experimental-edge' }`. Matching only the
75
+ * first undercounts by ~17 files.
76
+ */
77
+ const EDGE_RUNTIME_RE = /runtime\s*[:=]\s*['"](experimental-)?edge['"]/;
78
+ /**
79
+ * An empty `it('should skip …', () => {})` — next.js's stub for the modes a
80
+ * mode-gated file does not cover. `dev` in the title means dev is the excluded
81
+ * mode and deploy is not, so those are deliberately not matched.
82
+ */
83
+ const MODE_GATED_STUB_RE =
84
+ /it\(\s*(['"`])should skip (?!.*\bdev\b)[^'"`]*\1\s*,\s*\(\s*\)\s*=>\s*\{\s*\}\s*\)/;
85
+
86
+ const args = process.argv.slice(2);
87
+ const flag = (name) => args.includes(`--${name}`);
88
+ const value = (name) => {
89
+ const i = args.indexOf(`--${name}`);
90
+ return i === -1 ? undefined : args[i + 1];
91
+ };
92
+
93
+ const nextDir = resolve(value("next") ?? join(repoRoot, "..", "next.js"));
94
+
95
+ /** Every file under `dir`, skipping build output. */
96
+ function walk(dir, out = []) {
97
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
98
+ if (entry.name === "node_modules" || entry.name === ".next") continue;
99
+ const path = join(dir, entry.name);
100
+ if (entry.isDirectory()) walk(path, out);
101
+ else out.push(path);
102
+ }
103
+ return out;
104
+ }
105
+
106
+ const read = (path) => {
107
+ try {
108
+ return readFileSync(path, "utf8");
109
+ } catch {
110
+ return "";
111
+ }
112
+ };
113
+
114
+ /**
115
+ * The test file's text, plus the text of every sibling module it pulls in with a
116
+ * relative `import`/`require`, two levels deep. `nextTestSetup` is not always
117
+ * called in the test file: `app-dir/cache-components-errors` has thirteen files
118
+ * that call it through a `shared.util.ts`, or that are a two-line
119
+ * `require('./client.test')` wrapper, and every one of them reported a 4s "pass"
120
+ * having deployed nothing. Two levels is what those two shapes need.
121
+ */
122
+ function testSourceText(testFile, depth = 2, seen = new Set()) {
123
+ if (seen.has(testFile)) return "";
124
+ seen.add(testFile);
125
+ const text = read(testFile);
126
+ if (depth === 0) return text;
127
+ const dir = dirname(testFile);
128
+ const specifiers = [...text.matchAll(/(?:from\s*|require\(\s*)['"](\.[^'"]*)['"]/g)];
129
+ const extensions = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
130
+ return [
131
+ text,
132
+ ...specifiers.flatMap(([, specifier]) => {
133
+ for (const extension of extensions) {
134
+ const candidate = join(dir, specifier + extension);
135
+ if (existsSync(candidate) && !statSync(candidate).isDirectory()) {
136
+ return testSourceText(candidate, depth - 1, seen);
137
+ }
138
+ }
139
+ return [];
140
+ }),
141
+ ].join("\n");
142
+ }
143
+
144
+ /**
145
+ * The fixture for a test file. For `test/e2e/<name>/test/index.test.ts` it lives
146
+ * a directory *above* the test file, and screening only the test file's own
147
+ * directory quietly misses its `middleware.js`.
148
+ */
149
+ function fixtureRoot(testFile) {
150
+ const dir = dirname(testFile);
151
+ return basename(dir) === "test" ? dirname(dir) : dir;
152
+ }
153
+
154
+ /**
155
+ * The `excluded-notes` keys that name files rather than a topic ("edge runtime,
156
+ * generally" is prose), as predicates. Only `**` is honored, and only as a
157
+ * directory-tree suffix — that is every shape the keys use, and a real minimatch
158
+ * would be a dependency for nothing.
159
+ */
160
+ function decidedMatchers(manifest) {
161
+ return Object.keys(manifest["excluded-notes"] ?? {})
162
+ .filter((key) => key.startsWith(`${TEST_ROOT}/`))
163
+ .map((key) =>
164
+ key.endsWith("/**")
165
+ ? (file) => file.startsWith(key.slice(0, -2))
166
+ : (file) => file === key,
167
+ );
168
+ }
169
+
170
+ /** Which of the screens disqualify this file, if any. */
171
+ function screen(testFile, decided = []) {
172
+ const reasons = [];
173
+ if (decided.some((matches) => matches(testFile))) reasons.push("verdict");
174
+ const test = read(testFile);
175
+ const fixture = walk(fixtureRoot(testFile));
176
+ const sources = fixture.filter((p) => SOURCE_RE.test(p));
177
+
178
+ if (fixture.some((p) => /\/middleware\.[tj]sx?$/.test(p))) {
179
+ reasons.push("middleware");
180
+ }
181
+ if (sources.some((p) => EDGE_RUNTIME_RE.test(read(p)))) reasons.push("edge");
182
+ if (/describe\.skip/.test(test)) reasons.push("skipped-upstream");
183
+ // An empty `it('should skip …', () => {})` is what next.js writes for the modes
184
+ // a mode-gated file does not cover. Deploy mode is one of them unless the stub
185
+ // is the *dev*-only one, in which case the file runs here.
186
+ if (MODE_GATED_STUB_RE.test(test)) reasons.push("mode-gated");
187
+ if (/isNextDeploy/.test(test)) reasons.push("isNextDeploy-gate");
188
+ // `nextTestSetup({ skipDeployment: true })` returns `skipped`, the file
189
+ // early-returns, and jest reports it as passing in ~4s having deployed
190
+ // nothing. next.js declaring a file out of scope for deploy mode is the same
191
+ // signal as `isNextDeploy`, just spelled in the setup call.
192
+ // Read through relative imports for this one screen only. `skipDeployment: true`
193
+ // in a shared helper replaces every file that calls it, so following the import
194
+ // cannot over-report; the mode gates above can, since a helper's `isNextDev`
195
+ // branch may cover only some of a file's cases.
196
+ if (/skipDeployment:\s*true/.test(testSourceText(testFile))) {
197
+ reasons.push("skipDeployment");
198
+ }
199
+ // `test/e2e/**/test-template/{{ toFileName name }}/…` is a scaffold for
200
+ // `pnpm new-test`, not a test.
201
+ if (testFile.includes("{{")) reasons.push("scaffold");
202
+ if (sources.some((p) => /output:\s*['"]export['"]/.test(read(p)))) {
203
+ reasons.push("output-export");
204
+ }
205
+ return reasons;
206
+ }
207
+
208
+ process.chdir(nextDir);
209
+ const testFiles = walk(TEST_ROOT).filter((p) => TEST_RE.test(p));
210
+ if (testFiles.length === 0) {
211
+ console.error(`No e2e tests under ${join(nextDir, TEST_ROOT)}`);
212
+ process.exit(1);
213
+ }
214
+
215
+ const manifest = JSON.parse(readFileSync(MANIFEST, "utf8"));
216
+ const included = new Set(manifest.rules.include);
217
+ /**
218
+ * A file in `suites` runs too - minus its named cases - so it is neither a
219
+ * candidate nor an unscreened file.
220
+ */
221
+ const inPart = new Set(Object.keys(manifest.suites ?? {}));
222
+
223
+ const decided = decidedMatchers(manifest);
224
+
225
+ const rows = testFiles.map((file) => ({
226
+ file,
227
+ included: included.has(file) || inPart.has(file),
228
+ reasons: screen(file, decided),
229
+ cases: (read(file).match(/^\s*it(\.each)?\(/gm) ?? []).length,
230
+ }));
231
+
232
+ const candidates = rows.filter((r) => !r.included && r.reasons.length === 0);
233
+ const tally = {};
234
+ for (const row of rows) {
235
+ if (row.included) continue;
236
+ for (const reason of row.reasons) tally[reason] = (tally[reason] ?? 0) + 1;
237
+ }
238
+ /** The ceiling on what could ever run here, edge being the hard limitation. */
239
+ const edgeFree = rows.filter(
240
+ (r) => !r.reasons.includes("edge") && !r.reasons.includes("middleware"),
241
+ ).length;
242
+
243
+ const screening = {
244
+ comment: [
245
+ "Regenerate with `node scripts/e2e-harness/screen.mjs --next ../next.js --write`,",
246
+ "and `--check` to verify these numbers still hold. Not read by next.js's",
247
+ "test/get-test-filter.js, which only looks at `version`, `suites` and `rules`.",
248
+ "",
249
+ "`candidates` is what is left after the screens below, all of which are",
250
+ "explained in scripts/e2e-harness/README.md. A candidate is a file worth",
251
+ "spending a deploy on - not a file expected to pass. Nothing enters",
252
+ "`rules.include` until it has been watched to pass against a real deployment,",
253
+ "and anything that fails gets a verdict in docs/harness-coverage.md.",
254
+ "",
255
+ "`included-in-part` counts files in `suites`: run, minus the cases named in",
256
+ "their `failed` arrays. They are excluded from `candidates` too.",
257
+ "",
258
+ "`disqualified-by` counts overlap - a fixture can ship edge middleware *and*",
259
+ "be skipped upstream - so the values sum to more than `e2e-files` minus",
260
+ "`candidates` minus `included`.",
261
+ ],
262
+ "next-version": JSON.parse(
263
+ readFileSync(join(nextDir, "packages", "next", "package.json"), "utf8"),
264
+ ).version,
265
+ "e2e-files": rows.length,
266
+ included: included.size,
267
+ "included-in-part": inPart.size,
268
+ "edge-free": edgeFree,
269
+ candidates: candidates.length,
270
+ "disqualified-by": Object.fromEntries(
271
+ Object.entries(tally).sort((a, b) => b[1] - a[1]),
272
+ ),
273
+ };
274
+
275
+ if (flag("list")) {
276
+ const pattern = value("list");
277
+ for (const row of candidates) {
278
+ if (!pattern || row.file.includes(pattern)) {
279
+ console.log(`${String(row.cases).padStart(3)} ${row.file}`);
280
+ }
281
+ }
282
+ process.exit(0);
283
+ }
284
+
285
+ console.log(JSON.stringify(screening, null, 2));
286
+
287
+ if (flag("write")) {
288
+ writeFileSync(
289
+ MANIFEST,
290
+ `${JSON.stringify({ ...manifest, screening }, null, 2)}\n`,
291
+ );
292
+ console.log(`\nWrote ${MANIFEST}`);
293
+ } else if (flag("check")) {
294
+ const recorded = JSON.stringify(manifest.screening);
295
+ if (recorded !== JSON.stringify(screening)) {
296
+ console.error(
297
+ "\nThe manifest's `screening` block is stale. Re-run with --write.",
298
+ );
299
+ process.exit(1);
300
+ }
301
+ console.log("\n`screening` is up to date.");
302
+ }
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * A localhost front door for a `NextjsRegionalFunctions` harness stack, which
4
+ * puts the API Gateway stage back into every request path.
5
+ *
6
+ * node stage-proxy.mjs <port> <target>
7
+ * node stage-proxy.mjs 43210 https://abc123.execute-api.us-east-1.amazonaws.com/prod
8
+ *
9
+ * Why it has to exist: a REST API's execute-api URL always carries the stage
10
+ * (`.../prod`), and next.js's harness discards any path in the deployment URL —
11
+ * `getFullUrl` in `test/lib/next-test-utils.ts` assigns `pathname` outright — so
12
+ * every absolute path the suite requests would miss the stage and get a 403 from
13
+ * API Gateway. The fixtures are written against a deployment served at the
14
+ * origin root, which is what this presents: `http://127.0.0.1:<port>/foo` goes to
15
+ * `<target>/foo`.
16
+ *
17
+ * The app is deployed with the fixture's own `basePath`, or none (`app.js`),
18
+ * never one that includes the stage. API Gateway strips the stage again before
19
+ * invoking the Lambda, so the app sees the same paths it would at a root-mapped
20
+ * custom domain — which is the setup `examples/regional-functions/README.md`
21
+ * recommends for production.
22
+ *
23
+ * The response goes back unchanged except for `Location`: an absolute one naming
24
+ * the target, or this origin over `https`, is rewritten to this origin, so the harness follows redirects here
25
+ * rather than to a URL whose stage it would then strip. Bodies stream both ways,
26
+ * so a streamed render is observed as streamed.
27
+ *
28
+ * Plain Node, no dependencies: `e2e-deploy.sh` starts it detached and it outlives
29
+ * that script, serving every test file the shared stack hosts.
30
+ */
31
+ import { createServer, request as httpRequest } from "node:http";
32
+ import { request as httpsRequest } from "node:https";
33
+
34
+ const [portArg, targetArg] = process.argv.slice(2);
35
+ if (!portArg || !targetArg) {
36
+ console.error("usage: stage-proxy.mjs <port> <target-url-with-stage>");
37
+ process.exit(2);
38
+ }
39
+ const port = Number(portArg);
40
+ const target = new URL(targetArg);
41
+ // No trailing slash: request paths are absolute and supply their own.
42
+ const stagePrefix = target.pathname.replace(/\/+$/, "");
43
+ const targetOrigin = `${target.protocol}//${target.host}`;
44
+ const send = target.protocol === "https:" ? httpsRequest : httpRequest;
45
+
46
+ /** Hop-by-hop headers (RFC 9110 §7.6.1), which a proxy must not forward. */
47
+ const HOP_BY_HOP = new Set([
48
+ "connection",
49
+ "keep-alive",
50
+ "proxy-connection",
51
+ "transfer-encoding",
52
+ "te",
53
+ "trailer",
54
+ "upgrade",
55
+ ]);
56
+
57
+ function forwardable(headers) {
58
+ const out = {};
59
+ for (const [name, value] of Object.entries(headers)) {
60
+ if (!HOP_BY_HOP.has(name.toLowerCase()) && value !== undefined) {
61
+ out[name] = value;
62
+ }
63
+ }
64
+ return out;
65
+ }
66
+
67
+ const server = createServer((req, res) => {
68
+ const localOrigin = `http://${localHost(req)}`;
69
+ const headers = forwardable(req.headers);
70
+ // API Gateway routes on the Host header and TLS on SNI, so both have to name
71
+ // the target - which is not the origin the browser is on. `x-forwarded-host`
72
+ // carries that one, and both Next.js and cdk-nextjs's runtime prefer it over
73
+ // `host`: without it, a server action's CSRF check compares the browser's
74
+ // `Origin` (`127.0.0.1:<port>`) against the execute-api host and aborts, and a
75
+ // `redirect()` to `location.origin + ...` reads as external. API Gateway sets
76
+ // `x-forwarded-proto: https` over ours, so absolute URLs the app builds come
77
+ // back as `https://127.0.0.1:<port>`; `rewriteLocation` fixes those up.
78
+ //
79
+ // What no header can fix: a fetch the *server* makes to its own origin (a
80
+ // server action streaming its redirect target, for one). It leaves the Lambda
81
+ // for 127.0.0.1, which is not this machine from there. That is the price of a
82
+ // local front door, and why such a test failing here is not a product defect.
83
+ headers.host = target.host;
84
+ headers["x-forwarded-host"] = localHost(req);
85
+
86
+ const upstream = send(
87
+ {
88
+ protocol: target.protocol,
89
+ hostname: target.hostname,
90
+ port: target.port || undefined,
91
+ method: req.method,
92
+ // `req.url` is the raw request target, still percent-encoded and with any
93
+ // repeated slashes intact: what the suite sent is what the app should see.
94
+ path: `${stagePrefix}${req.url}`,
95
+ headers,
96
+ },
97
+ (upstreamRes) => {
98
+ const responseHeaders = forwardable(upstreamRes.headers);
99
+ const location = upstreamRes.headers.location;
100
+ if (typeof location === "string") {
101
+ responseHeaders.location = rewriteLocation(location, localOrigin);
102
+ }
103
+ res.writeHead(upstreamRes.statusCode ?? 502, responseHeaders);
104
+ upstreamRes.pipe(res);
105
+ },
106
+ );
107
+ upstream.on("error", (error) => {
108
+ console.error(`stage-proxy: ${req.method} ${req.url}:`, error.message);
109
+ if (!res.headersSent) {
110
+ res.writeHead(502, { "content-type": "text/plain" });
111
+ }
112
+ res.end(`stage-proxy: ${error.message}`);
113
+ });
114
+ req.pipe(upstream);
115
+ });
116
+
117
+ /**
118
+ * An absolute `Location` on the target — with or without the stage — comes back
119
+ * on this origin. A relative one needs nothing: the app sees no stage, so it
120
+ * never emits one.
121
+ */
122
+ function rewriteLocation(location, localOrigin) {
123
+ const httpsLocal = localOrigin.replace(/^http:/, "https:");
124
+ for (const prefix of [
125
+ `${targetOrigin}${stagePrefix}`,
126
+ targetOrigin,
127
+ httpsLocal,
128
+ ]) {
129
+ if (location === prefix || location.startsWith(`${prefix}/`)) {
130
+ return `${localOrigin}${location.slice(prefix.length) || "/"}`;
131
+ }
132
+ }
133
+ return location;
134
+ }
135
+
136
+ /** The authority the client addressed this proxy by. */
137
+ function localHost(req) {
138
+ return req.headers.host ?? `127.0.0.1:${port}`;
139
+ }
140
+
141
+ server.listen(port, "127.0.0.1", () => {
142
+ console.error(
143
+ `stage-proxy: http://127.0.0.1:${port} -> ${targetOrigin}${stagePrefix}`,
144
+ );
145
+ });
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Writes the manifest overlay that applies one NextjsType's exclusions.
4
+ *
5
+ * node scripts/e2e-harness/type-filters.mjs <manifest> <nextjs-type> <out>
6
+ *
7
+ * `rules.excludeByType` in `test/deploy-tests-manifest.json` maps a type to the
8
+ * files, or cases within a file, that the harness cannot pass on that type for
9
+ * a reason docs/harness-coverage.md records - never a cdk-nextjs defect, which
10
+ * is a bug to fix. next.js's `test/get-test-filter.js` reads only
11
+ * `rules.include`/`rules.exclude` and `suites`, but it merges every manifest in
12
+ * the comma-separated NEXT_EXTERNAL_TESTS_FILTERS, and an exclusion by filename
13
+ * beats an inclusion. So this writes one more manifest for that list:
14
+ *
15
+ * - an entry with no `failed` excludes the whole file (`rules.exclude`);
16
+ * - an entry with `failed` skips those cases and runs the rest (`suites`), the
17
+ * same mechanism as a `suites` entry in the manifest itself.
18
+ *
19
+ * A manifest without `rules.excludeByType`, or a type without an entry, gets an
20
+ * overlay that changes nothing, so a custom `test_filters` still works.
21
+ */
22
+ import { readFileSync, writeFileSync } from "node:fs";
23
+
24
+ const [manifestPath, nextjsType, outPath] = process.argv.slice(2);
25
+ if (!manifestPath || !nextjsType || !outPath) {
26
+ console.error("usage: type-filters.mjs <manifest> <nextjs-type> <out>");
27
+ process.exit(2);
28
+ }
29
+
30
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
31
+ const entries = manifest.rules?.excludeByType?.[nextjsType] ?? {};
32
+
33
+ const overlay = { version: 2, suites: {}, rules: { include: [], exclude: [] } };
34
+ for (const [file, entry] of Object.entries(entries)) {
35
+ if (file === "comment") continue;
36
+ if (entry.failed?.length) {
37
+ overlay.suites[file] = { failed: entry.failed, flakey: [] };
38
+ } else {
39
+ overlay.rules.exclude.push(file);
40
+ }
41
+ }
42
+
43
+ writeFileSync(outPath, `${JSON.stringify(overlay, null, 2)}\n`);
44
+ console.log(
45
+ `harness: ${nextjsType} excludes ${overlay.rules.exclude.length} files and ` +
46
+ `cases in ${Object.keys(overlay.suites).length} more`,
47
+ );
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # NEXT_TEST_DEPLOY_LOGS_SCRIPT_PATH for the Next.js adapter compatibility
4
+ # harness. Runs with `cwd` set to the harness's temporary app, in a separate
5
+ # process from the deploy script, and additionally gets NEXT_TEST_DIR and
6
+ # NEXT_TEST_DEPLOY_URL.
7
+ #
8
+ # Its output must start with the BUILD_ID / DEPLOYMENT_ID /
9
+ # NEXT_SUPPORTS_IMMUTABLE_ASSETS markers.
10
+ #
11
+ # What comes *after* them is not just failure diagnostics: in deploy mode this
12
+ # script's stdout **is** `next.cliOutput` (`next-deploy.ts`:
13
+ # `this._cliOutput = await this.fetchBuildLogsUsingCustomScript()`), so every test
14
+ # that asserts on `cliOutput` sees it. Vercel's own deploy mode puts only the
15
+ # *build* logs there, and a test like `test/e2e/deprecation-warnings` -
16
+ # `expect(cliOutput).not.toContain('is deprecated')` - fails on a deprecation
17
+ # notice printed by the CDK CLI or echoed out of a CloudWatch tail, neither of
18
+ # which the app emitted. So the default output stops at the build log, and the
19
+ # deploy log and runtime logs are opt-in via HARNESS_VERBOSE_LOGS=1 for when a
20
+ # deployment is actually being debugged.
21
+ #
22
+ # @see https://nextjs.org/docs/app/api-reference/adapters/testing-adapters
23
+ set -uo pipefail
24
+
25
+ : "${ADAPTER_DIR:?must be set to the cdk-nextjs checkout (the harness passes it through)}"
26
+ # shellcheck source=scripts/e2e-harness/common.sh
27
+ source "$ADAPTER_DIR/scripts/e2e-harness/common.sh"
28
+
29
+ # Never fail. The harness turns a non-zero exit here into
30
+ # "Custom deploy logs script failed", which replaces the deploy error that is
31
+ # the actual reason it asked for logs.
32
+ trap 'exit 0' ERR
33
+
34
+ LOG_LINES=400
35
+
36
+ # Markers first: `parseIdsFromCliOutput` takes the first match of each.
37
+ [ -f "$HARNESS_MARKERS_FILE" ] && cat "$HARNESS_MARKERS_FILE"
38
+
39
+ if [ -f "$HARNESS_BUILD_LOG" ]; then
40
+ echo "=== $HARNESS_BUILD_LOG (last $LOG_LINES lines) ==="
41
+ tail -n "$LOG_LINES" "$HARNESS_BUILD_LOG"
42
+ fi
43
+
44
+ # Everything below is off by default; see the header. The harness spawns this
45
+ # script with the environment `run-tests.js` was started in, so re-running one
46
+ # file with `HARNESS_VERBOSE_LOGS=1 node run-tests.js …` is all it takes.
47
+ if [ "${HARNESS_VERBOSE_LOGS:-0}" != "1" ]; then
48
+ exit 0
49
+ fi
50
+
51
+ if [ -f "$HARNESS_DEPLOY_LOG" ]; then
52
+ echo "=== $HARNESS_DEPLOY_LOG (last $LOG_LINES lines) ==="
53
+ tail -n "$LOG_LINES" "$HARNESS_DEPLOY_LOG"
54
+ fi
55
+
56
+ # Runtime logs: a deployment that came up but answers wrongly leaves its
57
+ # evidence in CloudWatch, not in either log above.
58
+ # The Containers types name their log group outright; the Functions types' is
59
+ # derived from the function name.
60
+ if [ -f "$HARNESS_STACK_FILE" ]; then
61
+ STACK_NAME="$(cat "$HARNESS_STACK_FILE")"
62
+ LOG_GROUP="$(harness_stack_output "$STACK_NAME" ServerLogGroupName)"
63
+ if [ -z "$LOG_GROUP" ]; then
64
+ FUNCTION_NAME="$(harness_stack_output "$STACK_NAME" ServerFunctionName)"
65
+ [ -n "$FUNCTION_NAME" ] && LOG_GROUP="/aws/lambda/$FUNCTION_NAME"
66
+ fi
67
+ if [ -n "$LOG_GROUP" ]; then
68
+ echo "=== $LOG_GROUP (last 30m) ==="
69
+ aws logs tail "$LOG_GROUP" \
70
+ --since 30m \
71
+ --format short 2>&1 | tail -n "$LOG_LINES" || true
72
+ fi
73
+ fi
74
+
75
+ exit 0
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Build one e2e fixture through the adapter and serve it locally, so a failing
4
+ # harness case can be instrumented without a 3-minute deploy per iteration.
5
+ #
6
+ # Usage, from this repo:
7
+ # scripts/e2e-offline.sh app-dir/layout-params [port]
8
+ #
9
+ # Prints two URLs: our container shell, and `next start` on the *same build*, which
10
+ # is the comparison that matters - every "why does the deployed app disagree with
11
+ # `next start`" question is one `diff` of two responses away once both are up.
12
+ #
13
+ # Why inside the next.js checkout: a fixture resolves `next` by walking up to the
14
+ # repo's own `node_modules/next` symlink. Staged anywhere else, Turbopack fails with
15
+ # "Could not find the Next.js package". `node_modules/cdk-nextjs` then has to live
16
+ # inside the app for the same reason `e2e-deploy.sh` puts it there - the adapter
17
+ # resolves its cache handler through a package boundary, and a relative path out of
18
+ # the app directory "leaves the filesystem root".
19
+ #
20
+ # @see scripts/e2e-harness/README.md
21
+ # @see docs/harness-coverage.md, which several entries were measured with this
22
+ set -euo pipefail
23
+
24
+ FIXTURE="${1:?usage: e2e-offline.sh <fixture, e.g. app-dir/layout-params> [port]}"
25
+ PORT="${2:-3112}"
26
+ NEXT_START_PORT=$((PORT + 1))
27
+
28
+ ADAPTER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
29
+ NEXTJS_DIR="${NEXTJS_DIR:-$(cd "$ADAPTER_DIR/../next.js" && pwd)}"
30
+ FIXTURE_DIR="$NEXTJS_DIR/test/e2e/$FIXTURE"
31
+ if [ ! -d "$FIXTURE_DIR" ]; then
32
+ echo "e2e-offline: no fixture at $FIXTURE_DIR (set NEXTJS_DIR?)" >&2
33
+ exit 1
34
+ fi
35
+ for required in "$ADAPTER_DIR/lib/adapter/adapter.mjs" \
36
+ "$ADAPTER_DIR/lib/adapter/cache-handler.mjs" \
37
+ "$ADAPTER_DIR/lib/adapter/use-cache-default-handler.mjs" \
38
+ "$ADAPTER_DIR/lib/adapter/use-cache-remote-handler.mjs" \
39
+ "$ADAPTER_DIR/lib/runtime/server.mjs"; do
40
+ if [ ! -f "$required" ]; then
41
+ echo "e2e-offline: $required is missing. Run \`pnpm bundle\` in $ADAPTER_DIR." >&2
42
+ exit 1
43
+ fi
44
+ done
45
+
46
+ APP_NAME="offline-$(basename "$FIXTURE")"
47
+ APP_DIR="$NEXTJS_DIR/$APP_NAME"
48
+ rm -rf "$APP_DIR"
49
+ mkdir -p "$APP_DIR"
50
+ # Everything the fixture ships except its test file: the harness copies the same
51
+ # set, minus the config snippet it appends (which only aliases __NEXT_TEST_MODE).
52
+ (cd "$FIXTURE_DIR" && tar cf - --exclude='*.test.ts' --exclude='*.test.js' .) |
53
+ (cd "$APP_DIR" && tar xf -)
54
+ printf '{ "name": "%s", "private": true }\n' "$APP_NAME" >"$APP_DIR/package.json"
55
+ [ -f "$APP_DIR/next.config.js" ] || [ -f "$APP_DIR/next.config.ts" ] ||
56
+ [ -f "$APP_DIR/next.config.mjs" ] || echo 'module.exports = {}' >"$APP_DIR/next.config.js"
57
+
58
+ mkdir -p "$APP_DIR/node_modules/cdk-nextjs/lib/adapter"
59
+ cp "$ADAPTER_DIR/package.json" "$APP_DIR/node_modules/cdk-nextjs/package.json"
60
+ # Every bundle the package's `exports` map points at: the adapter, the
61
+ # `cacheHandler`, and the `cache-handlers/*` it resolves. A glob rather than the
62
+ # list checked above, so a new export is copied without being added here.
63
+ cp "$ADAPTER_DIR"/lib/adapter/*.mjs "$APP_DIR/node_modules/cdk-nextjs/lib/adapter/"
64
+
65
+ cd "$APP_DIR"
66
+ echo "e2e-offline: building $APP_NAME" >&2
67
+ NEXT_ADAPTER_PATH="$APP_DIR/node_modules/cdk-nextjs/lib/adapter/adapter.mjs" \
68
+ NEXT_PRIVATE_TEST_MODE=e2e \
69
+ NEXT_TELEMETRY_DISABLED=1 \
70
+ "$NEXTJS_DIR/node_modules/.bin/next" build
71
+
72
+ # `deploymentRootOf` asserts the shell lives in a directory named
73
+ # `cdk-nextjs-runtime` next to the staged project, so reproduce that layout rather
74
+ # than running the bundle from wherever it happens to be.
75
+ STAGED="$APP_DIR/.next/cdk-nextjs-adapter/app"
76
+ mkdir -p "$STAGED/cdk-nextjs-runtime"
77
+ cp "$ADAPTER_DIR/lib/runtime/server.mjs" "$STAGED/cdk-nextjs-runtime/server.mjs"
78
+ cp "$APP_DIR/.next/cdk-nextjs-adapter/manifest.json" "$STAGED/cdk-nextjs-runtime/manifest.json"
79
+
80
+ cd "$STAGED"
81
+ PORT="$PORT" node cdk-nextjs-runtime/server.mjs &
82
+ SHELL_PID=$!
83
+ cd "$APP_DIR"
84
+ "$NEXTJS_DIR/node_modules/.bin/next" start --port "$NEXT_START_PORT" &
85
+ START_PID=$!
86
+ trap 'kill $SHELL_PID $START_PID 2>/dev/null || true' EXIT INT TERM
87
+ sleep 3
88
+ echo "e2e-offline: cdk-nextjs http://localhost:$PORT" >&2
89
+ echo "e2e-offline: next start http://localhost:$NEXT_START_PORT" >&2
90
+ echo "e2e-offline: app $APP_DIR (delete it when done)" >&2
91
+ wait