cdk-nextjs 0.7.7 → 0.7.9
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 +2 -2
- package/docs/adapter-runtime.md +1 -1
- package/docs/harness-coverage.md +153 -73
- package/lib/adapter/adapter.mjs +12 -3
- package/lib/adapter/aws-cache-store.d.ts +7 -0
- package/lib/adapter/aws-cache-store.js +12 -1
- package/lib/adapter/aws-cache-store.test.js +32 -1
- package/lib/adapter/cache-handler.d.ts +5 -3
- package/lib/adapter/cache-handler.js +16 -10
- package/lib/adapter/cache-handler.mjs +103 -26
- package/lib/adapter/cache-handler.test.js +44 -1
- package/lib/adapter/cache-utils.d.ts +22 -0
- package/lib/adapter/cache-utils.js +44 -1
- package/lib/adapter/function-groups.js +17 -4
- package/lib/adapter/function-groups.test.js +33 -1
- package/lib/adapter/memory-cache-handler.d.ts +5 -1
- package/lib/adapter/memory-cache-handler.js +6 -2
- package/lib/adapter/s3-cache-handler.d.ts +12 -1
- package/lib/adapter/s3-cache-handler.js +30 -8
- package/lib/adapter/s3-cache-handler.test.js +84 -2
- package/lib/adapter/use-cache-common.d.ts +13 -0
- package/lib/adapter/use-cache-common.js +22 -6
- package/lib/adapter/use-cache-common.test.js +37 -1
- package/lib/adapter/use-cache-default-handler.mjs +33 -10
- package/lib/adapter/use-cache-remote-handler.mjs +33 -12
- package/lib/nextjs-api.js +1 -1
- package/lib/nextjs-build/nextjs-build.js +1 -1
- package/lib/nextjs-cache.js +1 -1
- package/lib/nextjs-compute/nextjs-containers.js +1 -1
- package/lib/nextjs-compute/nextjs-functions.js +1 -1
- package/lib/nextjs-distribution.js +1 -1
- package/lib/nextjs-post-deploy.js +1 -1
- package/lib/nextjs-static-assets.js +1 -1
- package/lib/root-constructs/nextjs-base-construct.js +1 -1
- package/lib/root-constructs/nextjs-global-containers.js +1 -1
- package/lib/root-constructs/nextjs-global-functions.js +1 -1
- package/lib/root-constructs/nextjs-regional-containers.js +1 -1
- package/lib/root-constructs/nextjs-regional-functions.js +1 -1
- package/lib/runtime/core.js +3 -1
- package/lib/runtime/image-utils.d.ts +8 -9
- package/lib/runtime/image-utils.js +16 -1
- package/lib/runtime/image-utils.test.js +13 -1
- package/lib/runtime/image.js +9 -3
- package/lib/runtime/lambda.mjs +50 -15
- package/lib/runtime/server.mjs +50 -15
- package/lib/runtime/tag-manifest.d.ts +25 -0
- package/lib/runtime/tag-manifest.js +37 -2
- package/lib/runtime/tag-manifest.test.js +27 -1
- package/package.json +3 -3
- package/scripts/e2e-deploy.sh +7 -2
- package/scripts/e2e-harness/README.md +72 -35
- package/scripts/e2e-harness/screen.mjs +148 -7
- package/scripts/playwright-install-deps.sh +40 -0
|
@@ -6,9 +6,10 @@ write, because the tests were written by the people who define the behavior.
|
|
|
6
6
|
|
|
7
7
|
`examples/e2e-tests/` remains the per-commit gate on all four `NextjsType`s. This
|
|
8
8
|
is the scheduled one — every sixth day of the month at 14:00 UTC, so the day of
|
|
9
|
-
the week drifts — on
|
|
10
|
-
`workflow_dispatch` (its `nextjs_type` input
|
|
11
|
-
|
|
9
|
+
the week drifts — on all four types in one run, `NextjsGlobalFunctions` first
|
|
10
|
+
(see "Sharding"). A `workflow_dispatch` runs one type (its `nextjs_type` input,
|
|
11
|
+
`NextjsGlobalFunctions` by default) or `all`; see "Running on the other
|
|
12
|
+
`NextjsType`s".
|
|
12
13
|
|
|
13
14
|
What has actually been run, what failed, and whether each failure is a bug or
|
|
14
15
|
acceptable: [`docs/harness-coverage.md`](../../docs/harness-coverage.md).
|
|
@@ -26,7 +27,8 @@ acceptable: [`docs/harness-coverage.md`](../../docs/harness-coverage.md).
|
|
|
26
27
|
| `scripts/e2e-harness/common.sh` | Shared file names, stack naming, output reads, and the tag check that gates every delete. |
|
|
27
28
|
| `.github/actions/build-nextjs` | Checks out and builds vercel/next.js. The cache-miss path, shared by the `nextjs` job and a shard's fallback. |
|
|
28
29
|
| `test/deploy-tests-manifest.json` | Which next.js test files run (`NEXT_EXTERNAL_TESTS_FILTERS`). |
|
|
29
|
-
|
|
|
30
|
+
| `test/harness-smoke-manifest.json` | One file every type passes, for checking the workflow itself in about an hour (`test_filters`). |
|
|
31
|
+
| `.github/workflows/e2e-harness.yml` | Every six days (all four types) + `workflow_dispatch`. A matrix of (type, shard) jobs, one stack each. |
|
|
30
32
|
|
|
31
33
|
## One shared stack, not one per test file
|
|
32
34
|
|
|
@@ -36,8 +38,8 @@ one deployment — every file genuinely has different code to ship. What there i
|
|
|
36
38
|
way to do is ship it into infrastructure that already exists.
|
|
37
39
|
|
|
38
40
|
Every test file in a run deploys into the same stack (`hrns-shared` by default,
|
|
39
|
-
`hrns-shard-<n
|
|
40
|
-
`cdk deploy --hotswap-fallback`. Creating and propagating the CloudFront
|
|
41
|
+
`hrns-shard-<n>`, with the type's infix, under the workflow's matrix — see
|
|
42
|
+
"Sharding" below) with `cdk deploy --hotswap-fallback`. Creating and propagating the CloudFront
|
|
41
43
|
distribution costs ~4 minutes once per stack; every test file reuses it. That is
|
|
42
44
|
the whole saving, and it is most of the cost of a run.
|
|
43
45
|
|
|
@@ -141,9 +143,31 @@ of them at once locally" — and delete it afterwards with
|
|
|
141
143
|
|
|
142
144
|
A stack holds one app at a time, so the only way to run two test files at once is
|
|
143
145
|
to give them two stacks. `HARNESS_SHARED_STACK_SUFFIX` is that knob, and
|
|
144
|
-
`.github/workflows/e2e-harness.yml` turns it into a matrix
|
|
145
|
-
(
|
|
146
|
-
|
|
146
|
+
`.github/workflows/e2e-harness.yml` turns it into a matrix of (type, shard) jobs:
|
|
147
|
+
`shard_total` per type (by default 15 for Global Containers, whose ~4.3 minutes a
|
|
148
|
+
file do not fit 10 shards into the run step's cap, and 10 for the others), each
|
|
149
|
+
with a shared stack named `hrns-shard-<n>` (with the type's infix) and each
|
|
150
|
+
running its own slice of that type's file list.
|
|
151
|
+
|
|
152
|
+
A run of one type starts all its shards at once. A run of `all` (the schedule)
|
|
153
|
+
lists 45 jobs, Global Functions first, then Regional Functions, Regional
|
|
154
|
+
Containers and Global Containers, and runs at most 10 at a time, which is about 11
|
|
155
|
+
hours end to end (about 95 shard-hours: Global Containers' 15 shards take about
|
|
156
|
+
2.5 hours each, the others about 2; the shards of one type end together, so the
|
|
157
|
+
last five Global Containers shards run with half the slots idle). Ten is the peak
|
|
158
|
+
a single-type run of the other three already reaches: no more CloudFront
|
|
159
|
+
distributions at once than a Global Functions run, under the CloudFront
|
|
160
|
+
response-headers-policy quota (3 per distribution), no more ECR pulls than a
|
|
161
|
+
Regional Containers run, and no more of the org's concurrent jobs than the
|
|
162
|
+
schedule has always taken. A slot goes to the next job as soon as one frees, so
|
|
163
|
+
types overlap where one ends and the next begins; that is safe, because no two
|
|
164
|
+
entries share a stack name. Results arrive in that order: a Global Functions
|
|
165
|
+
regression shows up first, about 2 hours in.
|
|
166
|
+
|
|
167
|
+
To check the workflow itself rather than compatibility, dispatch `all` with
|
|
168
|
+
`shard_total: 1` and `test_filters: ../test/harness-smoke-manifest.json`: one
|
|
169
|
+
file on each type, four jobs, about an hour (Global Containers' ~15-minute warm-up
|
|
170
|
+
and its delete are most of it).
|
|
147
171
|
|
|
148
172
|
The slice comes from next.js's own `run-tests.js -g <n>/<N>`, which splits
|
|
149
173
|
**after** `NEXT_EXTERNAL_TESTS_FILTERS` has been applied — `run-tests.js` filters
|
|
@@ -158,9 +182,10 @@ prints per-file durations to build one from.
|
|
|
158
182
|
|
|
159
183
|
Each shard is self-contained, which is what makes this safe:
|
|
160
184
|
|
|
161
|
-
- `e2e-warm.sh` warms _its_ stack, so the ~4-minute distribution create
|
|
162
|
-
once per shard but in parallel — the same ~4 minutes of
|
|
163
|
-
shards
|
|
185
|
+
- `e2e-warm.sh` warms _its_ stack, so the ~4-minute distribution create (Global
|
|
186
|
+
types only) is paid once per shard but in parallel — the same ~4 minutes of
|
|
187
|
+
wall clock however many shards start together (in an `all` run, once per wave
|
|
188
|
+
of 10 Global shards).
|
|
164
189
|
- `e2e-sweep.sh --apply --shared` in an `always()` step deletes _its_ stack, since
|
|
165
190
|
`--shared` resolves the same suffix. Nothing lowers the age floor account-wide,
|
|
166
191
|
so one shard finishing early cannot delete another's stack out from under it.
|
|
@@ -179,9 +204,10 @@ workflow's `concurrency` group is what guarantees that, and it queues rather tha
|
|
|
179
204
|
cancels. It is one group for every type, not one per type: each run's
|
|
180
205
|
account-wide sweep could otherwise delete another type's idle-looking shard stack
|
|
181
206
|
as that run warms into it. GitHub keeps only one _pending_ run per group, so
|
|
182
|
-
dispatch a second type once the first has started, not while one is queued
|
|
183
|
-
|
|
184
|
-
|
|
207
|
+
dispatch a second type once the first has started, not while one is queued, or
|
|
208
|
+
dispatch `all`. A dispatch on a schedule day waits behind the schedule's ~11
|
|
209
|
+
hours. A pending run that gets cancelled that way, the scheduled one included,
|
|
210
|
+
loses no sweep: every run sweeps, not just the scheduled one.
|
|
185
211
|
|
|
186
212
|
## Caching the next.js build
|
|
187
213
|
|
|
@@ -239,8 +265,9 @@ the `v1` in it to discard every entry). Two things to know:
|
|
|
239
265
|
comparing against is whatever the other workflows are using at the time.
|
|
240
266
|
- **The Playwright _browser_ is cached; its system libraries are not.** Those are
|
|
241
267
|
apt packages outside any cacheable path, so
|
|
242
|
-
`playwright install
|
|
243
|
-
|
|
268
|
+
`scripts/playwright-install-deps.sh` (`playwright install-deps chromium`)
|
|
269
|
+
still runs in each shard. It caps and retries each attempt, because apt on its
|
|
270
|
+
own can hang on a stalled mirror until the job times out.
|
|
244
271
|
|
|
245
272
|
A `workflow_dispatch` from a branch writes to that branch's own cache scope, so
|
|
246
273
|
the first dispatched run on a new branch pays the build once. It can still _read_
|
|
@@ -275,16 +302,16 @@ last. A session that must have its own working tree wants a `git worktree` _and_
|
|
|
275
302
|
its own next.js checkout _and_ its own stack suffix; three of those is usually the
|
|
276
303
|
point at which running one at a time is the cheaper answer.
|
|
277
304
|
|
|
278
|
-
## Why
|
|
305
|
+
## Why `NextjsGlobalFunctions` is the default
|
|
279
306
|
|
|
280
|
-
All four types run
|
|
281
|
-
|
|
282
|
-
`getFullUrl` in `test/lib/next-test-utils.ts`
|
|
283
|
-
prefix in the deployment URL is dropped.
|
|
284
|
-
REST URL always ends in `/<stage>`, so it
|
|
285
|
-
proxy that puts the stage back (see
|
|
286
|
-
second front door, not the one users
|
|
287
|
-
at the origin root.
|
|
307
|
+
All four types run on the schedule (see "Sharding"); this one runs first, and is
|
|
308
|
+
what a dispatch runs unless told otherwise. The harness builds every request URL
|
|
309
|
+
as `new URL(path, deploymentUrl)` — `getFullUrl` in `test/lib/next-test-utils.ts`
|
|
310
|
+
assigns `pathname` outright — so any prefix in the deployment URL is dropped.
|
|
311
|
+
`NextjsRegionalFunctions`'s API Gateway REST URL always ends in `/<stage>`, so it
|
|
312
|
+
runs behind `stage-proxy.mjs`, a local proxy that puts the stage back (see
|
|
313
|
+
"Running on `NextjsRegionalFunctions`") — a second front door, not the one users
|
|
314
|
+
deploy. A CloudFront distribution is served at the origin root.
|
|
288
315
|
|
|
289
316
|
It is also the front door the suite was written for. `NEXT_TEST_MODE=deploy` is
|
|
290
317
|
the mode Vercel validates edge-fronted deployments with, so its tests tolerate a
|
|
@@ -308,17 +335,18 @@ Two caveats worth knowing before reading a failure as a regression:
|
|
|
308
335
|
limitation documented at `src/nextjs-distribution.ts`. A test that varies on a
|
|
309
336
|
header outside that list can be served a wrong cached response. That is a real
|
|
310
337
|
product limitation, not a harness artifact — and not a regression either.
|
|
311
|
-
- The other three types
|
|
312
|
-
|
|
338
|
+
- The schedule runs the other three types too, after this one, so their results
|
|
339
|
+
land hours later in the same run; see "Sharding".
|
|
313
340
|
|
|
314
341
|
## Running on the other `NextjsType`s
|
|
315
342
|
|
|
316
343
|
`HARNESS_NEXTJS_TYPE` picks the root construct: `global-functions` (the default),
|
|
317
344
|
`regional-functions`, `global-containers` or `regional-containers`. Each type but
|
|
318
345
|
the default gets a stack infix of its own — `hrns-rf-*`, `hrns-gc-*`,
|
|
319
|
-
`hrns-rc-*` — so no run can deploy into another type's stack
|
|
320
|
-
types still queue behind each other
|
|
321
|
-
`nextjs_type` input sets
|
|
346
|
+
`hrns-rc-*` — so no run can deploy into another type's stack, and shards of all
|
|
347
|
+
four types can share one run. In CI, two *runs* still queue behind each other
|
|
348
|
+
(see "Sharding"). The workflow's `nextjs_type` input sets the type, or `all`
|
|
349
|
+
runs every type, as the schedule does.
|
|
322
350
|
|
|
323
351
|
```bash
|
|
324
352
|
ADAPTER_DIR=$PWD HARNESS_NEXTJS_TYPE=global-containers ./scripts/e2e-warm.sh
|
|
@@ -396,7 +424,7 @@ reason alone.
|
|
|
396
424
|
|
|
397
425
|
`HARNESS_NEXTJS_TYPE=regional-functions` deploys `NextjsRegionalFunctions`
|
|
398
426
|
instead, into `hrns-rf-<suffix>` stacks so it can never land in a Global run's.
|
|
399
|
-
By hand
|
|
427
|
+
By hand, by `workflow_dispatch`, or on the schedule with the other three types.
|
|
400
428
|
|
|
401
429
|
The stage is the whole problem, and `stage-proxy.mjs` is the whole answer. The
|
|
402
430
|
suite discards any path in the deployment URL (above), and a REST API's is always
|
|
@@ -464,7 +492,8 @@ To check one candidate by hand instead:
|
|
|
464
492
|
```bash
|
|
465
493
|
# in the next.js checkout, against the fixture root (usually the test file's dir
|
|
466
494
|
# or its parent)
|
|
467
|
-
|
|
495
|
+
# a middleware.* is edge unless it sets `runtime: "nodejs"` in its config
|
|
496
|
+
find <fixture> -name "middleware.*" -exec grep -LE "runtime\s*:\s*.nodejs." {} +
|
|
468
497
|
grep -rlE "runtime\s*[:=]\s*.(experimental-)?edge." <fixture>
|
|
469
498
|
```
|
|
470
499
|
|
|
@@ -476,7 +505,7 @@ Mind the "or its parent": for a `test/e2e/<name>/test/index.test.ts` the fixture
|
|
|
476
505
|
lives a directory _above_ the test file, and screening only the test file's own
|
|
477
506
|
directory quietly misses its `middleware.js`.
|
|
478
507
|
|
|
479
|
-
|
|
508
|
+
Seven more screens are worth running before spending a deploy on a candidate, all
|
|
480
509
|
against the test file rather than the fixture:
|
|
481
510
|
|
|
482
511
|
- `skipDeployment: true` in the `nextTestSetup` call — next.js replaces the whole
|
|
@@ -484,6 +513,13 @@ against the test file rather than the fixture:
|
|
|
484
513
|
early-returns (`test/lib/e2e-utils/index.ts`). The costliest screen to have been
|
|
485
514
|
missing: it accounts for a third of what was previously counted as the candidate
|
|
486
515
|
pool, and two files reached `rules.include` on the strength of a 4s "pass".
|
|
516
|
+
- `// @force-gate <expr>` on every top-level `describe`, where `<expr>` is false
|
|
517
|
+
in deploy mode — next.js 16.4's spelling of the same exclusion, which replaced
|
|
518
|
+
most `skipDeployment: true` calls. A gated-out suite is skipped, so a file whose
|
|
519
|
+
every suite is gated out "passes" having deployed nothing. `screen.mjs`
|
|
520
|
+
evaluates the expression with the harness's own conditions (`deploy`,
|
|
521
|
+
`adapter`, `turbopack`); one it cannot know up front (`cacheComponents`) never
|
|
522
|
+
disqualifies, and a file with one suite left still deploys it.
|
|
487
523
|
- `describe.skip` / `(isNextDev ? describe : describe.skip)` — a file skipped
|
|
488
524
|
upstream reports as passing in a few seconds without deploying anything, and
|
|
489
525
|
adding it to `rules.include` claims coverage that does not exist. This screen
|
|
@@ -521,7 +557,8 @@ pass.
|
|
|
521
557
|
each screen, and how many are already included. Regenerate it with `--write`
|
|
522
558
|
whenever `next` is upgraded; `--check` exits nonzero if it has drifted, which is
|
|
523
559
|
the only thing that keeps those numbers worth quoting. The workflow runs it in
|
|
524
|
-
|
|
560
|
+
the first job of every run against the default next.js ref (Global Functions
|
|
561
|
+
shard 1 in an `all` run).
|
|
525
562
|
|
|
526
563
|
Passing every screen makes a file a _candidate_, not a pass. It still has to
|
|
527
564
|
be deployed and watched, and anything that fails gets root-caused and given a
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
*
|
|
18
18
|
* - **edge / middleware** — cdk-nextjs throws during `next build` for any
|
|
19
19
|
* non-`nodejs` runtime (`assertNodeRuntimes`), so the fixture is unbuildable
|
|
20
|
-
* and no per-case `failed` entry can rescue it.
|
|
20
|
+
* and no per-case `failed` entry can rescue it. A `middleware.*` counts as
|
|
21
|
+
* edge, its default, unless it opts into `runtime: "nodejs"`.
|
|
21
22
|
* - **skipped-upstream** — `describe.skip`, so the file reports "passing" in
|
|
22
23
|
* seconds without deploying anything. Adding it claims coverage that does not
|
|
23
24
|
* exist. This one over-reports, because the match is textual and some skips are
|
|
@@ -30,6 +31,10 @@
|
|
|
30
31
|
* The costliest screen to have been missing — it covers 236 of what were
|
|
31
32
|
* otherwise counted as candidates, and two files had already been added to
|
|
32
33
|
* `rules.include` on the strength of a 4s "pass".
|
|
34
|
+
* - **force-gate** — next.js 16.4's spelling of `skipDeployment`:
|
|
35
|
+
* `// @force-gate !deploy` on a `describe`, which skips the suite. Only a file
|
|
36
|
+
* whose every top-level suite is gated out in deploy mode is disqualified,
|
|
37
|
+
* since one that keeps a suite still deploys it.
|
|
33
38
|
* - **mode-gated** — the third spelling of the same trap, and the one that cost a
|
|
34
39
|
* deploy slot to find: the file's real cases sit inside `if (isNextStart)` or
|
|
35
40
|
* `if (isNextDev)`, and every other mode gets an empty
|
|
@@ -75,6 +80,11 @@ const TEST_RE = /\.test\.[tj]sx?$/;
|
|
|
75
80
|
* first undercounts by ~17 files.
|
|
76
81
|
*/
|
|
77
82
|
const EDGE_RUNTIME_RE = /runtime\s*[:=]\s*['"](experimental-)?edge['"]/;
|
|
83
|
+
/** A middleware file's opt-in to the Node runtime: `config = { runtime: "nodejs" }`. */
|
|
84
|
+
const NODE_RUNTIME_RE = /runtime\s*:\s*['"]nodejs['"]/;
|
|
85
|
+
/** `source` less its comments, so a commented-out opt-in doesn't count. */
|
|
86
|
+
// A block comment only where a line starts one: `/*` is common inside a matcher.
|
|
87
|
+
const withoutComments = (source) => source.replace(/^\s*\/\*[\s\S]*?\*\/|(^|[^:])\/\/.*$/gm, "$1");
|
|
78
88
|
/**
|
|
79
89
|
* An empty `it('should skip …', () => {})` — next.js's stub for the modes a
|
|
80
90
|
* mode-gated file does not cover. `dev` in the title means dev is the excluded
|
|
@@ -83,6 +93,124 @@ const EDGE_RUNTIME_RE = /runtime\s*[:=]\s*['"](experimental-)?edge['"]/;
|
|
|
83
93
|
const MODE_GATED_STUB_RE =
|
|
84
94
|
/it\(\s*(['"`])should skip (?!.*\bdev\b)[^'"`]*\1\s*,\s*\(\s*\)\s*=>\s*\{\s*\}\s*\)/;
|
|
85
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The `test/lib/gate/conditions.ts` values the harness runs under:
|
|
98
|
+
* `NEXT_TEST_MODE=deploy`, `IS_TURBOPACK_TEST=1` and `NEXT_ENABLE_ADAPTER=1`
|
|
99
|
+
* (`.github/workflows/e2e-harness.yml`), and none of the other env flags. A
|
|
100
|
+
* condition missing here (`cacheComponents`, read off the fixture's config) is
|
|
101
|
+
* unknown, which never disqualifies.
|
|
102
|
+
*/
|
|
103
|
+
const DEPLOY_GATE_CONDITIONS = {
|
|
104
|
+
mode: "deploy",
|
|
105
|
+
dev: false,
|
|
106
|
+
start: false,
|
|
107
|
+
deploy: true,
|
|
108
|
+
prod: true,
|
|
109
|
+
prefetching: true,
|
|
110
|
+
bundler: "turbopack",
|
|
111
|
+
turbopack: true,
|
|
112
|
+
rspack: false,
|
|
113
|
+
webpack: false,
|
|
114
|
+
adapter: true,
|
|
115
|
+
nodeMiddleware: false,
|
|
116
|
+
standaloneOutput: false,
|
|
117
|
+
turbopackDev: false,
|
|
118
|
+
turbopackBuild: false,
|
|
119
|
+
react18: false,
|
|
120
|
+
wasm: false,
|
|
121
|
+
FIXME: false,
|
|
122
|
+
TODO: false,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A `@force-gate` expression (`test/lib/gate/expr.ts`'s grammar: `!`, `&&`,
|
|
127
|
+
* `||`, `==`/`!=`/`===`/`!==`, parentheses, names, quoted strings, booleans)
|
|
128
|
+
* under {@link DEPLOY_GATE_CONDITIONS}: `true`, `false`, or `undefined` when it
|
|
129
|
+
* turns on a condition that isn't known up front.
|
|
130
|
+
*/
|
|
131
|
+
function evaluateGate(source) {
|
|
132
|
+
const tokens = source.match(/&&|\|\||[!=]==?|[!()]|'[^']*'|"[^"]*"|[A-Za-z_$][\w$]*/g) ?? [];
|
|
133
|
+
let i = 0;
|
|
134
|
+
// Condition values are coerced by truthiness, as next.js does (`mode` is a string).
|
|
135
|
+
const truth = (value) => (value === undefined ? undefined : Boolean(value));
|
|
136
|
+
const and = (a, b) => {
|
|
137
|
+
const [x, y] = [truth(a), truth(b)];
|
|
138
|
+
return x === false || y === false ? false : x && y ? true : undefined;
|
|
139
|
+
};
|
|
140
|
+
const or = (a, b) => {
|
|
141
|
+
const [x, y] = [truth(a), truth(b)];
|
|
142
|
+
return x === true || y === true ? true : x === false && y === false ? false : undefined;
|
|
143
|
+
};
|
|
144
|
+
const primary = () => {
|
|
145
|
+
const token = tokens[i++];
|
|
146
|
+
if (token === "(") {
|
|
147
|
+
const value = expression();
|
|
148
|
+
i++; // ")"
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
if (token === "!") {
|
|
152
|
+
const value = primary();
|
|
153
|
+
return value === undefined ? undefined : !value;
|
|
154
|
+
}
|
|
155
|
+
if (token === "true" || token === "false") return token === "true";
|
|
156
|
+
if (/^['"]/.test(token)) return token.slice(1, -1);
|
|
157
|
+
return Object.hasOwn(DEPLOY_GATE_CONDITIONS, token) ? DEPLOY_GATE_CONDITIONS[token] : undefined;
|
|
158
|
+
};
|
|
159
|
+
const binary = () => {
|
|
160
|
+
let left = primary();
|
|
161
|
+
while (/^[!=]==?$/.test(tokens[i] ?? "")) {
|
|
162
|
+
const negate = tokens[i++].startsWith("!");
|
|
163
|
+
const right = primary();
|
|
164
|
+
left = left === undefined || right === undefined ? undefined : (left === right) !== negate;
|
|
165
|
+
}
|
|
166
|
+
return left;
|
|
167
|
+
};
|
|
168
|
+
// `&&` and `||` share one precedence level, left to right, as in expr.ts.
|
|
169
|
+
const expression = () => {
|
|
170
|
+
let left = binary();
|
|
171
|
+
while (tokens[i] === "&&" || tokens[i] === "||") {
|
|
172
|
+
const op = tokens[i++];
|
|
173
|
+
const right = binary();
|
|
174
|
+
left = op === "&&" ? and(left, right) : or(left, right);
|
|
175
|
+
}
|
|
176
|
+
return left;
|
|
177
|
+
};
|
|
178
|
+
return truth(expression());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Whether `@force-gate` pragmas skip every top-level suite in `text` under
|
|
183
|
+
* deploy mode. next.js 16.4 moved deploy exclusions from
|
|
184
|
+
* `nextTestSetup({ skipDeployment: true })` to `// @force-gate !deploy` on the
|
|
185
|
+
* suite, and a gated-out suite is skipped rather than failed: a file whose every
|
|
186
|
+
* suite is gated out reports a pass having deployed nothing, the
|
|
187
|
+
* `skipDeployment` trap again. A file with one suite left still deploys it, so
|
|
188
|
+
* it stays a candidate.
|
|
189
|
+
*/
|
|
190
|
+
function forceGatedOutOfDeploy(text) {
|
|
191
|
+
const lines = text.split("\n");
|
|
192
|
+
// The outermost suites, a top-level `it`/`test` among them: column 0 usually,
|
|
193
|
+
// but some files declare theirs inside a loop (`filesystem-cache` does, in a
|
|
194
|
+
// `for (const cacheEnabled of …)`). A `.skip` or `.todo` never runs, so it is
|
|
195
|
+
// no suite left to deploy.
|
|
196
|
+
const indentOf = (line) => /^(\s*)(?:describe|it|test)\b(?!\.(?:skip|todo)\b)/.exec(line)?.[1].length;
|
|
197
|
+
const outer = Math.min(...lines.flatMap((line) => indentOf(line) ?? []));
|
|
198
|
+
let suites = 0;
|
|
199
|
+
let gatedOut = 0;
|
|
200
|
+
for (let index = 0; index < lines.length; index++) {
|
|
201
|
+
if (indentOf(lines[index]) !== outer) continue;
|
|
202
|
+
suites++;
|
|
203
|
+
for (let above = index - 1; above >= 0 && /^\s*\/\//.test(lines[above]); above--) {
|
|
204
|
+
const pragma = /^\s*\/\/\s*@force-gate\s+(.+?)\s*$/.exec(lines[above]);
|
|
205
|
+
if (pragma && evaluateGate(pragma[1]) === false) {
|
|
206
|
+
gatedOut++;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return suites > 0 && gatedOut === suites;
|
|
212
|
+
}
|
|
213
|
+
|
|
86
214
|
const args = process.argv.slice(2);
|
|
87
215
|
const flag = (name) => args.includes(`--${name}`);
|
|
88
216
|
const value = (name) => {
|
|
@@ -175,7 +303,15 @@ function screen(testFile, decided = []) {
|
|
|
175
303
|
const fixture = walk(fixtureRoot(testFile));
|
|
176
304
|
const sources = fixture.filter((p) => SOURCE_RE.test(p));
|
|
177
305
|
|
|
178
|
-
|
|
306
|
+
// `middleware.*` builds for the edge runtime unless it exports
|
|
307
|
+
// `config = { runtime: "nodejs" }`, which cdk-nextjs deploys like `proxy.*`.
|
|
308
|
+
if (
|
|
309
|
+
fixture.some(
|
|
310
|
+
(p) =>
|
|
311
|
+
/\/middleware\.[tj]sx?$/.test(p) &&
|
|
312
|
+
!NODE_RUNTIME_RE.test(withoutComments(read(p))),
|
|
313
|
+
)
|
|
314
|
+
) {
|
|
179
315
|
reasons.push("middleware");
|
|
180
316
|
}
|
|
181
317
|
if (sources.some((p) => EDGE_RUNTIME_RE.test(read(p)))) reasons.push("edge");
|
|
@@ -189,13 +325,18 @@ function screen(testFile, decided = []) {
|
|
|
189
325
|
// early-returns, and jest reports it as passing in ~4s having deployed
|
|
190
326
|
// nothing. next.js declaring a file out of scope for deploy mode is the same
|
|
191
327
|
// signal as `isNextDeploy`, just spelled in the setup call.
|
|
192
|
-
// Read through relative imports for this
|
|
193
|
-
// in a shared helper replaces every file that calls it,
|
|
194
|
-
// cannot over-report; the mode gates above can, since a
|
|
195
|
-
// branch may cover only some of a file's cases.
|
|
196
|
-
|
|
328
|
+
// Read through relative imports for this screen and the `@force-gate` one.
|
|
329
|
+
// `skipDeployment: true` in a shared helper replaces every file that calls it,
|
|
330
|
+
// so following the import cannot over-report; the mode gates above can, since a
|
|
331
|
+
// helper's `isNextDev` branch may cover only some of a file's cases. A helper's
|
|
332
|
+
// suites are the file's suites, so `@force-gate` counts them too.
|
|
333
|
+
const sourceText = testSourceText(testFile);
|
|
334
|
+
if (/skipDeployment:\s*true/.test(sourceText)) {
|
|
197
335
|
reasons.push("skipDeployment");
|
|
198
336
|
}
|
|
337
|
+
if (forceGatedOutOfDeploy(sourceText)) {
|
|
338
|
+
reasons.push("force-gate");
|
|
339
|
+
}
|
|
199
340
|
// `test/e2e/**/test-template/{{ toFileName name }}/…` is a scaffold for
|
|
200
341
|
// `pnpm new-test`, not a test.
|
|
201
342
|
if (testFile.includes("{{")) reasons.push("scaffold");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Installs Playwright's system packages (`playwright install-deps chromium`)
|
|
4
|
+
# with apt network timeouts and bounded retries. CI only: it runs apt as root
|
|
5
|
+
# and writes to /etc/apt/apt.conf.d. Run it from the directory whose
|
|
6
|
+
# node_modules has Playwright, then install the browser itself unprivileged
|
|
7
|
+
# with `playwright install chromium`.
|
|
8
|
+
#
|
|
9
|
+
# apt has no overall deadline: a mirror that stops answering partway through a
|
|
10
|
+
# fetch can hang `apt-get update` indefinitely. On 2026-10-07 two harness
|
|
11
|
+
# shards hung right after fetching noble-security InRelease, one for two hours.
|
|
12
|
+
# Each attempt is capped and retried instead.
|
|
13
|
+
#
|
|
14
|
+
# The cap runs inside sudo, not around it. sudo starts its command in a new
|
|
15
|
+
# session, so a `timeout` outside it kills sudo but not the apt-get under it,
|
|
16
|
+
# which keeps holding /var/lib/apt/lists/lock and fails every retry on it.
|
|
17
|
+
#
|
|
18
|
+
# Usage: scripts/playwright-install-deps.sh
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
ATTEMPTS="${PLAYWRIGHT_INSTALL_ATTEMPTS:-3}"
|
|
23
|
+
# Normal runs take 15 s to 4 min (2026-10-07).
|
|
24
|
+
ATTEMPT_TIMEOUT="${PLAYWRIGHT_INSTALL_TIMEOUT:-420}"
|
|
25
|
+
|
|
26
|
+
sudo tee /etc/apt/apt.conf.d/99cdk-nextjs-timeouts > /dev/null << 'EOF'
|
|
27
|
+
Acquire::Retries "3";
|
|
28
|
+
Acquire::http::Timeout "30";
|
|
29
|
+
Acquire::https::Timeout "30";
|
|
30
|
+
EOF
|
|
31
|
+
|
|
32
|
+
for attempt in $(seq 1 "$ATTEMPTS"); do
|
|
33
|
+
# sudo resets PATH; playwright's shim needs node.
|
|
34
|
+
if sudo env "PATH=$PATH" timeout --kill-after=30 "$ATTEMPT_TIMEOUT" \
|
|
35
|
+
./node_modules/.bin/playwright install-deps chromium; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
echo "playwright-install-deps: attempt $attempt/$ATTEMPTS failed or exceeded ${ATTEMPT_TIMEOUT}s" >&2
|
|
39
|
+
done
|
|
40
|
+
exit 1
|