cdk-nextjs 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsii +2467 -2714
- package/.jsii.tabl.json +1 -1
- package/API.md +1725 -1909
- package/CLAUDE.md +2 -1
- package/README.md +429 -130
- package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +152 -170
- package/assets/lambdas/redeploy-stage/redeploy-stage.lambda/index.js +875 -0
- package/docs/adapter-runtime.md +185 -0
- package/docs/breaking-changes.md +687 -4
- package/docs/caching-guide.md +156 -112
- package/docs/cdk-nextjs-cfn-exec-policy.json +2 -1
- package/docs/development-guide.md +1 -1
- package/docs/harness-coverage.md +598 -0
- package/docs/init-cache-deployment.md +32 -1
- package/docs/next-build-output-guide.md +30 -20
- package/examples/README.md +5 -1
- package/lib/adapter/__fixtures__/app-playground-base-path.json +4926 -0
- package/lib/adapter/__fixtures__/app-playground.json +4917 -0
- package/lib/adapter/__fixtures__/pages-i18n.json +1728 -0
- package/lib/adapter/adapter.mjs +1548 -185
- package/lib/adapter/adapter.test.js +106 -0
- package/lib/adapter/aws-cache-store.d.ts +489 -0
- package/lib/adapter/aws-cache-store.js +1004 -0
- package/lib/adapter/aws-cache-store.test.d.ts +1 -0
- package/lib/adapter/aws-cache-store.test.js +361 -0
- package/lib/adapter/build-outputs.d.ts +80 -0
- package/lib/adapter/build-outputs.js +852 -0
- package/lib/adapter/build-outputs.test.d.ts +1 -0
- package/lib/adapter/build-outputs.test.js +861 -0
- package/lib/adapter/cache-handler.d.ts +9 -2
- package/lib/adapter/cache-handler.js +72 -96
- package/lib/adapter/cache-handler.mjs +18956 -17774
- package/lib/adapter/cache-handler.test.js +263 -31
- package/lib/adapter/cache-kinds.d.ts +32 -0
- package/lib/adapter/cache-kinds.js +35 -0
- package/lib/adapter/cache-kinds.test.d.ts +1 -0
- package/lib/adapter/cache-kinds.test.js +69 -0
- package/lib/adapter/cache-utils.d.ts +90 -0
- package/lib/adapter/cache-utils.js +199 -4
- package/lib/adapter/cache-utils.test.d.ts +1 -0
- package/lib/adapter/cache-utils.test.js +179 -0
- package/lib/adapter/function-groups.d.ts +215 -0
- package/lib/adapter/function-groups.js +1099 -0
- package/lib/adapter/function-groups.test.d.ts +1 -0
- package/lib/adapter/function-groups.test.js +743 -0
- package/lib/adapter/init-cache.d.ts +6 -0
- package/lib/adapter/init-cache.js +245 -0
- package/lib/adapter/init-cache.test.d.ts +1 -0
- package/lib/adapter/init-cache.test.js +137 -0
- package/lib/adapter/local-file-cache-handler.d.ts +23 -0
- package/lib/adapter/local-file-cache-handler.js +52 -1
- package/lib/adapter/memory-cache-handler.d.ts +26 -17
- package/lib/adapter/memory-cache-handler.js +32 -50
- package/lib/adapter/memory-cache-handler.test.js +105 -85
- package/lib/adapter/s3-cache-handler.d.ts +179 -40
- package/lib/adapter/s3-cache-handler.js +807 -277
- package/lib/adapter/s3-cache-handler.test.js +1186 -31
- package/lib/adapter/use-cache-common.d.ts +90 -0
- package/lib/adapter/use-cache-common.js +250 -0
- package/lib/adapter/use-cache-common.test.d.ts +1 -0
- package/lib/adapter/use-cache-common.test.js +646 -0
- package/lib/adapter/use-cache-default-handler.d.ts +24 -0
- package/lib/adapter/use-cache-default-handler.js +90 -0
- package/lib/adapter/use-cache-default-handler.mjs +38645 -0
- package/lib/adapter/use-cache-handlers.test.d.ts +1 -0
- package/lib/adapter/use-cache-handlers.test.js +349 -0
- package/lib/adapter/use-cache-remote-handler.d.ts +26 -0
- package/lib/adapter/use-cache-remote-handler.js +186 -0
- package/lib/{image-optimization/handler.mjs → adapter/use-cache-remote-handler.mjs} +33572 -38798
- package/lib/generated-structs/OptionalNextjsApiProps.d.ts +59 -0
- package/lib/generated-structs/OptionalNextjsApiProps.js +3 -0
- package/lib/generated-structs/OptionalNextjsCacheProps.d.ts +15 -6
- package/lib/generated-structs/OptionalNextjsCacheProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsContainersProps.d.ts +25 -11
- package/lib/generated-structs/OptionalNextjsContainersProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +31 -9
- package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsPostDeployProps.d.ts +13 -0
- package/lib/generated-structs/OptionalNextjsPostDeployProps.js +1 -1
- package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.d.ts +11 -2
- 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/post-deploy.lambda.js +3 -2
- 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 +19 -3
- package/lib/lambdas/post-deploy/prune-s3.js +54 -15
- package/lib/lambdas/post-deploy/prune-s3.test.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-s3.test.js +255 -0
- 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 +107 -18
- package/lib/nextjs-api.js +349 -39
- package/lib/nextjs-api.test.js +582 -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 +23 -3
- package/lib/nextjs-post-deploy.js +14 -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 +23 -4
- package/lib/nextjs-static-assets.js +29 -6
- package/lib/nextjs-static-assets.test.js +20 -1
- package/lib/root-constructs/nextjs-base-construct.d.ts +105 -45
- package/lib/root-constructs/nextjs-base-construct.js +144 -34
- package/lib/root-constructs/nextjs-global-containers.d.ts +18 -20
- package/lib/root-constructs/nextjs-global-containers.js +20 -70
- package/lib/root-constructs/nextjs-global-functions.d.ts +26 -28
- package/lib/root-constructs/nextjs-global-functions.js +52 -69
- package/lib/root-constructs/nextjs-regional-containers.d.ts +25 -5
- package/lib/root-constructs/nextjs-regional-containers.js +17 -15
- package/lib/root-constructs/nextjs-regional-functions.d.ts +22 -18
- package/lib/root-constructs/nextjs-regional-functions.js +8 -25
- package/lib/runtime/api-gateway-path.d.ts +34 -0
- package/lib/runtime/api-gateway-path.js +71 -0
- package/lib/runtime/api-gateway-path.test.d.ts +1 -0
- package/lib/runtime/api-gateway-path.test.js +65 -0
- package/lib/runtime/case-redirect.d.ts +22 -0
- package/lib/runtime/case-redirect.js +63 -0
- package/lib/runtime/case-redirect.test.d.ts +1 -0
- package/lib/runtime/case-redirect.test.js +32 -0
- package/lib/runtime/core.d.ts +234 -0
- package/lib/runtime/core.js +1213 -0
- package/lib/runtime/core.test.d.ts +1 -0
- package/lib/runtime/core.test.js +1678 -0
- package/lib/runtime/deployment-root.d.ts +13 -0
- package/lib/runtime/deployment-root.js +29 -0
- package/lib/runtime/dispatch.d.ts +331 -0
- package/lib/runtime/dispatch.js +751 -0
- package/lib/runtime/dispatch.test.d.ts +1 -0
- package/lib/runtime/dispatch.test.js +853 -0
- package/lib/runtime/entrypoints.d.ts +57 -0
- package/lib/runtime/entrypoints.js +115 -0
- package/lib/runtime/http/abort-signal.d.ts +6 -0
- package/lib/runtime/http/abort-signal.js +19 -0
- package/lib/runtime/http/abort-signal.test.d.ts +1 -0
- package/lib/runtime/http/abort-signal.test.js +49 -0
- package/lib/runtime/http/node-server.d.ts +38 -0
- package/lib/runtime/http/node-server.js +99 -0
- package/lib/runtime/http/node-server.test.d.ts +1 -0
- package/lib/runtime/http/node-server.test.js +96 -0
- package/lib/runtime/http/node-sink.d.ts +16 -0
- package/lib/runtime/http/node-sink.js +48 -0
- package/lib/runtime/http/node-sink.test.d.ts +1 -0
- package/lib/runtime/http/node-sink.test.js +66 -0
- package/lib/runtime/http/request.d.ts +62 -0
- package/lib/runtime/http/request.js +134 -0
- package/lib/runtime/http/request.test.d.ts +1 -0
- package/lib/runtime/http/request.test.js +103 -0
- package/lib/runtime/http/response.d.ts +77 -0
- package/lib/runtime/http/response.js +229 -0
- package/lib/runtime/http/response.test.d.ts +1 -0
- package/lib/runtime/http/response.test.js +178 -0
- package/lib/runtime/http/sink.d.ts +73 -0
- package/lib/runtime/http/sink.js +188 -0
- package/lib/runtime/http/sink.test.d.ts +1 -0
- package/lib/runtime/http/sink.test.js +257 -0
- package/lib/runtime/image-utils.d.ts +78 -0
- package/lib/runtime/image-utils.js +129 -0
- package/lib/runtime/image-utils.test.d.ts +1 -0
- package/lib/runtime/image-utils.test.js +237 -0
- package/lib/runtime/image.d.ts +76 -0
- package/lib/runtime/image.js +296 -0
- package/lib/runtime/image.test.d.ts +1 -0
- package/lib/runtime/image.test.js +405 -0
- package/lib/runtime/lambda-event.d.ts +4 -0
- package/lib/runtime/lambda-event.js +113 -0
- package/lib/runtime/lambda-event.test.d.ts +1 -0
- package/lib/runtime/lambda-event.test.js +119 -0
- package/lib/runtime/lambda.mjs +41595 -0
- package/lib/runtime/load-module.d.ts +8 -0
- package/lib/runtime/load-module.js +35 -0
- package/lib/runtime/manifest.d.ts +215 -0
- package/lib/runtime/manifest.js +118 -0
- package/lib/runtime/middleware.d.ts +43 -0
- package/lib/runtime/middleware.js +113 -0
- package/lib/runtime/middleware.test.d.ts +1 -0
- package/lib/runtime/middleware.test.js +259 -0
- package/lib/runtime/next-modules.d.ts +55 -0
- package/lib/runtime/next-modules.js +124 -0
- package/lib/runtime/next-modules.test.d.ts +1 -0
- package/lib/runtime/next-modules.test.js +34 -0
- package/lib/runtime/public-files.d.ts +32 -0
- package/lib/runtime/public-files.js +117 -0
- package/lib/runtime/public-files.test.d.ts +1 -0
- package/lib/runtime/public-files.test.js +97 -0
- package/lib/runtime/server.mjs +41588 -0
- package/lib/runtime/static-files.d.ts +50 -0
- package/lib/runtime/static-files.js +266 -0
- package/lib/runtime/static-files.test.d.ts +1 -0
- package/lib/runtime/static-files.test.js +118 -0
- package/lib/runtime/util.d.ts +28 -0
- package/lib/runtime/util.js +79 -0
- package/lib/utils/base-path.d.ts +123 -11
- package/lib/utils/base-path.js +276 -39
- package/lib/utils/base-path.test.js +275 -50
- 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,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CDK app `scripts/e2e-deploy.sh` deploys: one long-lived stack wrapping
|
|
3
|
+
* whichever temporary app the Next.js compatibility harness most recently built.
|
|
4
|
+
*
|
|
5
|
+
* Plain CommonJS on purpose. It runs straight from `lib/` after `pnpm compile`,
|
|
6
|
+
* with no tsx, no tsconfig and no place in the jsii assembly - it is test
|
|
7
|
+
* infrastructure, not part of the published library.
|
|
8
|
+
*
|
|
9
|
+
* @see scripts/e2e-harness/README.md
|
|
10
|
+
*/
|
|
11
|
+
const { App, CfnOutput, Duration, Stack } = require("aws-cdk-lib");
|
|
12
|
+
const { Vpc } = require("aws-cdk-lib/aws-ec2");
|
|
13
|
+
const {
|
|
14
|
+
NextjsGlobalContainers,
|
|
15
|
+
NextjsGlobalFunctions,
|
|
16
|
+
NextjsRegionalContainers,
|
|
17
|
+
NextjsRegionalFunctions,
|
|
18
|
+
} = require("../../lib");
|
|
19
|
+
|
|
20
|
+
const appDir = required("HARNESS_APP_DIR");
|
|
21
|
+
const stackName = required("HARNESS_STACK_NAME");
|
|
22
|
+
// One of the four `NextjsType`s, `global-functions` by default; see common.sh's
|
|
23
|
+
// `harness_nextjs_type`, which validates it and keeps the stack names apart.
|
|
24
|
+
const nextjsType = process.env["HARNESS_NEXTJS_TYPE"] || "global-functions";
|
|
25
|
+
|
|
26
|
+
function required(name) {
|
|
27
|
+
const value = process.env[name];
|
|
28
|
+
if (!value) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`${name} is required. This app is deployed by scripts/e2e-deploy.sh, ` +
|
|
31
|
+
`which sets it; see scripts/e2e-harness/README.md to run it by hand.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Pins every post-deploy custom resource property that can differ between two
|
|
39
|
+
* fixtures, so the resource never changes and never does anything. Shared by
|
|
40
|
+
* every type.
|
|
41
|
+
*
|
|
42
|
+
* - `buildId` is the real build ID and `createInvalidationCommandInput` carries
|
|
43
|
+
* a `new Date().toISOString()` caller reference, so either one alone would
|
|
44
|
+
* make every synth a non-hotswappable change, and every test file a
|
|
45
|
+
* CloudFormation deployment.
|
|
46
|
+
* - `staticAssetsKeyPrefix` follows the fixture's `basePath`, so switching
|
|
47
|
+
* between a basePath fixture and one without would still force an Update -
|
|
48
|
+
* and an Update with the bucket and table names set prunes every
|
|
49
|
+
* `<buildId>/` prefix but `"harness"`, i.e. the cache the fixture just
|
|
50
|
+
* seeded. Dropping the names turns off every prune branch in
|
|
51
|
+
* `post-deploy.lambda.ts`, so the Create and any Update are both no-ops.
|
|
52
|
+
*
|
|
53
|
+
* What that gives up is the post-deploy pass itself: no pruning and no
|
|
54
|
+
* invalidation. Neither is missed. Cache isolation between files comes from
|
|
55
|
+
* `CDK_NEXTJS_BUILD_ID` (src/adapter/s3-cache-handler.ts), which *is*
|
|
56
|
+
* hotswappable, and `scripts/e2e-deploy.sh` invalidates the distribution itself
|
|
57
|
+
* - it has to, since a hotswap never runs a custom resource at all. The stack
|
|
58
|
+
* is deleted after every run, so nothing accumulates.
|
|
59
|
+
*
|
|
60
|
+
* `undefined` rather than fixed values: CDK strips undefined properties, and the
|
|
61
|
+
* handler guards on each being absent.
|
|
62
|
+
*/
|
|
63
|
+
const PINNED_POST_DEPLOY = {
|
|
64
|
+
customResourceProperties: {
|
|
65
|
+
buildId: "harness",
|
|
66
|
+
createInvalidationCommandInput: undefined,
|
|
67
|
+
cacheBucketName: undefined,
|
|
68
|
+
revalidationTableName: undefined,
|
|
69
|
+
staticAssetsBucketName: undefined,
|
|
70
|
+
staticAssetsKeyPrefix: undefined,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const COMMON_PROPS = {
|
|
75
|
+
buildDirectory: appDir,
|
|
76
|
+
// `scripts/e2e-deploy.sh` already ran `next build`: the harness's own `build`
|
|
77
|
+
// script chains a `post-build` that prints the BUILD_ID / DEPLOYMENT_ID /
|
|
78
|
+
// NEXT_SUPPORTS_IMMUTABLE_ASSETS markers the harness parses, so the build has
|
|
79
|
+
// to happen there rather than in this synth.
|
|
80
|
+
skipBuild: true,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `NextjsRegionalFunctions`: API Gateway REST + Lambda, no CloudFront.
|
|
85
|
+
*
|
|
86
|
+
* Its URL always carries the stage, which the harness would discard, so
|
|
87
|
+
* `e2e-deploy.sh` does not report `HarnessUrl` to the suite directly: it puts
|
|
88
|
+
* `stage-proxy.mjs` in front of it and reports that. See there. The fixture is
|
|
89
|
+
* deployed exactly as built - no `basePath` injected - because the stage never
|
|
90
|
+
* reaches the app: API Gateway strips it, and the proxy re-adds it on the way in.
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* The app's resolved `basePath` from the build's `required-server-files.json`,
|
|
94
|
+
* the same file `NextjsBuild` reads it from.
|
|
95
|
+
*/
|
|
96
|
+
function fixtureBasePath(dir) {
|
|
97
|
+
const file = require("node:path").join(dir, ".next", "required-server-files.json");
|
|
98
|
+
const { config } = JSON.parse(require("node:fs").readFileSync(file, "utf8"));
|
|
99
|
+
return config.basePath || undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class RegionalHarnessStack extends Stack {
|
|
103
|
+
constructor(scope, id, props) {
|
|
104
|
+
super(scope, id, props);
|
|
105
|
+
const nextjs = new NextjsRegionalFunctions(this, "Nextjs", {
|
|
106
|
+
...COMMON_PROPS,
|
|
107
|
+
// The fixture's own `basePath`, as the prop. `resolveBasePath` would derive
|
|
108
|
+
// the same value - a fixture's `basePath` never starts with the stage, so
|
|
109
|
+
// all of it is a resource path - but it would also warn on every deploy
|
|
110
|
+
// that the app's links miss the stage. They do not here: the proxy plays
|
|
111
|
+
// the root-mapped custom domain synth cannot see. Setting the prop to the
|
|
112
|
+
// app's own value is the documented way to say so.
|
|
113
|
+
basePath: fixtureBasePath(appDir),
|
|
114
|
+
overrides: {
|
|
115
|
+
nextjsApi: {
|
|
116
|
+
restApiProps: {
|
|
117
|
+
// One `AWS::ApiGateway::Account` per region, and it is not ours to
|
|
118
|
+
// own from a test stack.
|
|
119
|
+
cloudWatchRole: false,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
nextjsPostDeploy: PINNED_POST_DEPLOY,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
new CfnOutput(this, "HarnessUrl", {
|
|
126
|
+
key: "HarnessUrl",
|
|
127
|
+
// The stage URL alone - not `nextjs.url`, which appends the `basePath`
|
|
128
|
+
// prop set above, while the fixture's requests already carry it.
|
|
129
|
+
// `stage-proxy.mjs` appends each request's own absolute path, and trims
|
|
130
|
+
// the trailing slash `RestApi.url` ends with.
|
|
131
|
+
value: nextjs.nextjsApi.api.url,
|
|
132
|
+
});
|
|
133
|
+
new CfnOutput(this, "ServerFunctionName", {
|
|
134
|
+
key: "ServerFunctionName",
|
|
135
|
+
value: nextjs.nextjsFunctions.function.functionName,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class HarnessStack extends Stack {
|
|
141
|
+
constructor(scope, id, props) {
|
|
142
|
+
super(scope, id, props);
|
|
143
|
+
|
|
144
|
+
// `NextjsGlobalFunctions` is the default, because its front door is a
|
|
145
|
+
// CloudFront distribution served at the origin root. The harness builds
|
|
146
|
+
// every request URL as `new URL(path, deployUrl)` (`getFullUrl` in
|
|
147
|
+
// `test/lib/next-test-utils.ts` assigns `pathname`), so any prefix in the
|
|
148
|
+
// deployment URL is discarded - which is why `RegionalHarnessStack` above
|
|
149
|
+
// needs `stage-proxy.mjs` and this one needs nothing.
|
|
150
|
+
//
|
|
151
|
+
// It is also the front door the suite expects. `NEXT_TEST_MODE=deploy` is
|
|
152
|
+
// the mode Vercel validates edge-fronted deployments with, so its tests are
|
|
153
|
+
// written to tolerate a CDN, and the ones that cannot are already gated out
|
|
154
|
+
// of deploy mode upstream. Serving from CloudFront means `_next/static` and
|
|
155
|
+
// `public/` are answered by the `NextjsStaticAssets` bucket exactly as in
|
|
156
|
+
// production, rather than by some harness-only arrangement.
|
|
157
|
+
const nextjs = new NextjsGlobalFunctions(this, "Nextjs", {
|
|
158
|
+
...COMMON_PROPS,
|
|
159
|
+
overrides: { nextjsPostDeploy: PINNED_POST_DEPLOY },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
new CfnOutput(this, "HarnessUrl", {
|
|
163
|
+
key: "HarnessUrl",
|
|
164
|
+
// The distribution's own origin, deliberately not `nextjs.url`: that
|
|
165
|
+
// property appends the app's `basePath` (see NextjsGlobalFunctions#url),
|
|
166
|
+
// and next.js's tests expect a deployment URL without one. They build
|
|
167
|
+
// request URLs both ways - `new URL(path, next.url)`, which discards a
|
|
168
|
+
// prefix, and `` `${next.url}${path}` `` with `path` already carrying the
|
|
169
|
+
// basePath, which doubles it (`/base//base/refresh`). Vercel's deployment
|
|
170
|
+
// URL is the bare origin, so that is what a basePath fixture is written
|
|
171
|
+
// against.
|
|
172
|
+
//
|
|
173
|
+
// No trailing slash either, for the same reason: `vercel deploy` prints a
|
|
174
|
+
// bare origin, so `${next.url}${path}` is what the fixtures are written
|
|
175
|
+
// against. `new URL(path, next.url)` is unaffected - `path` is absolute.
|
|
176
|
+
value: `https://${nextjs.nextjsDistribution.distribution.domainName}`,
|
|
177
|
+
});
|
|
178
|
+
// Read by `scripts/e2e-deploy.sh` to invalidate between test files, and by
|
|
179
|
+
// `scripts/e2e-logs.sh`.
|
|
180
|
+
new CfnOutput(this, "DistributionId", {
|
|
181
|
+
key: "DistributionId",
|
|
182
|
+
value: nextjs.nextjsDistribution.distribution.distributionId,
|
|
183
|
+
});
|
|
184
|
+
new CfnOutput(this, "ServerFunctionName", {
|
|
185
|
+
key: "ServerFunctionName",
|
|
186
|
+
value: nextjs.nextjsFunctions.function.functionName,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A path no fixture routes, for the container health checks.
|
|
193
|
+
*
|
|
194
|
+
* Both container types require a `healthCheckPath` that answers 200, and the
|
|
195
|
+
* harness's fixtures - written for Vercel - have no such route. Adding one to
|
|
196
|
+
* every fixture would change the app under test (a pages-only fixture would grow
|
|
197
|
+
* an `app/` directory), so instead both checks are pointed at a path the app
|
|
198
|
+
* 404s and told that any answer means the server is up. They exist to tell a
|
|
199
|
+
* running task from one that is not listening, and a 404 tells them that.
|
|
200
|
+
*
|
|
201
|
+
* Under `/_next/static/`, which both this runtime and `next start` answer from
|
|
202
|
+
* the build's static files alone: a missing file there is a plain 404 before
|
|
203
|
+
* any app route is tried. Anywhere else a fixture's dynamic route can catch it.
|
|
204
|
+
* The top-level `/__cdk-nextjs-harness-health` was taken by
|
|
205
|
+
* `use-cache-metadata-route-handler`'s `/[slug]` page, which threw on it (a 500,
|
|
206
|
+
* past the ALB's 200-499 range, so the task under test kept being replaced);
|
|
207
|
+
* `/_next/__cdk-nextjs-harness-health` was taken by `sub-shell-generation`'s
|
|
208
|
+
* `/[lang]/[slug]` as `lang: "_next"`, and the health check's renders left
|
|
209
|
+
* `/es/*` served a runtime root layout instead of the build-time shell.
|
|
210
|
+
*/
|
|
211
|
+
const HEALTH_CHECK_PATH = "/_next/static/__cdk-nextjs-harness-health";
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The container types' own VPC, with one NAT gateway rather than the default of
|
|
215
|
+
* one per AZ. A sharded run creates a stack per shard, and every NAT gateway
|
|
216
|
+
* holds an Elastic IP out of a per-region quota the account's other stacks share.
|
|
217
|
+
*/
|
|
218
|
+
function harnessVpc(scope) {
|
|
219
|
+
return new Vpc(scope, "Vpc", { maxAzs: 2, natGateways: 1 });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Loosens both container health checks to "the server answered" (see
|
|
224
|
+
* HEALTH_CHECK_PATH), keeping the construct's timings.
|
|
225
|
+
*/
|
|
226
|
+
function acceptAnyAnswer(nextjsContainers, basePath) {
|
|
227
|
+
const path = `${basePath || ""}${HEALTH_CHECK_PATH}`;
|
|
228
|
+
const { albFargateService } = nextjsContainers;
|
|
229
|
+
// `configureHealthCheck` replaces the whole health check, so the construct's
|
|
230
|
+
// timings are restated (src/nextjs-compute/nextjs-containers.ts).
|
|
231
|
+
albFargateService.targetGroup.configureHealthCheck({
|
|
232
|
+
path,
|
|
233
|
+
healthyHttpCodes: "200-499",
|
|
234
|
+
healthyThresholdCount: 2,
|
|
235
|
+
interval: Duration.seconds(10),
|
|
236
|
+
timeout: Duration.seconds(5),
|
|
237
|
+
});
|
|
238
|
+
// The construct sets this the same way, through the container's props; there
|
|
239
|
+
// is no public setter. `wget --spider` fails on a 404, so ask node instead,
|
|
240
|
+
// which resolves `fetch` for any HTTP answer and rejects only when nothing is
|
|
241
|
+
// listening.
|
|
242
|
+
albFargateService.taskDefinition.defaultContainer.props.healthCheck = {
|
|
243
|
+
command: [
|
|
244
|
+
"CMD-SHELL",
|
|
245
|
+
`node -e "fetch('http://localhost:3000${path}').then(() => process.exit(0), () => process.exit(1))"`,
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Keeps the harness's own files out of the image's build context, which is the
|
|
252
|
+
* fixture directory. `.adapter-cdk-out` is where this very synth writes its
|
|
253
|
+
* assembly - left in, the asset would try to stage a copy of itself - and
|
|
254
|
+
* `node_modules` and `.next/cache` are large and unused: the Dockerfile copies
|
|
255
|
+
* only the staged deployment root, `.next/static` and `public`.
|
|
256
|
+
*/
|
|
257
|
+
const CONTAINER_OVERRIDES = {
|
|
258
|
+
dockerImageAssetProps: {
|
|
259
|
+
exclude: ["cdk.out", ".adapter-*", "node_modules", ".next/cache"],
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
function containerOutputs(scope, nextjsContainers) {
|
|
264
|
+
const { albFargateService } = nextjsContainers;
|
|
265
|
+
new CfnOutput(scope, "EcsClusterName", {
|
|
266
|
+
key: "EcsClusterName",
|
|
267
|
+
value: albFargateService.cluster.clusterName,
|
|
268
|
+
});
|
|
269
|
+
new CfnOutput(scope, "EcsServiceName", {
|
|
270
|
+
key: "EcsServiceName",
|
|
271
|
+
value: albFargateService.service.serviceName,
|
|
272
|
+
});
|
|
273
|
+
// Read by `e2e-deploy.sh`, which waits on it: a hotswap into ECS returns once
|
|
274
|
+
// the task is RUNNING, which is before the load balancer will route to it.
|
|
275
|
+
new CfnOutput(scope, "TargetGroupArn", {
|
|
276
|
+
key: "TargetGroupArn",
|
|
277
|
+
value: albFargateService.targetGroup.targetGroupArn,
|
|
278
|
+
});
|
|
279
|
+
const logDriver = albFargateService.taskDefinition.defaultContainer.logDriverConfig;
|
|
280
|
+
new CfnOutput(scope, "ServerLogGroupName", {
|
|
281
|
+
key: "ServerLogGroupName",
|
|
282
|
+
value: logDriver.options["awslogs-group"],
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* `NextjsGlobalContainers`: CloudFront + ALB + Fargate. Served at the
|
|
288
|
+
* distribution's origin root, like `HarnessStack`, and invalidated the same way
|
|
289
|
+
* by `e2e-deploy.sh`.
|
|
290
|
+
*/
|
|
291
|
+
class GlobalContainersHarnessStack extends Stack {
|
|
292
|
+
constructor(scope, id, props) {
|
|
293
|
+
super(scope, id, props);
|
|
294
|
+
const nextjs = new NextjsGlobalContainers(this, "Nextjs", {
|
|
295
|
+
...COMMON_PROPS,
|
|
296
|
+
healthCheckPath: HEALTH_CHECK_PATH,
|
|
297
|
+
vpc: harnessVpc(this),
|
|
298
|
+
overrides: {
|
|
299
|
+
nextjsContainers: CONTAINER_OVERRIDES,
|
|
300
|
+
nextjsPostDeploy: PINNED_POST_DEPLOY,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
acceptAnyAnswer(nextjs.nextjsContainers, fixtureBasePath(appDir));
|
|
304
|
+
new CfnOutput(this, "HarnessUrl", {
|
|
305
|
+
key: "HarnessUrl",
|
|
306
|
+
// The bare origin, for the reasons given in `HarnessStack`.
|
|
307
|
+
value: `https://${nextjs.nextjsDistribution.distribution.domainName}`,
|
|
308
|
+
});
|
|
309
|
+
new CfnOutput(this, "DistributionId", {
|
|
310
|
+
key: "DistributionId",
|
|
311
|
+
value: nextjs.nextjsDistribution.distribution.distributionId,
|
|
312
|
+
});
|
|
313
|
+
containerOutputs(this, nextjs.nextjsContainers);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* `NextjsRegionalContainers`: a public ALB + Fargate, no CloudFront. The ALB
|
|
319
|
+
* forwards every path unchanged, so - unlike `NextjsRegionalFunctions` - its URL
|
|
320
|
+
* has no stage and needs no proxy. Plain HTTP, which is what the construct
|
|
321
|
+
* deploys without a certificate.
|
|
322
|
+
*/
|
|
323
|
+
class RegionalContainersHarnessStack extends Stack {
|
|
324
|
+
constructor(scope, id, props) {
|
|
325
|
+
super(scope, id, props);
|
|
326
|
+
const nextjs = new NextjsRegionalContainers(this, "Nextjs", {
|
|
327
|
+
...COMMON_PROPS,
|
|
328
|
+
healthCheckPath: HEALTH_CHECK_PATH,
|
|
329
|
+
vpc: harnessVpc(this),
|
|
330
|
+
overrides: {
|
|
331
|
+
nextjsContainers: CONTAINER_OVERRIDES,
|
|
332
|
+
nextjsPostDeploy: PINNED_POST_DEPLOY,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
acceptAnyAnswer(nextjs.nextjsContainers, fixtureBasePath(appDir));
|
|
336
|
+
new CfnOutput(this, "HarnessUrl", {
|
|
337
|
+
key: "HarnessUrl",
|
|
338
|
+
// The ALB's bare origin - not `nextjs.url`, which appends the fixture's
|
|
339
|
+
// `basePath`, for the reasons given in `HarnessStack`.
|
|
340
|
+
value: nextjs.nextjsContainers.url,
|
|
341
|
+
});
|
|
342
|
+
containerOutputs(this, nextjs.nextjsContainers);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const STACK_CLASSES = {
|
|
347
|
+
"global-functions": HarnessStack,
|
|
348
|
+
"regional-functions": RegionalHarnessStack,
|
|
349
|
+
"global-containers": GlobalContainersHarnessStack,
|
|
350
|
+
"regional-containers": RegionalContainersHarnessStack,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const app = new App();
|
|
354
|
+
new STACK_CLASSES[nextjsType](app, stackName, {
|
|
355
|
+
stackName,
|
|
356
|
+
env: {
|
|
357
|
+
account: process.env["CDK_DEFAULT_ACCOUNT"],
|
|
358
|
+
region: process.env["CDK_DEFAULT_REGION"],
|
|
359
|
+
},
|
|
360
|
+
// A stack tag, so an orphaned stack is identifiable from CloudFormation alone
|
|
361
|
+
// long after the temporary app directory is gone. `e2e-sweep.sh` refuses to
|
|
362
|
+
// delete a stack without it. The key and value come from common.sh, which
|
|
363
|
+
// exports them, so the tag written here and the tag checked there cannot
|
|
364
|
+
// drift. Deliberately a constant: a tag whose value changed per deploy (a
|
|
365
|
+
// timestamp, say) would be a stack-level diff on every run and so a
|
|
366
|
+
// CloudFormation update, which is the one thing the hotswap path is trying to
|
|
367
|
+
// avoid. The sweeper ages stacks off their server function's (or ECS
|
|
368
|
+
// service's) last hotswap instead.
|
|
369
|
+
tags: { [required("HARNESS_TAG_KEY")]: required("HARNESS_TAG_VALUE") },
|
|
370
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared by scripts/e2e-deploy.sh, e2e-logs.sh, e2e-warm.sh and e2e-sweep.sh.
|
|
3
|
+
# Sourced, not executed.
|
|
4
|
+
|
|
5
|
+
# Files the two lifecycle scripts hand to each other. They run as separate
|
|
6
|
+
# processes with `cwd` set to the harness's temporary app, so everything one
|
|
7
|
+
# needs from another has to be on disk in that directory.
|
|
8
|
+
# See https://nextjs.org/docs/app/api-reference/adapters/testing-adapters
|
|
9
|
+
readonly HARNESS_MARKERS_FILE=".adapter-markers.log"
|
|
10
|
+
readonly HARNESS_BUILD_LOG=".adapter-build.log"
|
|
11
|
+
readonly HARNESS_DEPLOY_LOG=".adapter-deploy.log"
|
|
12
|
+
readonly HARNESS_STACK_FILE=".adapter-stack.txt"
|
|
13
|
+
readonly HARNESS_CDK_OUT=".adapter-cdk-out"
|
|
14
|
+
|
|
15
|
+
# Every stack the harness creates carries these, and `e2e-sweep.sh` refuses to
|
|
16
|
+
# delete a stack that does not. A CloudFormation stack is the only record that
|
|
17
|
+
# survives the temporary app directory, so the tag is what makes an orphan
|
|
18
|
+
# identifiable later. Exported because `app.js` writes the tag from them.
|
|
19
|
+
readonly HARNESS_TAG_KEY="cdk-nextjs:harness"
|
|
20
|
+
readonly HARNESS_TAG_VALUE="1"
|
|
21
|
+
export HARNESS_TAG_KEY HARNESS_TAG_VALUE
|
|
22
|
+
readonly HARNESS_STACK_PREFIX="hrns-"
|
|
23
|
+
|
|
24
|
+
# A stable identifier for one harness app directory.
|
|
25
|
+
#
|
|
26
|
+
# Derived from the directory rather than random so that any script can recompute
|
|
27
|
+
# it without a handoff file. The harness's directory name is already unique per
|
|
28
|
+
# test (`next-test-<ms>-<rand>`), but tests with a `subDir` end in a shared
|
|
29
|
+
# basename like `app`, so the hash is of the full path.
|
|
30
|
+
harness_app_id() {
|
|
31
|
+
local dir="$1"
|
|
32
|
+
local base
|
|
33
|
+
# `printf` rather than a bare `basename` pipe: `tr` would turn the trailing
|
|
34
|
+
# newline into another separator, giving `<name>--<hash>`.
|
|
35
|
+
base="$(printf '%s' "$(basename "$dir")" | tr -cs '[:alnum:]' '-')"
|
|
36
|
+
local hash
|
|
37
|
+
hash="$(node -e 'process.stdout.write(require("node:crypto").createHash("sha1").update(process.argv[1]).digest("hex").slice(0, 8))' "$dir")"
|
|
38
|
+
printf '%s-%s' "${base:0:60}" "$hash"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# The stack name every harness app directory deploys into.
|
|
42
|
+
#
|
|
43
|
+
# One shared stack. The harness builds a *different* app per test file, so the
|
|
44
|
+
# deploy itself cannot be skipped - but the CloudFront distribution it goes behind
|
|
45
|
+
# can be created once instead of once per file, which is where most of the
|
|
46
|
+
# wall-clock went. That only works if every test file lands in the
|
|
47
|
+
# same stack, which in turn means:
|
|
48
|
+
#
|
|
49
|
+
# - Test files must be serialized (`run-tests.js -c 1`). Two concurrent
|
|
50
|
+
# deploys into one stack would race, and the second would see the first's
|
|
51
|
+
# app.
|
|
52
|
+
# - Nothing may delete the stack between files, which is why the harness gets
|
|
53
|
+
# no NEXT_TEST_CLEANUP_SCRIPT_PATH; `e2e-sweep.sh --apply --shared` deletes
|
|
54
|
+
# it once at the end of the run.
|
|
55
|
+
# - Nothing may leak between files. Server cache entries are keyed by
|
|
56
|
+
# `CDK_NEXTJS_BUILD_ID` (src/adapter/s3-cache-handler.ts), which differs per
|
|
57
|
+
# file and hotswaps with the function, and `e2e-deploy.sh` invalidates the
|
|
58
|
+
# distribution's edge cache before it reports the URL.
|
|
59
|
+
#
|
|
60
|
+
# HARNESS_SHARED_STACK_SUFFIX is how a run gets parallel anyway: N processes,
|
|
61
|
+
# each with a shared stack of its own and its own disjoint slice of the file
|
|
62
|
+
# list (`run-tests.js -g <n>/<N>`), still serial *within* a slice. That is what
|
|
63
|
+
# .github/workflows/e2e-harness.yml's matrix does, one stack per shard. Two
|
|
64
|
+
# concurrent runs must not reuse a suffix, for the same reason two files in one
|
|
65
|
+
# shard cannot overlap.
|
|
66
|
+
#
|
|
67
|
+
# A developer debugging one file gets a stack of their own the same way
|
|
68
|
+
# (`HARNESS_SHARED_STACK_SUFFIX=dev-$USER`), and deletes it with
|
|
69
|
+
# `e2e-sweep.sh --apply --shared`.
|
|
70
|
+
#
|
|
71
|
+
# Every type but `global-functions` (HARNESS_NEXTJS_TYPE) gets its own infix
|
|
72
|
+
# after the prefix - `rf-`, `gc-`, `rc-` - so a run can never deploy into another
|
|
73
|
+
# type's shared stack: a different root construct in the same stack would be a
|
|
74
|
+
# replacement of nearly everything in it. The prefix is unchanged, so
|
|
75
|
+
# `e2e-sweep.sh` finds them all.
|
|
76
|
+
harness_stack_name() {
|
|
77
|
+
local prefix="$HARNESS_STACK_PREFIX"
|
|
78
|
+
local type
|
|
79
|
+
# Assigned on its own line so a rejected type fails this function instead of
|
|
80
|
+
# comparing as "" and quietly naming a Global stack.
|
|
81
|
+
type="$(harness_nextjs_type)" || return 1
|
|
82
|
+
case "$type" in
|
|
83
|
+
regional-functions) prefix="${prefix}rf-" ;;
|
|
84
|
+
global-containers) prefix="${prefix}gc-" ;;
|
|
85
|
+
regional-containers) prefix="${prefix}rc-" ;;
|
|
86
|
+
esac
|
|
87
|
+
printf '%s%s' "$prefix" "${HARNESS_SHARED_STACK_SUFFIX:-shared}"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Which root construct `app.js` deploys: `global-functions` (the default),
|
|
91
|
+
# `regional-functions`, `global-containers` or `regional-containers`. Exits on
|
|
92
|
+
# anything else, rather than quietly deploying the default under a name that says
|
|
93
|
+
# otherwise.
|
|
94
|
+
#
|
|
95
|
+
# When called as `$(harness_nextjs_type)`, that `exit` only ends the command
|
|
96
|
+
# substitution's subshell, which is why this file also runs it once at the top
|
|
97
|
+
# level (at the bottom), where it ends the script that sourced it.
|
|
98
|
+
harness_nextjs_type() {
|
|
99
|
+
local type="${HARNESS_NEXTJS_TYPE:-global-functions}"
|
|
100
|
+
case "$type" in
|
|
101
|
+
global-functions | regional-functions | global-containers | regional-containers)
|
|
102
|
+
printf '%s' "$type"
|
|
103
|
+
;;
|
|
104
|
+
*)
|
|
105
|
+
echo "harness: HARNESS_NEXTJS_TYPE=$type is not global-functions, regional-functions, global-containers or regional-containers" >&2
|
|
106
|
+
exit 1
|
|
107
|
+
;;
|
|
108
|
+
esac
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# The localhost port `stage-proxy.mjs` listens on for a stack. Derived from the
|
|
112
|
+
# stack name, so every script - and every shard - computes the same port for the
|
|
113
|
+
# same stack with no handoff file, and two shards' proxies do not collide.
|
|
114
|
+
harness_proxy_port() {
|
|
115
|
+
local stack="$1"
|
|
116
|
+
node -e '
|
|
117
|
+
const hash = require("node:crypto").createHash("sha1").update(process.argv[1]).digest();
|
|
118
|
+
process.stdout.write(String(40000 + (hash.readUInt16BE(0) % 10000)));
|
|
119
|
+
' "$stack"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Where a stack's proxy records its pid and target, so the next test file can
|
|
123
|
+
# reuse it. In TMPDIR rather than the app directory: the proxy outlives the app.
|
|
124
|
+
harness_proxy_state() {
|
|
125
|
+
local stack="$1"
|
|
126
|
+
printf '%s/%s-stage-proxy' "${TMPDIR:-/tmp}" "$stack"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Read one CloudFormation output off the stack, or nothing if it has none.
|
|
130
|
+
harness_stack_output() {
|
|
131
|
+
local stack="$1" key="$2"
|
|
132
|
+
local value
|
|
133
|
+
value="$(aws cloudformation describe-stacks --stack-name "$stack" \
|
|
134
|
+
--query "Stacks[0].Outputs[?OutputKey=='${key}'].OutputValue" \
|
|
135
|
+
--output text 2>/dev/null)" || return 0
|
|
136
|
+
# `--output text` prints `None` for an empty result, which is not a value.
|
|
137
|
+
[ "$value" = "None" ] && return 0
|
|
138
|
+
printf '%s' "$value"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Print the named stack's StackStatus, or nothing if CloudFormation says it
|
|
142
|
+
# does not exist. Any other failure (AccessDenied, throttling, no credentials)
|
|
143
|
+
# returns non-zero with the CLI's error on stderr, so a caller can't mistake a
|
|
144
|
+
# stack it isn't allowed to read for a stack that is missing.
|
|
145
|
+
harness_stack_status() {
|
|
146
|
+
local stack="$1"
|
|
147
|
+
local out
|
|
148
|
+
if out="$(aws cloudformation describe-stacks --stack-name "$stack" \
|
|
149
|
+
--query "Stacks[0].StackStatus" --output text 2>&1)"; then
|
|
150
|
+
printf '%s' "$out"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
case "$out" in
|
|
154
|
+
*"does not exist"*) return 0 ;;
|
|
155
|
+
esac
|
|
156
|
+
printf '%s\n' "$out" >&2
|
|
157
|
+
return 1
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Wait for an orphaned `cdk deploy` of the named stack to exit, then stop it if
|
|
161
|
+
# it has not. A deploy that outran its caller's timeout was killed, but its
|
|
162
|
+
# `cdk deploy` can outlive it - and a hotswap runs in that process, invisible to
|
|
163
|
+
# CloudFormation, so no stack status says it is still going.
|
|
164
|
+
#
|
|
165
|
+
# Bounded by HARNESS_ORPHAN_WAIT (10 minutes), then killed: the file that started
|
|
166
|
+
# it has already failed, and waiting longer only fails the next one too. The
|
|
167
|
+
# bound used to be 30 minutes with no kill, and one `cdk deploy` that hung after
|
|
168
|
+
# synth (global-containers shard 11, 2026-09-29) failed every file behind it on
|
|
169
|
+
# its 15-minute budget until the shard hit its step cap, 23 files unrun. What a
|
|
170
|
+
# kill leaves server-side, the caller's stack-status wait covers.
|
|
171
|
+
harness_wait_for_cdk() {
|
|
172
|
+
local stack="$1"
|
|
173
|
+
local waited=0
|
|
174
|
+
local limit="${HARNESS_ORPHAN_WAIT:-600}"
|
|
175
|
+
while pgrep -f -- "deploy $stack --app" >/dev/null; do
|
|
176
|
+
if [ "$waited" -ge "$limit" ]; then
|
|
177
|
+
echo "harness: an earlier cdk deploy of $stack outlived ${limit}s; stopping it" >&2
|
|
178
|
+
pkill -TERM -f -- "deploy $stack --app" || true
|
|
179
|
+
sleep 10
|
|
180
|
+
pkill -KILL -f -- "deploy $stack --app" || true
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
echo "harness: an earlier cdk deploy of $stack is still running; waiting" >&2
|
|
184
|
+
sleep 15
|
|
185
|
+
waited=$((waited + 15))
|
|
186
|
+
done
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Run a command, stopping it after the given number of seconds. Portable, since
|
|
190
|
+
# macOS has no `timeout(1)`. The watchdog writes nowhere, so a caller's pipe
|
|
191
|
+
# (`| tee`) closes when the command does, not when the watchdog's sleep ends.
|
|
192
|
+
harness_run_bounded() {
|
|
193
|
+
local seconds="$1"
|
|
194
|
+
shift
|
|
195
|
+
"$@" &
|
|
196
|
+
local pid=$!
|
|
197
|
+
(sleep "$seconds" && kill -TERM "$pid") </dev/null >/dev/null 2>&1 &
|
|
198
|
+
local watchdog=$!
|
|
199
|
+
local status=0
|
|
200
|
+
wait "$pid" || status=$?
|
|
201
|
+
pkill -P "$watchdog" 2>/dev/null || true
|
|
202
|
+
kill "$watchdog" 2>/dev/null || true
|
|
203
|
+
if [ "$status" -eq 143 ]; then
|
|
204
|
+
echo "harness: stopped after ${seconds}s: $1" >&2
|
|
205
|
+
fi
|
|
206
|
+
return "$status"
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# True when the named stack exists and carries the harness tag. Read-only, and
|
|
210
|
+
# the gate on every delete: nothing else in the account can be removed by these
|
|
211
|
+
# scripts even if a stack name is passed in by hand.
|
|
212
|
+
harness_stack_is_ours() {
|
|
213
|
+
local stack="$1"
|
|
214
|
+
local tags
|
|
215
|
+
tags="$(aws cloudformation describe-stacks --stack-name "$stack" \
|
|
216
|
+
--query "Stacks[0].Tags[?Key=='${HARNESS_TAG_KEY}'].Value" \
|
|
217
|
+
--output text 2>/dev/null)" || return 1
|
|
218
|
+
[ "$tags" = "$HARNESS_TAG_VALUE" ]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# The pid in a stack's proxy state file, if that process is still running and is
|
|
222
|
+
# still `stage-proxy.mjs`. A pid file outlives its proxy (a crash, a reboot), and
|
|
223
|
+
# the pid can by then belong to anything else of the user's, so nothing may
|
|
224
|
+
# signal or reuse it on the file's word alone.
|
|
225
|
+
harness_proxy_pid() {
|
|
226
|
+
local state pid
|
|
227
|
+
state="$(harness_proxy_state "$1")"
|
|
228
|
+
pid="$(cat "$state.pid" 2>/dev/null)" || return 1
|
|
229
|
+
case "$(ps -p "$pid" -o command= 2>/dev/null)" in
|
|
230
|
+
*stage-proxy.mjs*) printf '%s' "$pid" ;;
|
|
231
|
+
*) return 1 ;;
|
|
232
|
+
esac
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# Stop a stack's `stage-proxy.mjs`, if one is running here. Called next to every
|
|
236
|
+
# stack delete, so a proxy never outlives the API it forwards to. A no-op for a
|
|
237
|
+
# Global stack, which never has one.
|
|
238
|
+
harness_stop_proxy() {
|
|
239
|
+
local state pid
|
|
240
|
+
state="$(harness_proxy_state "$1")"
|
|
241
|
+
if pid="$(harness_proxy_pid "$1")"; then
|
|
242
|
+
kill "$pid" 2>/dev/null || true
|
|
243
|
+
fi
|
|
244
|
+
rm -f "$state.pid" "$state.target"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Validate HARNESS_NEXTJS_TYPE as soon as any harness script sources this file.
|
|
248
|
+
# Not in a subshell, so `harness_nextjs_type`'s `exit 1` ends the sourcing script.
|
|
249
|
+
harness_nextjs_type >/dev/null
|