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