@nx/playwright 23.2.0-beta.10 → 23.2.0-beta.12

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.
@@ -0,0 +1,117 @@
1
+ import type { PlaywrightTestConfig } from '@playwright/test';
2
+ /**
3
+ * The serializable subset of a Playwright `webServer` entry that the readiness
4
+ * gate needs. Passed back from the config-eval child over IPC.
5
+ */
6
+ export interface ResolvedWebServer {
7
+ command: string;
8
+ url?: string;
9
+ port?: number;
10
+ reuseExistingServer?: boolean;
11
+ ignoreHTTPSErrors?: boolean;
12
+ timeout?: number;
13
+ waitsForOutput?: boolean;
14
+ hasEnv?: boolean;
15
+ }
16
+ export declare function normalizeWebServers(webServer: PlaywrightTestConfig['webServer']): ResolvedWebServer[];
17
+ /** The values of the probe variables set in an env. */
18
+ export type ProbeEnv = Record<string, string>;
19
+ export declare function pickProbeEnv(env: NodeJS.ProcessEnv): ProbeEnv;
20
+ /**
21
+ * The webServer entries a config evaluation resolved and the probe env it left
22
+ * behind, which is what Playwright's own probe runs under: the config runs
23
+ * before the probe and can write env (through `dotenv`, say) that the readiness
24
+ * task, its own target in its own process, never sees.
25
+ */
26
+ export interface ConfigEvaluation {
27
+ webServers: ResolvedWebServer[];
28
+ probeEnv: ProbeEnv;
29
+ }
30
+ /**
31
+ * Loads a Playwright config in this process, hands it to `consume`, and
32
+ * returns the consumed value with the probe env the evaluation left in
33
+ * `process.env`, then puts `process.env` back the way it was: the task and
34
+ * gate dotenv files expand against it, and a later env comparison has to see
35
+ * the graph-time env, not whatever the last config wrote. `consume` must be
36
+ * synchronous and must not let the config object escape: a config can expose
37
+ * getters that read env it set while loading, so every read has to happen
38
+ * before the restore. Loads are serialized because createNodes evaluates
39
+ * configs concurrently and a concurrent load's writes would be misattributed.
40
+ * A client env applied mid-load (the daemon forwards it as it arrives) is
41
+ * re-applied over the restored env; restoring the pre-load values alone would
42
+ * revert it.
43
+ *
44
+ * `ambientEnv` is a snapshot of the restored env, taken while still holding
45
+ * the load lock. Task-env reconstruction and comparison after this returns
46
+ * must base on it rather than on the live `process.env`: the lock is released
47
+ * on return, so a sibling load's transient writes can be visible in the live
48
+ * env by then and would be read as ambient.
49
+ */
50
+ export declare function loadConfigWithProbeEnv<T extends object, R>(configPath: string, consume: (config: T) => R): Promise<{
51
+ consumed: R;
52
+ probeEnv: ProbeEnv;
53
+ ambientEnv: NodeJS.ProcessEnv;
54
+ }>;
55
+ /**
56
+ * The env var names whose values would make the readiness gate probe `servers`
57
+ * differently than the consuming task's own Playwright probe: `taskEnv` is the
58
+ * env that probe runs under (the task env after its config loaded) and
59
+ * `gateEnv` the env the gate target runs under. The gate is its own target, so
60
+ * it loads its own dotenv files, not the consumer's, and it never loads the
61
+ * config: a task-scoped or config-written proxy exclusion or CA bundle never
62
+ * reaches it, and a gate probing through the wrong route can fail where
63
+ * Playwright would pass. A non-empty result means the gate cannot reproduce
64
+ * the task's probe and must not be inferred.
65
+ *
66
+ * Both probes follow redirects, so as soon as one server probes a url the
67
+ * routes for every protocol and host are compared, not only the route the
68
+ * configured url takes: an `https_proxy` for an http url or a `no_proxy` entry
69
+ * for another host can still decide how a redirect target is reached.
70
+ * `NODE_EXTRA_CA_CERTS` is compared unless every url server sets
71
+ * `ignoreHTTPSErrors`, which turns verification off on both sides for every
72
+ * hop, except that the tunnel to an https proxy is verified with the process
73
+ * defaults either way. With `legacyProxiedTls` (an installed Playwright below
74
+ * 1.59.0, whose probe never verifies the origin behind a proxy tunnel, and
75
+ * neither does the gate on that version) it is also left out while every
76
+ * https hop can only take such a tunnel: an http proxy on the https route, no
77
+ * https proxy on the http route, and no `no_proxy` entry that could send a
78
+ * redirect target direct. `NODE_TLS_REJECT_UNAUTHORIZED` only reaches that
79
+ * tunnel to an https proxy, since every request sets `rejectUnauthorized`
80
+ * itself, and only its `'0'` state counts. With `npmConfigProxy` (an installed
81
+ * Playwright below 1.59.0, whose probe reads npm's `npm_config_*` proxy
82
+ * variables ahead of the standard ones, as the gate does on that version) those
83
+ * variables route the probe and are compared too.
84
+ */
85
+ export declare function getProbeEnvDivergence(servers: Array<{
86
+ url?: string;
87
+ ignoreHTTPSErrors?: boolean;
88
+ }>, taskEnv: NodeJS.ProcessEnv, gateEnv: NodeJS.ProcessEnv, legacyProxiedTls?: boolean, npmConfigProxy?: boolean): string[];
89
+ /**
90
+ * Whether the task env a chain would run with differs from the graph-time
91
+ * ambient env. Only a difference can change how the config resolves, so an
92
+ * identical env skips the (expensive) child evaluation entirely. `ambientEnv`
93
+ * is the locked snapshot loadConfigWithProbeEnv returned, not the live
94
+ * `process.env`, which a concurrent load may be mutating.
95
+ */
96
+ export declare function taskEnvDivergesFromAmbient(taskEnv: NodeJS.ProcessEnv, ambientEnv: NodeJS.ProcessEnv): boolean;
97
+ /**
98
+ * The messages the config-eval worker sends over IPC. Tagged so the parent can
99
+ * tell them apart from anything else on the channel: the user's config module
100
+ * runs in the child and can itself call `process.send` during evaluation, and
101
+ * such a message must not settle the resolution.
102
+ */
103
+ export type WebserverConfigWorkerMessage = ({
104
+ type: 'webserver-config-result';
105
+ } & ConfigEvaluation) | {
106
+ type: 'webserver-config-error';
107
+ error: string;
108
+ };
109
+ type ChildEval = (configFilePath: string, workspaceRoot: string, env: NodeJS.ProcessEnv) => Promise<ConfigEvaluation>;
110
+ /**
111
+ * Evaluates `configFilePath`'s `webServer` addresses under `taskEnv`, bounded by
112
+ * the concurrency cap. Used only when the env diverges from ambient.
113
+ */
114
+ export declare function resolveWebServersUnderEnv(configFilePath: string, workspaceRoot: string, taskEnv: NodeJS.ProcessEnv): Promise<ConfigEvaluation>;
115
+ export declare function _setChildEval(impl: ChildEval | null): void;
116
+ export declare function _setWorkerScriptPath(path: string | null): void;
117
+ export {};
@@ -0,0 +1,391 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeWebServers = normalizeWebServers;
4
+ exports.pickProbeEnv = pickProbeEnv;
5
+ exports.loadConfigWithProbeEnv = loadConfigWithProbeEnv;
6
+ exports.getProbeEnvDivergence = getProbeEnvDivergence;
7
+ exports.taskEnvDivergesFromAmbient = taskEnvDivergesFromAmbient;
8
+ exports.resolveWebServersUnderEnv = resolveWebServersUnderEnv;
9
+ exports._setChildEval = _setChildEval;
10
+ exports._setWorkerScriptPath = _setWorkerScriptPath;
11
+ const internal_1 = require("@nx/devkit/internal");
12
+ const node_child_process_1 = require("node:child_process");
13
+ const node_os_1 = require("node:os");
14
+ const node_path_1 = require("node:path");
15
+ const proxy_1 = require("../executors/wait-for-webserver/proxy");
16
+ function normalizeWebServers(webServer) {
17
+ if (!webServer) {
18
+ return [];
19
+ }
20
+ const servers = Array.isArray(webServer) ? webServer : [webServer];
21
+ return servers.map((server) => ({
22
+ command: server.command,
23
+ url: server.url,
24
+ port: server.port,
25
+ reuseExistingServer: server.reuseExistingServer,
26
+ ignoreHTTPSErrors: server.ignoreHTTPSErrors,
27
+ timeout: server.timeout,
28
+ waitsForOutput: server.wait?.stdout || server.wait?.stderr ? true : undefined,
29
+ hasEnv: server.env && Object.keys(server.env).length > 0 ? true : undefined,
30
+ }));
31
+ }
32
+ // The TLS material Node reads when a probe verifies an https certificate.
33
+ const TLS_PROBE_VARS = ['NODE_EXTRA_CA_CERTS', 'NODE_TLS_REJECT_UNAUTHORIZED'];
34
+ // The variables a url probe routes a request by, in both spellings the proxy
35
+ // resolution accepts.
36
+ const ROUTE_VARS = ['http_proxy', 'https_proxy', 'all_proxy', 'no_proxy'];
37
+ // The npm-exported ones a Playwright below 1.59.0 reads ahead of them.
38
+ const NPM_CONFIG_ROUTE_VARS = [
39
+ 'npm_config_http_proxy',
40
+ 'npm_config_https_proxy',
41
+ 'npm_config_proxy',
42
+ 'npm_config_no_proxy',
43
+ ];
44
+ // Every variable a url probe reads to route and verify a request.
45
+ const PROBE_ENV_VARS = [
46
+ ...[...ROUTE_VARS, ...NPM_CONFIG_ROUTE_VARS].flatMap((name) => [
47
+ name,
48
+ name.toUpperCase(),
49
+ ]),
50
+ ...TLS_PROBE_VARS,
51
+ ];
52
+ function pickProbeEnv(env) {
53
+ const picked = {};
54
+ for (const variable of PROBE_ENV_VARS) {
55
+ if (env[variable] !== undefined) {
56
+ picked[variable] = env[variable];
57
+ }
58
+ }
59
+ return picked;
60
+ }
61
+ let inProcessLoad = Promise.resolve();
62
+ /**
63
+ * Loads a Playwright config in this process, hands it to `consume`, and
64
+ * returns the consumed value with the probe env the evaluation left in
65
+ * `process.env`, then puts `process.env` back the way it was: the task and
66
+ * gate dotenv files expand against it, and a later env comparison has to see
67
+ * the graph-time env, not whatever the last config wrote. `consume` must be
68
+ * synchronous and must not let the config object escape: a config can expose
69
+ * getters that read env it set while loading, so every read has to happen
70
+ * before the restore. Loads are serialized because createNodes evaluates
71
+ * configs concurrently and a concurrent load's writes would be misattributed.
72
+ * A client env applied mid-load (the daemon forwards it as it arrives) is
73
+ * re-applied over the restored env; restoring the pre-load values alone would
74
+ * revert it.
75
+ *
76
+ * `ambientEnv` is a snapshot of the restored env, taken while still holding
77
+ * the load lock. Task-env reconstruction and comparison after this returns
78
+ * must base on it rather than on the live `process.env`: the lock is released
79
+ * on return, so a sibling load's transient writes can be visible in the live
80
+ * env by then and would be read as ambient.
81
+ */
82
+ async function loadConfigWithProbeEnv(configPath, consume) {
83
+ const previous = inProcessLoad;
84
+ let release;
85
+ inProcessLoad = new Promise((resolve) => (release = resolve));
86
+ await previous;
87
+ try {
88
+ const applySequence = (0, internal_1.getAppliedDaemonClientEnv)()?.sequence;
89
+ const before = { ...process.env };
90
+ let consumed;
91
+ let probeEnv;
92
+ try {
93
+ const config = await (0, internal_1.loadConfigFile)(configPath);
94
+ consumed = consume(config);
95
+ probeEnv = pickProbeEnv(process.env);
96
+ }
97
+ finally {
98
+ restoreEnv(before);
99
+ const applied = (0, internal_1.getAppliedDaemonClientEnv)();
100
+ if (applied && applied.sequence !== applySequence) {
101
+ (0, internal_1.applyDaemonEnvFromClient)(applied.env);
102
+ }
103
+ }
104
+ return { consumed, probeEnv, ambientEnv: { ...process.env } };
105
+ }
106
+ finally {
107
+ release();
108
+ }
109
+ }
110
+ function restoreEnv(snapshot) {
111
+ for (const key of Object.keys(process.env)) {
112
+ if (!(key in snapshot)) {
113
+ delete process.env[key];
114
+ }
115
+ }
116
+ for (const [key, value] of Object.entries(snapshot)) {
117
+ if (process.env[key] !== value) {
118
+ process.env[key] = value;
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * The routes a probe under `env` can take, read as the executor's proxy
124
+ * resolution reads them: the proxy for each protocol a redirect can reach
125
+ * (`<protocol>_proxy`, falling back to `all_proxy`, normalized as it would be
126
+ * dialled) and the `no_proxy` filter as its set of entries, the `npm_config_*`
127
+ * variables included with `npmConfigProxy`. A `no_proxy` that excludes every
128
+ * host, or no proxy at all, sends every hop direct, so both collapse to empty
129
+ * routes and a masked variable never counts as a difference.
130
+ */
131
+ function proxyRoutes(env, npmConfigProxy) {
132
+ const route = (protocol) => {
133
+ const resolution = (0, proxy_1.resolveProxyForProtocol)(protocol, env, npmConfigProxy);
134
+ switch (resolution.kind) {
135
+ case 'direct':
136
+ return '';
137
+ case 'proxy':
138
+ return resolution.proxy.href;
139
+ case 'unusable':
140
+ return `unusable ${resolution.value}`;
141
+ default: {
142
+ const unhandled = resolution;
143
+ throw new Error(`Unhandled resolution ${JSON.stringify(unhandled)}`);
144
+ }
145
+ }
146
+ };
147
+ const http = route('http');
148
+ const https = route('https');
149
+ // Entries exclude hosts independently, so a differing order or separator is
150
+ // not a difference. `*.host` and `.host` are the same suffix match, whereas
151
+ // `*host` (any suffix) and `host` (exact) are not. A bare `*` excludes every
152
+ // host.
153
+ const entries = [
154
+ ...new Set((0, proxy_1.noProxyEntries)(env, npmConfigProxy).map((entry) => entry.replace(/^\*\./, '.'))),
155
+ ].sort();
156
+ if (entries.includes('*') || !(http || https)) {
157
+ return { http: '', https: '', no_proxy: '' };
158
+ }
159
+ return { http, https, no_proxy: entries.join(',') };
160
+ }
161
+ /**
162
+ * The env var names whose values would make the readiness gate probe `servers`
163
+ * differently than the consuming task's own Playwright probe: `taskEnv` is the
164
+ * env that probe runs under (the task env after its config loaded) and
165
+ * `gateEnv` the env the gate target runs under. The gate is its own target, so
166
+ * it loads its own dotenv files, not the consumer's, and it never loads the
167
+ * config: a task-scoped or config-written proxy exclusion or CA bundle never
168
+ * reaches it, and a gate probing through the wrong route can fail where
169
+ * Playwright would pass. A non-empty result means the gate cannot reproduce
170
+ * the task's probe and must not be inferred.
171
+ *
172
+ * Both probes follow redirects, so as soon as one server probes a url the
173
+ * routes for every protocol and host are compared, not only the route the
174
+ * configured url takes: an `https_proxy` for an http url or a `no_proxy` entry
175
+ * for another host can still decide how a redirect target is reached.
176
+ * `NODE_EXTRA_CA_CERTS` is compared unless every url server sets
177
+ * `ignoreHTTPSErrors`, which turns verification off on both sides for every
178
+ * hop, except that the tunnel to an https proxy is verified with the process
179
+ * defaults either way. With `legacyProxiedTls` (an installed Playwright below
180
+ * 1.59.0, whose probe never verifies the origin behind a proxy tunnel, and
181
+ * neither does the gate on that version) it is also left out while every
182
+ * https hop can only take such a tunnel: an http proxy on the https route, no
183
+ * https proxy on the http route, and no `no_proxy` entry that could send a
184
+ * redirect target direct. `NODE_TLS_REJECT_UNAUTHORIZED` only reaches that
185
+ * tunnel to an https proxy, since every request sets `rejectUnauthorized`
186
+ * itself, and only its `'0'` state counts. With `npmConfigProxy` (an installed
187
+ * Playwright below 1.59.0, whose probe reads npm's `npm_config_*` proxy
188
+ * variables ahead of the standard ones, as the gate does on that version) those
189
+ * variables route the probe and are compared too.
190
+ */
191
+ function getProbeEnvDivergence(servers, taskEnv, gateEnv, legacyProxiedTls = false, npmConfigProxy = false) {
192
+ // A port is probed with a raw TCP connect, and the executor rejects a
193
+ // malformed url up front; env plays no part in either.
194
+ const urlServers = servers.filter((server) => server.url && URL.canParse(server.url));
195
+ if (urlServers.length === 0) {
196
+ return [];
197
+ }
198
+ const diverging = new Set();
199
+ const taskRoutes = proxyRoutes(taskEnv, npmConfigProxy);
200
+ const gateRoutes = proxyRoutes(gateEnv, npmConfigProxy);
201
+ // Name the raw variables behind a differing route. A proxy route is decided
202
+ // by every proxy variable (`no_proxy` can mask them all); the filter only by
203
+ // `no_proxy`.
204
+ const readVars = npmConfigProxy
205
+ ? [...ROUTE_VARS, ...NPM_CONFIG_ROUTE_VARS]
206
+ : ROUTE_VARS;
207
+ const routeVars = taskRoutes.http !== gateRoutes.http || taskRoutes.https !== gateRoutes.https
208
+ ? readVars
209
+ : taskRoutes.no_proxy !== gateRoutes.no_proxy
210
+ ? readVars.filter((name) => name.endsWith('no_proxy'))
211
+ : [];
212
+ for (const name of routeVars) {
213
+ for (const variable of [name, name.toUpperCase()]) {
214
+ if (taskEnv[variable] !== gateEnv[variable]) {
215
+ diverging.add(variable);
216
+ }
217
+ }
218
+ }
219
+ // `ignoreHTTPSErrors` reaches the target's certificate only. An https target
220
+ // is tunnelled through an https proxy over a TLS connection the proxy agent
221
+ // opens with the process defaults, on both sides, so that certificate is
222
+ // verified regardless.
223
+ const tunnelsThroughTlsProxy = [taskRoutes.https, gateRoutes.https].some((route) => route.startsWith('https:'));
224
+ const onlyUnverifiedLegacyTunnels = legacyProxiedTls &&
225
+ [taskRoutes, gateRoutes].every((routes) => routes.https.startsWith('http:') &&
226
+ !routes.http.startsWith('https:') &&
227
+ routes.no_proxy === '');
228
+ if ((tunnelsThroughTlsProxy ||
229
+ (urlServers.some((server) => !server.ignoreHTTPSErrors) &&
230
+ !onlyUnverifiedLegacyTunnels)) &&
231
+ taskEnv.NODE_EXTRA_CA_CERTS !== gateEnv.NODE_EXTRA_CA_CERTS) {
232
+ diverging.add('NODE_EXTRA_CA_CERTS');
233
+ }
234
+ // Both probes pass `rejectUnauthorized` on every request, which overrides
235
+ // the env default. Only the proxy agent's own connection to an https proxy
236
+ // is left to it, and Node reads it as exactly '0'.
237
+ if (tunnelsThroughTlsProxy &&
238
+ (taskEnv.NODE_TLS_REJECT_UNAUTHORIZED === '0') !==
239
+ (gateEnv.NODE_TLS_REJECT_UNAUTHORIZED === '0')) {
240
+ diverging.add('NODE_TLS_REJECT_UNAUTHORIZED');
241
+ }
242
+ return [...diverging].sort();
243
+ }
244
+ /**
245
+ * Whether the task env a chain would run with differs from the graph-time
246
+ * ambient env. Only a difference can change how the config resolves, so an
247
+ * identical env skips the (expensive) child evaluation entirely. `ambientEnv`
248
+ * is the locked snapshot loadConfigWithProbeEnv returned, not the live
249
+ * `process.env`, which a concurrent load may be mutating.
250
+ */
251
+ function taskEnvDivergesFromAmbient(taskEnv, ambientEnv) {
252
+ const keys = new Set([...Object.keys(ambientEnv), ...Object.keys(taskEnv)]);
253
+ for (const key of keys) {
254
+ if (ambientEnv[key] !== taskEnv[key]) {
255
+ return true;
256
+ }
257
+ }
258
+ return false;
259
+ }
260
+ // A forked config evaluation holds a full config module graph in memory, so cap
261
+ // how many run at once.
262
+ const MAX_CONCURRENT_EVALS = Math.max(1, Math.min((0, node_os_1.cpus)().length, 8));
263
+ const CHILD_EVAL_TIMEOUT = 30_000;
264
+ // Cap the worker stderr buffered to fold into a failure message.
265
+ const MAX_STDERR = 8192;
266
+ let activeEvals = 0;
267
+ const evalQueue = [];
268
+ let childEval = forkChildEval;
269
+ let workerScriptPath = (0, node_path_1.join)(__dirname, 'webserver-config-worker.js');
270
+ /**
271
+ * Evaluates `configFilePath`'s `webServer` addresses under `taskEnv`, bounded by
272
+ * the concurrency cap. Used only when the env diverges from ambient.
273
+ */
274
+ async function resolveWebServersUnderEnv(configFilePath, workspaceRoot, taskEnv) {
275
+ // `while` rather than `if`: a caller arriving between a slot's release and
276
+ // the woken waiter's resume can claim the slot first, so the waiter must
277
+ // re-check before taking it.
278
+ while (activeEvals >= MAX_CONCURRENT_EVALS) {
279
+ await new Promise((resolve) => evalQueue.push(resolve));
280
+ }
281
+ activeEvals++;
282
+ try {
283
+ return await childEval(configFilePath, workspaceRoot, taskEnv);
284
+ }
285
+ finally {
286
+ activeEvals--;
287
+ evalQueue.shift()?.();
288
+ }
289
+ }
290
+ function isWorkerMessage(message) {
291
+ if (typeof message !== 'object' || message === null) {
292
+ return false;
293
+ }
294
+ const candidate = message;
295
+ return ((candidate.type === 'webserver-config-error' &&
296
+ typeof candidate.error === 'string') ||
297
+ (candidate.type === 'webserver-config-result' &&
298
+ Array.isArray(candidate.webServers) &&
299
+ typeof candidate.probeEnv === 'object' &&
300
+ candidate.probeEnv !== null));
301
+ }
302
+ function forkChildEval(configFilePath, workspaceRoot, env) {
303
+ return new Promise((resolve, reject) => {
304
+ // Startup env and project-root cwd mirror the inferred task, which runs
305
+ // `playwright test` from the project root: a NODE_OPTIONS loader runs at
306
+ // process start and resolves a relative path from there, whichever
307
+ // directory nx was invoked from.
308
+ const child = (0, node_child_process_1.fork)(workerScriptPath, [configFilePath, workspaceRoot], {
309
+ cwd: (0, node_path_1.join)(workspaceRoot, (0, node_path_1.dirname)(configFilePath)),
310
+ env,
311
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
312
+ });
313
+ let stderr = '';
314
+ child.stderr?.on('data', (chunk) => {
315
+ if (stderr.length < MAX_STDERR) {
316
+ stderr += chunk.toString().slice(0, MAX_STDERR - stderr.length);
317
+ }
318
+ });
319
+ // Fold the worker's stderr into a failure so a crash before it can report
320
+ // over IPC (a missing module, a syntax error, a signal kill) is not
321
+ // surfaced as a bare exit code.
322
+ const withStderr = (message) => {
323
+ const tail = stderr.trim();
324
+ return tail ? `${message}\n${tail}` : message;
325
+ };
326
+ let settled = false;
327
+ const finish = (fn) => {
328
+ if (settled) {
329
+ return;
330
+ }
331
+ settled = true;
332
+ clearTimeout(timer);
333
+ child.removeAllListeners();
334
+ if (child.connected) {
335
+ child.disconnect();
336
+ }
337
+ fn();
338
+ };
339
+ const timer = setTimeout(() => {
340
+ // SIGKILL: the eval child holds no state that needs graceful teardown,
341
+ // and a config stuck in a busy loop would ignore SIGTERM anyway.
342
+ child.kill('SIGKILL');
343
+ finish(() => reject(new Error(withStderr('Timed out evaluating the Playwright config'))));
344
+ }, CHILD_EVAL_TIMEOUT);
345
+ child.on('message', (message) => {
346
+ // Anything that fails the guard came from the evaluated config itself;
347
+ // ignore it.
348
+ if (!isWorkerMessage(message)) {
349
+ return;
350
+ }
351
+ switch (message.type) {
352
+ case 'webserver-config-error':
353
+ finish(() => reject(new Error(withStderr(message.error))));
354
+ break;
355
+ case 'webserver-config-result':
356
+ finish(() => resolve({
357
+ webServers: message.webServers,
358
+ probeEnv: message.probeEnv,
359
+ }));
360
+ break;
361
+ default: {
362
+ // A new message variant has to say how it settles the resolution.
363
+ const unhandled = message;
364
+ throw new Error(`Unhandled worker message ${JSON.stringify(unhandled)}`);
365
+ }
366
+ }
367
+ });
368
+ child.on('error', (error) => finish(() => reject(error)));
369
+ // `close` rather than `exit` so the stderr the failure folds in has fully
370
+ // flushed before the message is built. A worker that closes before sending a
371
+ // result (even with code 0, e.g. an IPC delivery failure) rejects here
372
+ // rather than hanging to the timeout; finish() no-ops once a result settled.
373
+ child.on('close', (code) => {
374
+ finish(() => reject(new Error(withStderr(code
375
+ ? `Config evaluation worker exited with code ${code}`
376
+ : 'Config evaluation worker exited without resolving the web server address'))));
377
+ });
378
+ });
379
+ }
380
+ // Test seam: plugin unit tests replace the fork with an in-process evaluation
381
+ // (the compiled worker only exists in dist) and concurrency tests use it to
382
+ // control completion order. The fork itself is exercised against fixture
383
+ // workers via _setWorkerScriptPath below.
384
+ function _setChildEval(impl) {
385
+ childEval = impl ?? forkChildEval;
386
+ }
387
+ // Test seam: point the fork at a fixture worker so forkChildEval's own timeout,
388
+ // exit and stderr handling can be exercised without the compiled worker.
389
+ function _setWorkerScriptPath(path) {
390
+ workerScriptPath = path ?? (0, node_path_1.join)(__dirname, 'webserver-config-worker.js');
391
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The version of the `@playwright/test` resolved from `requirePaths`, the
3
+ * directories the install is resolved from, the project root first so a
4
+ * pnpm-isolated copy wins over a hoisted one. `null` when none resolves or its
5
+ * version does not parse.
6
+ */
7
+ export declare function installedPlaywrightVersion(requirePaths: string[]): string | null;
8
+ /** Whether an installed version read by `installedPlaywrightVersion` is older than `floor`. */
9
+ export declare function installedPlaywrightIsBelow(version: string | null, floor: string): boolean;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installedPlaywrightVersion = installedPlaywrightVersion;
4
+ exports.installedPlaywrightIsBelow = installedPlaywrightIsBelow;
5
+ const internal_1 = require("@nx/devkit/internal");
6
+ const semver_1 = require("semver");
7
+ /**
8
+ * The version of the `@playwright/test` resolved from `requirePaths`, the
9
+ * directories the install is resolved from, the project root first so a
10
+ * pnpm-isolated copy wins over a hoisted one. `null` when none resolves or its
11
+ * version does not parse.
12
+ */
13
+ function installedPlaywrightVersion(requirePaths) {
14
+ if (requirePaths.length === 0) {
15
+ return null;
16
+ }
17
+ const version = (0, internal_1.getInstalledPackageVersion)('@playwright/test', requirePaths);
18
+ return version !== null && (0, semver_1.valid)(version) !== null ? version : null;
19
+ }
20
+ /** Whether an installed version read by `installedPlaywrightVersion` is older than `floor`. */
21
+ function installedPlaywrightIsBelow(version, floor) {
22
+ return version !== null && (0, semver_1.lt)(version, floor);
23
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Whether the installed Playwright routes its probe by npm's `npm_config_*`
3
+ * proxy variables as well as the standard ones, which it did until 1.59.0
4
+ * dropped them. The readiness gate and its graph-time inference follow the
5
+ * installed version: ignoring a variable that version's own probe reads would
6
+ * send the gate a different way than Playwright. `version` is the installed
7
+ * version as `installedPlaywrightVersion` reads it; an unresolvable or
8
+ * unparseable installation (`null`) reads as ignoring them, the semantics of
9
+ * current versions.
10
+ */
11
+ export declare function installedPlaywrightReadsNpmConfigProxy(version: string | null): boolean;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installedPlaywrightReadsNpmConfigProxy = installedPlaywrightReadsNpmConfigProxy;
4
+ const installed_playwright_1 = require("./installed-playwright");
5
+ const versions_1 = require("./versions");
6
+ /**
7
+ * Whether the installed Playwright routes its probe by npm's `npm_config_*`
8
+ * proxy variables as well as the standard ones, which it did until 1.59.0
9
+ * dropped them. The readiness gate and its graph-time inference follow the
10
+ * installed version: ignoring a variable that version's own probe reads would
11
+ * send the gate a different way than Playwright. `version` is the installed
12
+ * version as `installedPlaywrightVersion` reads it; an unresolvable or
13
+ * unparseable installation (`null`) reads as ignoring them, the semantics of
14
+ * current versions.
15
+ */
16
+ function installedPlaywrightReadsNpmConfigProxy(version) {
17
+ return (0, installed_playwright_1.installedPlaywrightIsBelow)(version, versions_1.minPlaywrightVersionIgnoringNpmConfigProxy);
18
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Whether the installed Playwright probes an https url tunnelled through a
3
+ * proxy without verifying the origin certificate, which it did until 1.59.0
4
+ * started honoring the caller's setting. The readiness gate and its graph-time
5
+ * inference follow the installed version: verifying where that version's own
6
+ * probe does not would fail a wait Playwright itself would pass, and counting
7
+ * TLS material that probe never reads would drop a gate for nothing.
8
+ * `version` is the installed version as `installedPlaywrightVersion` reads it;
9
+ * an unresolvable or unparseable installation (`null`) reads as verifying, the
10
+ * semantics of current versions.
11
+ */
12
+ export declare function installedPlaywrightSkipsProxiedTls(version: string | null): boolean;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installedPlaywrightSkipsProxiedTls = installedPlaywrightSkipsProxiedTls;
4
+ const installed_playwright_1 = require("./installed-playwright");
5
+ const versions_1 = require("./versions");
6
+ /**
7
+ * Whether the installed Playwright probes an https url tunnelled through a
8
+ * proxy without verifying the origin certificate, which it did until 1.59.0
9
+ * started honoring the caller's setting. The readiness gate and its graph-time
10
+ * inference follow the installed version: verifying where that version's own
11
+ * probe does not would fail a wait Playwright itself would pass, and counting
12
+ * TLS material that probe never reads would drop a gate for nothing.
13
+ * `version` is the installed version as `installedPlaywrightVersion` reads it;
14
+ * an unresolvable or unparseable installation (`null`) reads as verifying, the
15
+ * semantics of current versions.
16
+ */
17
+ function installedPlaywrightSkipsProxiedTls(version) {
18
+ return (0, installed_playwright_1.installedPlaywrightIsBelow)(version, versions_1.minPlaywrightVersionVerifyingProxiedTls);
19
+ }
@@ -1,5 +1,7 @@
1
1
  export declare const nxVersion: any;
2
2
  export declare const minSupportedPlaywrightVersion = "1.36.0";
3
3
  export declare const minPlaywrightVersionForBlobReports = "1.37.0";
4
+ export declare const minPlaywrightVersionVerifyingProxiedTls = "1.59.0";
5
+ export declare const minPlaywrightVersionIgnoringNpmConfigProxy = "1.59.0";
4
6
  export declare const playwrightVersion = "^1.37.0";
5
7
  export declare const eslintPluginPlaywrightVersion = "^1.6.2";
@@ -1,9 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.eslintPluginPlaywrightVersion = exports.playwrightVersion = exports.minPlaywrightVersionForBlobReports = exports.minSupportedPlaywrightVersion = exports.nxVersion = void 0;
3
+ exports.eslintPluginPlaywrightVersion = exports.playwrightVersion = exports.minPlaywrightVersionIgnoringNpmConfigProxy = exports.minPlaywrightVersionVerifyingProxiedTls = exports.minPlaywrightVersionForBlobReports = exports.minSupportedPlaywrightVersion = exports.nxVersion = void 0;
4
4
  const path_1 = require("path");
5
5
  exports.nxVersion = require((0, path_1.join)('@nx/playwright', 'package.json')).version;
6
6
  exports.minSupportedPlaywrightVersion = '1.36.0';
7
7
  exports.minPlaywrightVersionForBlobReports = '1.37.0';
8
+ // Earlier versions never verify the origin certificate of an https url probed
9
+ // through a proxy tunnel, whatever the caller asked for.
10
+ // TODO(v25): drop with the unverified-tunnel branch once
11
+ // minSupportedPlaywrightVersion reaches 1.59.0.
12
+ exports.minPlaywrightVersionVerifyingProxiedTls = '1.59.0';
13
+ // Earlier versions bundle a `proxy-from-env` that also reads npm's
14
+ // `npm_config_*` proxy variables ahead of the standard ones.
15
+ // TODO(v25): drop with the npm_config_* proxy handling once
16
+ // minSupportedPlaywrightVersion reaches 1.59.0.
17
+ exports.minPlaywrightVersionIgnoringNpmConfigProxy = '1.59.0';
8
18
  exports.playwrightVersion = '^1.37.0';
9
19
  exports.eslintPluginPlaywrightVersion = '^1.6.2';
package/executors.json CHANGED
@@ -10,6 +10,12 @@
10
10
  "schema": "./dist/src/executors/merge-reports/schema.json",
11
11
  "description": "Merge Playwright blob reports to produce unified reports for the configured reporters (excluding the `blob` reporter).",
12
12
  "hidden": true
13
+ },
14
+ "wait-for-webserver": {
15
+ "implementation": "./dist/src/executors/wait-for-webserver/wait-for-webserver.impl",
16
+ "schema": "./dist/src/executors/wait-for-webserver/schema.json",
17
+ "description": "Wait for the E2E web server(s) to be ready.",
18
+ "hidden": true
13
19
  }
14
20
  }
15
21
  }