@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.
- package/dist/src/executors/wait-for-webserver/proxy.d.ts +28 -0
- package/dist/src/executors/wait-for-webserver/proxy.js +158 -0
- package/dist/src/executors/wait-for-webserver/schema.d.ts +9 -0
- package/dist/src/executors/wait-for-webserver/schema.json +44 -0
- package/dist/src/executors/wait-for-webserver/wait-for-webserver.impl.d.ts +6 -0
- package/dist/src/executors/wait-for-webserver/wait-for-webserver.impl.js +388 -0
- package/dist/src/generators/configuration/schema.d.ts +1 -2
- package/dist/src/plugins/plugin.d.ts +15 -0
- package/dist/src/plugins/plugin.js +395 -43
- package/dist/src/plugins/webserver-config-worker.d.ts +13 -0
- package/dist/src/plugins/webserver-config-worker.js +56 -0
- package/dist/src/plugins/webserver-readiness.d.ts +117 -0
- package/dist/src/plugins/webserver-readiness.js +391 -0
- package/dist/src/utils/installed-playwright.d.ts +9 -0
- package/dist/src/utils/installed-playwright.js +23 -0
- package/dist/src/utils/npm-config-proxy.d.ts +11 -0
- package/dist/src/utils/npm-config-proxy.js +18 -0
- package/dist/src/utils/proxied-tls-verification.d.ts +12 -0
- package/dist/src/utils/proxied-tls-verification.js +19 -0
- package/dist/src/utils/versions.d.ts +2 -0
- package/dist/src/utils/versions.js +11 -1
- package/executors.json +6 -0
- package/package.json +7 -6
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ProxyResolution = {
|
|
2
|
+
kind: 'direct';
|
|
3
|
+
} | {
|
|
4
|
+
kind: 'proxy';
|
|
5
|
+
proxy: URL;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'unusable';
|
|
8
|
+
variable: string;
|
|
9
|
+
value: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function resolveProxyForUrl(url: URL, env?: NodeJS.ProcessEnv, npmConfigProxy?: boolean): ProxyResolution;
|
|
13
|
+
/**
|
|
14
|
+
* The proxy `env` configures for `protocol`, before any `no_proxy` exclusion:
|
|
15
|
+
* `<protocol>_proxy`, falling back to `all_proxy`, normalized as the probe
|
|
16
|
+
* would use it (a value with no scheme takes the target's protocol). With
|
|
17
|
+
* `npmConfigProxy`, `npm_config_<protocol>_proxy` comes first and
|
|
18
|
+
* `npm_config_proxy` sits between the two.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveProxyForProtocol(protocol: string, env?: NodeJS.ProcessEnv, npmConfigProxy?: boolean): ProxyResolution;
|
|
21
|
+
/**
|
|
22
|
+
* The `no_proxy` entries `env` configures (`npm_config_no_proxy` first with
|
|
23
|
+
* `npmConfigProxy`), lowercased and with the empty entries a separator run
|
|
24
|
+
* leaves dropped. Each entry excludes hosts on its own, so their order carries
|
|
25
|
+
* no meaning.
|
|
26
|
+
*/
|
|
27
|
+
export declare function noProxyEntries(env: NodeJS.ProcessEnv, npmConfigProxy?: boolean): string[];
|
|
28
|
+
export declare function withoutCredentials(value: string): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Playwright routes its readiness poll through a proxy configured in the
|
|
3
|
+
// environment, so a gate that always connected directly would be answering a
|
|
4
|
+
// different question than the check it exists to satisfy. Selection mirrors the
|
|
5
|
+
// `proxy-from-env` copy playwright-core 1.60.0 bundles (`getProxyForUrl`)
|
|
6
|
+
// together with its own `normalizeProxyURL`; with `npmConfigProxy` it mirrors
|
|
7
|
+
// the copy bundled before 1.59.0, which read npm's `npm_config_*` proxy
|
|
8
|
+
// variables ahead of the standard ones. The transport is left to
|
|
9
|
+
// `https-proxy-agent`, the agent Playwright itself hands to `https.request`
|
|
10
|
+
// for a proxied https target.
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.resolveProxyForUrl = resolveProxyForUrl;
|
|
13
|
+
exports.resolveProxyForProtocol = resolveProxyForProtocol;
|
|
14
|
+
exports.noProxyEntries = noProxyEntries;
|
|
15
|
+
exports.withoutCredentials = withoutCredentials;
|
|
16
|
+
const DEFAULT_PORTS = {
|
|
17
|
+
ftp: 21,
|
|
18
|
+
gopher: 70,
|
|
19
|
+
http: 80,
|
|
20
|
+
https: 443,
|
|
21
|
+
ws: 80,
|
|
22
|
+
wss: 443,
|
|
23
|
+
};
|
|
24
|
+
function resolveProxyForUrl(url, env = process.env, npmConfigProxy = false) {
|
|
25
|
+
const protocol = url.protocol.split(':')[0];
|
|
26
|
+
// Matched on the host with any port stripped, which keeps the brackets on an
|
|
27
|
+
// IPv6 literal.
|
|
28
|
+
const hostname = url.host.replace(/:\d*$/, '');
|
|
29
|
+
if (!hostname || !protocol) {
|
|
30
|
+
return { kind: 'direct' };
|
|
31
|
+
}
|
|
32
|
+
const port = parseInt(url.port) || DEFAULT_PORTS[protocol] || 0;
|
|
33
|
+
if (!shouldProxy(hostname, port, env, npmConfigProxy)) {
|
|
34
|
+
return { kind: 'direct' };
|
|
35
|
+
}
|
|
36
|
+
return resolveProxyForProtocol(protocol, env, npmConfigProxy);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The proxy `env` configures for `protocol`, before any `no_proxy` exclusion:
|
|
40
|
+
* `<protocol>_proxy`, falling back to `all_proxy`, normalized as the probe
|
|
41
|
+
* would use it (a value with no scheme takes the target's protocol). With
|
|
42
|
+
* `npmConfigProxy`, `npm_config_<protocol>_proxy` comes first and
|
|
43
|
+
* `npm_config_proxy` sits between the two.
|
|
44
|
+
*/
|
|
45
|
+
function resolveProxyForProtocol(protocol, env = process.env, npmConfigProxy = false) {
|
|
46
|
+
const source = readEnv(npmConfigProxy
|
|
47
|
+
? [
|
|
48
|
+
`npm_config_${protocol}_proxy`,
|
|
49
|
+
`${protocol}_proxy`,
|
|
50
|
+
'npm_config_proxy',
|
|
51
|
+
'all_proxy',
|
|
52
|
+
]
|
|
53
|
+
: [`${protocol}_proxy`, 'all_proxy'], env);
|
|
54
|
+
if (!source.value) {
|
|
55
|
+
return { kind: 'direct' };
|
|
56
|
+
}
|
|
57
|
+
// A value with no scheme takes the target's protocol, which is where this
|
|
58
|
+
// differs from `normalizeProxyURL` alone: that only defaults to http.
|
|
59
|
+
const scheme = source.value.includes('://') ? '' : `${protocol}://`;
|
|
60
|
+
const normalized = `${scheme}${source.value}`.trim();
|
|
61
|
+
// Playwright's own probe errors out on a proxy URL it cannot use instead of
|
|
62
|
+
// falling back to a direct request, so the gate must not clear here. Each
|
|
63
|
+
// cause is named, because the value alone often does not show it: redacting
|
|
64
|
+
// the credentials takes the offending character with them.
|
|
65
|
+
const unusable = (reason) => ({
|
|
66
|
+
kind: 'unusable',
|
|
67
|
+
variable: source.variable,
|
|
68
|
+
value: withoutCredentials(source.value),
|
|
69
|
+
reason,
|
|
70
|
+
});
|
|
71
|
+
let proxy;
|
|
72
|
+
try {
|
|
73
|
+
proxy = new URL(/^\w+:\/\//.test(normalized) ? normalized : `http://${normalized}`);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return unusable('is not a valid url');
|
|
77
|
+
}
|
|
78
|
+
// Anything else parses but cannot be dialled: an http target throws out of
|
|
79
|
+
// `http.request`, and an https one gets `CONNECT` spoken at a port that is
|
|
80
|
+
// not answering HTTP, which surfaces much later as a refused server.
|
|
81
|
+
if (proxy.protocol !== 'http:' && proxy.protocol !== 'https:') {
|
|
82
|
+
return unusable('is not an http or https url');
|
|
83
|
+
}
|
|
84
|
+
// Credentials are percent-decoded when the request is built, far from the
|
|
85
|
+
// configuration that produced them, so a value that cannot be decoded is
|
|
86
|
+
// rejected here where the failure can still name where it came from.
|
|
87
|
+
try {
|
|
88
|
+
decodeURIComponent(proxy.username);
|
|
89
|
+
decodeURIComponent(proxy.password);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return unusable('has credentials that are not valid percent-encoding');
|
|
93
|
+
}
|
|
94
|
+
return { kind: 'proxy', proxy };
|
|
95
|
+
}
|
|
96
|
+
// `no_proxy` governs where traffic may go. Sending the target URL to an
|
|
97
|
+
// excluded proxy would disclose it even when the direct probe is the one that
|
|
98
|
+
// succeeds.
|
|
99
|
+
function shouldProxy(hostname, port, env, npmConfigProxy) {
|
|
100
|
+
const noProxy = readNoProxy(env, npmConfigProxy).toLowerCase();
|
|
101
|
+
if (!noProxy) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (noProxy === '*') {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return noProxyEntries(env, npmConfigProxy).every((entry) => {
|
|
108
|
+
const withPort = entry.match(/^(.+):(\d+)$/);
|
|
109
|
+
let host = withPort ? withPort[1] : entry;
|
|
110
|
+
const entryPort = withPort ? parseInt(withPort[2]) : 0;
|
|
111
|
+
if (entryPort && entryPort !== port) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (!/^[.*]/.test(host)) {
|
|
115
|
+
return hostname !== host;
|
|
116
|
+
}
|
|
117
|
+
if (host.charAt(0) === '*') {
|
|
118
|
+
host = host.slice(1);
|
|
119
|
+
}
|
|
120
|
+
return !hostname.endsWith(host);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The `no_proxy` entries `env` configures (`npm_config_no_proxy` first with
|
|
125
|
+
* `npmConfigProxy`), lowercased and with the empty entries a separator run
|
|
126
|
+
* leaves dropped. Each entry excludes hosts on its own, so their order carries
|
|
127
|
+
* no meaning.
|
|
128
|
+
*/
|
|
129
|
+
function noProxyEntries(env, npmConfigProxy = false) {
|
|
130
|
+
return readNoProxy(env, npmConfigProxy)
|
|
131
|
+
.toLowerCase()
|
|
132
|
+
.split(/[,\s]/)
|
|
133
|
+
.filter(Boolean);
|
|
134
|
+
}
|
|
135
|
+
function readNoProxy(env, npmConfigProxy) {
|
|
136
|
+
return readEnv(npmConfigProxy ? ['npm_config_no_proxy', 'no_proxy'] : ['no_proxy'], env).value;
|
|
137
|
+
}
|
|
138
|
+
// The executor runs in its own process, so a proxy variable the Playwright
|
|
139
|
+
// config sets while loading (through `dotenv`, say) reaches Playwright's probe
|
|
140
|
+
// and not this one. The name that was read is reported alongside the value so a
|
|
141
|
+
// bad one can be found among the variables this looks at. The first name with
|
|
142
|
+
// a value wins, in the lowercase spelling before the uppercase one.
|
|
143
|
+
function readEnv(names, env) {
|
|
144
|
+
for (const name of names) {
|
|
145
|
+
for (const variable of [name.toLowerCase(), name.toUpperCase()]) {
|
|
146
|
+
if (env[variable]) {
|
|
147
|
+
return { variable, value: env[variable] };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { variable: names[names.length - 1].toUpperCase(), value: '' };
|
|
152
|
+
}
|
|
153
|
+
// A rejected value is echoed back so it can be recognized in the environment,
|
|
154
|
+
// and it reaches the task log, so the credentials come off first.
|
|
155
|
+
function withoutCredentials(value) {
|
|
156
|
+
const at = value.lastIndexOf('@');
|
|
157
|
+
return at === -1 ? value : `***@${value.slice(at + 1)}`;
|
|
158
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"title": "Schema for Playwright Wait For Web Server Executor",
|
|
5
|
+
"description": "Waits for the E2E web server(s) to be ready.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"servers": {
|
|
9
|
+
"type": "array",
|
|
10
|
+
"description": "The web servers to wait for.",
|
|
11
|
+
"items": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"port": {
|
|
15
|
+
"type": "number",
|
|
16
|
+
"description": "The port to wait for TCP connections on.",
|
|
17
|
+
"minimum": 1,
|
|
18
|
+
"maximum": 65535,
|
|
19
|
+
"multipleOf": 1
|
|
20
|
+
},
|
|
21
|
+
"url": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "The URL to poll. Redirects are followed and the server is ready once the final response has a status from 200 to 403."
|
|
24
|
+
},
|
|
25
|
+
"ignoreHTTPSErrors": {
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"description": "Whether to ignore HTTPS errors (e.g. a self-signed certificate) when polling the URL.",
|
|
28
|
+
"default": false
|
|
29
|
+
},
|
|
30
|
+
"timeout": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "The maximum time to wait for this server in milliseconds. A value of 0 means unset, as it does for Playwright's webServer.timeout. Defaults to 60000.",
|
|
33
|
+
"default": 60000
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"timeout": {
|
|
39
|
+
"type": "number",
|
|
40
|
+
"description": "The maximum time to wait in milliseconds before failing. Overrides the timeout configured on every server. A value of 0 means unset, as it does for Playwright's webServer.timeout."
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"required": ["servers"]
|
|
44
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.waitForWebserverExecutor = waitForWebserverExecutor;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const devkit_1 = require("@nx/devkit");
|
|
6
|
+
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
7
|
+
const http = tslib_1.__importStar(require("node:http"));
|
|
8
|
+
const https = tslib_1.__importStar(require("node:https"));
|
|
9
|
+
const node_net_1 = require("node:net");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const installed_playwright_1 = require("../../utils/installed-playwright");
|
|
12
|
+
const npm_config_proxy_1 = require("../../utils/npm-config-proxy");
|
|
13
|
+
const proxied_tls_verification_1 = require("../../utils/proxied-tls-verification");
|
|
14
|
+
const proxy_1 = require("./proxy");
|
|
15
|
+
const DEFAULT_TIMEOUT = 60_000;
|
|
16
|
+
// Retry cadence mirrors Playwright's web server readiness loop: a short ramp
|
|
17
|
+
// up front, then a steady interval.
|
|
18
|
+
const RETRY_SCHEDULE = [100, 250, 500];
|
|
19
|
+
const FALLBACK_RETRY_DELAY = 1_000;
|
|
20
|
+
// A probe the deadline cut short before it could learn anything observed
|
|
21
|
+
// nothing, so it must not replace what the previous probe saw.
|
|
22
|
+
const NO_OBSERVATION = 'the deadline passed before the server responded';
|
|
23
|
+
async function waitForWebserverExecutor(options, context) {
|
|
24
|
+
const servers = options.servers ?? [];
|
|
25
|
+
if (servers.length === 0) {
|
|
26
|
+
devkit_1.logger.error('@nx/playwright:wait-for-webserver requires at least one server to wait for.');
|
|
27
|
+
return { success: false };
|
|
28
|
+
}
|
|
29
|
+
const playwright = (0, installed_playwright_1.installedPlaywrightVersion)(playwrightRequirePaths(context));
|
|
30
|
+
const skipProxiedTls = (0, proxied_tls_verification_1.installedPlaywrightSkipsProxiedTls)(playwright);
|
|
31
|
+
const npmConfigProxy = (0, npm_config_proxy_1.installedPlaywrightReadsNpmConfigProxy)(playwright);
|
|
32
|
+
const problem = servers
|
|
33
|
+
.map((server) => findServerProblem(server, npmConfigProxy))
|
|
34
|
+
.find(Boolean);
|
|
35
|
+
if (problem) {
|
|
36
|
+
devkit_1.logger.error(`@nx/playwright:wait-for-webserver ${problem}`);
|
|
37
|
+
return { success: false };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await Promise.all(servers.map((server) =>
|
|
41
|
+
// A zero timeout means "unset" rather than "give up immediately", which
|
|
42
|
+
// is how Playwright reads it (`this._options.timeout || 60 * 1e3`).
|
|
43
|
+
waitForServer(server, options.timeout || server.timeout || DEFAULT_TIMEOUT, skipProxiedTls, npmConfigProxy)));
|
|
44
|
+
return { success: true };
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
devkit_1.logger.error(`@nx/playwright:wait-for-webserver ${e instanceof Error ? e.message : String(e)}`);
|
|
48
|
+
return { success: false };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.default = waitForWebserverExecutor;
|
|
52
|
+
// A malformed URL, an out-of-range port and a proxy that cannot be used are all
|
|
53
|
+
// inputs that can never become valid, so they are rejected up front instead of
|
|
54
|
+
// being retried until the timeout and reported as a slow server. An unusable
|
|
55
|
+
// port is also the only input that makes `connect` throw synchronously, which
|
|
56
|
+
// no probe could turn into a failure.
|
|
57
|
+
function findServerProblem(server, npmConfigProxy) {
|
|
58
|
+
if (server.port == null && !server.url) {
|
|
59
|
+
return 'requires each server to define a "port" or a "url".';
|
|
60
|
+
}
|
|
61
|
+
if (server.port != null && !isValidPort(server.port)) {
|
|
62
|
+
return `received an invalid "port": ${server.port}.`;
|
|
63
|
+
}
|
|
64
|
+
if (server.url) {
|
|
65
|
+
let url;
|
|
66
|
+
try {
|
|
67
|
+
url = new URL(server.url);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return `received an invalid "url": ${(0, proxy_1.withoutCredentials)(server.url)}.`;
|
|
71
|
+
}
|
|
72
|
+
// Only the address the wait starts from is checked here. A redirect can
|
|
73
|
+
// cross to a protocol with its own proxy variable, which the probe reports
|
|
74
|
+
// when it gets there.
|
|
75
|
+
const resolution = (0, proxy_1.resolveProxyForUrl)(url, process.env, npmConfigProxy);
|
|
76
|
+
if (resolution.kind === 'unusable') {
|
|
77
|
+
return `${unusableProxyProblem(resolution)}.`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Left unpunctuated because a probe failure is also quoted mid-sentence when
|
|
82
|
+
// the wait times out.
|
|
83
|
+
function unusableProxyProblem(resolution) {
|
|
84
|
+
return `cannot use the proxy configured in ${resolution.variable}: ${resolution.value} ${resolution.reason}`;
|
|
85
|
+
}
|
|
86
|
+
// 0 is not a connectable port, so it is rejected rather than probed.
|
|
87
|
+
function isValidPort(port) {
|
|
88
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
89
|
+
}
|
|
90
|
+
// The directories the task's Playwright install resolves from: the project
|
|
91
|
+
// root, where `playwright test` runs, then the workspace root.
|
|
92
|
+
function playwrightRequirePaths(context) {
|
|
93
|
+
if (context.root == null) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const projectRoot = context.projectName != null
|
|
97
|
+
? context.projectsConfigurations?.projects?.[context.projectName]?.root
|
|
98
|
+
: undefined;
|
|
99
|
+
return projectRoot != null
|
|
100
|
+
? [(0, node_path_1.join)(context.root, projectRoot), context.root]
|
|
101
|
+
: [context.root];
|
|
102
|
+
}
|
|
103
|
+
async function waitForServer(server, timeout, skipProxiedTls, npmConfigProxy) {
|
|
104
|
+
// Named the way it is probed: a port wins over a url when both are set.
|
|
105
|
+
const label = server.port != null ? `port ${server.port}` : redactedHref(server.url);
|
|
106
|
+
const deadline = Date.now() + timeout;
|
|
107
|
+
const schedule = [...RETRY_SCHEDULE];
|
|
108
|
+
let lastObserved;
|
|
109
|
+
while (true) {
|
|
110
|
+
const failure = await probeServer(server, deadline, skipProxiedTls, npmConfigProxy);
|
|
111
|
+
if (failure === null) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (failure !== NO_OBSERVATION) {
|
|
115
|
+
lastObserved = failure;
|
|
116
|
+
}
|
|
117
|
+
if (Date.now() >= deadline) {
|
|
118
|
+
throw new Error(`Timed out after ${timeout}ms waiting for the E2E web server at ${label} to be ready. Last observation: ${lastObserved ?? failure}. If the server now listens at a different address, run "nx reset" so the gate is re-inferred from the Playwright config. If a configuration-scoped env file (".env.<target>.<configuration>") sets the address, the gate is inferred without it; set "waitForWebServer" to false on @nx/playwright/plugin instead.`);
|
|
119
|
+
}
|
|
120
|
+
const delay = schedule.shift() ?? FALLBACK_RETRY_DELAY;
|
|
121
|
+
await sleep(Math.min(delay, Math.max(0, deadline - Date.now())));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Probe the way Playwright does: connect to the port on both loopback
|
|
125
|
+
// addresses, or poll the URL for a status from 200 to 403, through whatever
|
|
126
|
+
// proxy the environment configures for it. A port is always probed directly,
|
|
127
|
+
// because Playwright checks one with a raw socket too.
|
|
128
|
+
function probeServer(server, deadline, skipProxiedTls, npmConfigProxy) {
|
|
129
|
+
return server.port != null
|
|
130
|
+
? probePort(server.port, deadline)
|
|
131
|
+
: probeUrl(server.url, server.ignoreHTTPSErrors ?? false, deadline, skipProxiedTls, npmConfigProxy);
|
|
132
|
+
}
|
|
133
|
+
function probePort(port, deadline) {
|
|
134
|
+
// Silence for the retry-delay window is the probe's observation; clamp it so
|
|
135
|
+
// a connect that hangs (e.g. a firewalled port) cannot run past the
|
|
136
|
+
// readiness deadline. Floored at 1ms because setTimeout(0) disables the
|
|
137
|
+
// socket's idle timeout instead.
|
|
138
|
+
const timeout = Math.max(1, Math.min(FALLBACK_RETRY_DELAY, deadline - Date.now()));
|
|
139
|
+
const canConnect = (host) => new Promise((resolve) => {
|
|
140
|
+
const address = host.includes(':')
|
|
141
|
+
? `[${host}]:${port}`
|
|
142
|
+
: `${host}:${port}`;
|
|
143
|
+
const socket = (0, node_net_1.connect)({ port, host });
|
|
144
|
+
const settle = (failure) => {
|
|
145
|
+
socket.removeAllListeners();
|
|
146
|
+
socket.destroy();
|
|
147
|
+
resolve(failure);
|
|
148
|
+
};
|
|
149
|
+
socket.setTimeout(timeout);
|
|
150
|
+
socket.once('connect', () => settle(null));
|
|
151
|
+
socket.once('timeout', () => settle(`${address} timed out`));
|
|
152
|
+
socket.once('error', (error) => settle(`${address} ${error.code ?? error.message}`));
|
|
153
|
+
});
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const failures = [];
|
|
156
|
+
let pending = 2;
|
|
157
|
+
const onResult = (failure) => {
|
|
158
|
+
if (failure === null) {
|
|
159
|
+
resolve(null);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
failures.push(failure);
|
|
163
|
+
if (--pending === 0) {
|
|
164
|
+
resolve(failures.join('; '));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
void canConnect('127.0.0.1').then(onResult);
|
|
169
|
+
void canConnect('::1').then(onResult);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async function probeUrl(url, ignoreHTTPSErrors, deadline, skipProxiedTls, npmConfigProxy) {
|
|
173
|
+
// Validated before any server is probed, so this cannot throw.
|
|
174
|
+
const parsedUrl = new URL(url);
|
|
175
|
+
let response = await requestStatus(parsedUrl, ignoreHTTPSErrors, deadline, skipProxiedTls, npmConfigProxy);
|
|
176
|
+
if (response.status === 404 && parsedUrl.pathname === '/') {
|
|
177
|
+
const indexUrl = new URL(parsedUrl);
|
|
178
|
+
indexUrl.pathname = '/index.html';
|
|
179
|
+
response = await requestStatus(indexUrl, ignoreHTTPSErrors, deadline, skipProxiedTls, npmConfigProxy);
|
|
180
|
+
}
|
|
181
|
+
if (response.status >= 200 && response.status < 404) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return (response.failure ??
|
|
185
|
+
`${redactedHref(response.href)} responded with status ${response.status}${viaProxy(response.via)}`);
|
|
186
|
+
}
|
|
187
|
+
// The one place the phrase is written, so a failure the probe words and one the
|
|
188
|
+
// request words cannot drift apart.
|
|
189
|
+
function viaProxy(via) {
|
|
190
|
+
return via ? ` via the proxy at ${via}` : '';
|
|
191
|
+
}
|
|
192
|
+
// Probe failures reach the task log, so a URL's credentials come off before it
|
|
193
|
+
// enters one. Callers only pass URLs that already round-tripped through `URL`.
|
|
194
|
+
function redactedHref(href) {
|
|
195
|
+
const url = new URL(href);
|
|
196
|
+
if (!url.username && !url.password) {
|
|
197
|
+
return href;
|
|
198
|
+
}
|
|
199
|
+
url.username = '***';
|
|
200
|
+
url.password = '';
|
|
201
|
+
return url.href;
|
|
202
|
+
}
|
|
203
|
+
function requestStatus(url, ignoreHTTPSErrors, deadline, skipProxiedTls, npmConfigProxy, redirects = 0) {
|
|
204
|
+
// A chain that ran out of budget still told us the server is answering, so it
|
|
205
|
+
// is a real observation rather than silence, whether the budget ran out
|
|
206
|
+
// between hops or inside the last one.
|
|
207
|
+
const redirectFailure = redirects > 0
|
|
208
|
+
? `${redactedHref(url.href)} redirected ${redirects} times without resolving`
|
|
209
|
+
: undefined;
|
|
210
|
+
const remaining = deadline - Date.now();
|
|
211
|
+
if (remaining <= 0) {
|
|
212
|
+
return Promise.resolve({
|
|
213
|
+
href: url.href,
|
|
214
|
+
status: 0,
|
|
215
|
+
failure: redirectFailure ?? NO_OBSERVATION,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// Resolved per URL rather than once per probe so a redirect that changes
|
|
219
|
+
// protocol re-selects the way Playwright's own recursion does.
|
|
220
|
+
const resolution = (0, proxy_1.resolveProxyForUrl)(url, process.env, npmConfigProxy);
|
|
221
|
+
if (resolution.kind === 'unusable') {
|
|
222
|
+
return Promise.resolve({
|
|
223
|
+
href: url.href,
|
|
224
|
+
status: 0,
|
|
225
|
+
failure: `${redactedHref(url.href)} ${unusableProxyProblem(resolution)}`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const via = resolution.kind === 'proxy' ? resolution.proxy.host : undefined;
|
|
229
|
+
const describe = (detail) => `${redactedHref(url.href)} ${detail}${viaProxy(via)}`;
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
const requestOptions = {
|
|
232
|
+
method: 'GET',
|
|
233
|
+
headers: { Accept: '*/*' },
|
|
234
|
+
rejectUnauthorized: !ignoreHTTPSErrors,
|
|
235
|
+
};
|
|
236
|
+
let request;
|
|
237
|
+
let settled = false;
|
|
238
|
+
// Until the agent finishes `CONNECT` the request holds no socket, so
|
|
239
|
+
// destroying it cannot reach the connection to the proxy. Aborting is what
|
|
240
|
+
// closes that leg; without it a proxy that never answers leaves a live
|
|
241
|
+
// socket nothing else can reach.
|
|
242
|
+
const connecting = new AbortController();
|
|
243
|
+
// Declared ahead of the settle it is cleared by, which the timer's own
|
|
244
|
+
// callback in turn calls.
|
|
245
|
+
let budget;
|
|
246
|
+
const settle = (response) => {
|
|
247
|
+
if (settled) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
settled = true;
|
|
251
|
+
clearTimeout(budget);
|
|
252
|
+
request?.destroy();
|
|
253
|
+
connecting.abort();
|
|
254
|
+
resolve(response);
|
|
255
|
+
};
|
|
256
|
+
// The rest of the readiness budget bounds the request in wall-clock time. A
|
|
257
|
+
// request arms its own idle timeout only once it holds a socket, and one
|
|
258
|
+
// waiting on a proxy that accepted the connection but never answered
|
|
259
|
+
// `CONNECT` never gets that far. Unreferenced because it is a bound rather
|
|
260
|
+
// than work of its own: it still fires while a request is in flight, and
|
|
261
|
+
// cannot hold the process open once nothing else does.
|
|
262
|
+
budget = setTimeout(() => {
|
|
263
|
+
// Silence for the window the port probe allows is an observation of the
|
|
264
|
+
// server. A request cut off with less budget than that only means the
|
|
265
|
+
// deadline arrived mid-flight, and must not displace what an earlier
|
|
266
|
+
// probe saw.
|
|
267
|
+
settle({
|
|
268
|
+
href: url.href,
|
|
269
|
+
status: 0,
|
|
270
|
+
failure: redirectFailure ??
|
|
271
|
+
(remaining >= FALLBACK_RETRY_DELAY
|
|
272
|
+
? describe('did not respond')
|
|
273
|
+
: NO_OBSERVATION),
|
|
274
|
+
});
|
|
275
|
+
}, remaining).unref();
|
|
276
|
+
const onResponse = (response) => {
|
|
277
|
+
const status = response.statusCode ?? 0;
|
|
278
|
+
const location = response.headers.location;
|
|
279
|
+
response.resume();
|
|
280
|
+
// Follow redirects like Playwright and evaluate the final
|
|
281
|
+
// destination, so a redirect to an unavailable page is not read as
|
|
282
|
+
// ready. The readiness deadline bounds the chain instead of a hop
|
|
283
|
+
// count, which would fail a long chain Playwright follows to a
|
|
284
|
+
// ready server. Treat a malformed chain as not ready.
|
|
285
|
+
if (status >= 300 && status < 400 && location) {
|
|
286
|
+
let redirectUrl;
|
|
287
|
+
try {
|
|
288
|
+
redirectUrl = new URL(location, url);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
settle({
|
|
292
|
+
href: url.href,
|
|
293
|
+
status: 0,
|
|
294
|
+
// Node's header parser passes tab and the C1 controls through, and
|
|
295
|
+
// the value reaches a terminal from here. Unparseable for URL, so
|
|
296
|
+
// credentials come off with the string-based treatment.
|
|
297
|
+
failure: describe(`redirected to an invalid location "${(0, proxy_1.withoutCredentials)(location.replace(/[\x00-\x1f\x7f-\x9f]/g, ''))}"`),
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (redirectUrl.protocol !== 'http:' &&
|
|
302
|
+
redirectUrl.protocol !== 'https:') {
|
|
303
|
+
// Classified like an unparseable location rather than followed into
|
|
304
|
+
// `http.request`, which would throw synchronously and fail the wait
|
|
305
|
+
// outright. A boot-time redirect can still settle into a ready
|
|
306
|
+
// server within the budget.
|
|
307
|
+
settle({
|
|
308
|
+
href: url.href,
|
|
309
|
+
status: 0,
|
|
310
|
+
failure: describe(`redirected to "${redactedHref(redirectUrl.href)}", which is not an http or https url`),
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
settle(requestStatus(redirectUrl, ignoreHTTPSErrors, deadline, skipProxiedTls, npmConfigProxy, redirects + 1));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
settle({ href: url.href, via, status });
|
|
318
|
+
};
|
|
319
|
+
switch (resolution.kind) {
|
|
320
|
+
case 'direct':
|
|
321
|
+
// `localhost` can resolve to ::1 first while the server listens on
|
|
322
|
+
// 127.0.0.1 only; Happy Eyeballs tries both families the way
|
|
323
|
+
// Playwright's probe agent does, on every Node line regardless of the
|
|
324
|
+
// process-wide default.
|
|
325
|
+
// The cast is needed because @types/node's RequestOptions omits the
|
|
326
|
+
// option; Node forwards request options to socket.connect, which
|
|
327
|
+
// honors it.
|
|
328
|
+
request = (url.protocol === 'https:' ? https : http).request(url, {
|
|
329
|
+
...requestOptions,
|
|
330
|
+
autoSelectFamily: true,
|
|
331
|
+
}, onResponse);
|
|
332
|
+
break;
|
|
333
|
+
case 'proxy':
|
|
334
|
+
request = proxiedRequest(url, resolution.proxy, requestOptions, skipProxiedTls, connecting.signal, onResponse);
|
|
335
|
+
break;
|
|
336
|
+
default: {
|
|
337
|
+
// Both kinds that reach here route the request somewhere; a new one has
|
|
338
|
+
// to say where.
|
|
339
|
+
const unhandled = resolution;
|
|
340
|
+
throw new Error(`Unhandled proxy resolution ${JSON.stringify(unhandled)}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Not `once`: aborting the connection raises a second error on a request
|
|
344
|
+
// that has already been settled, and an emitter left without a listener
|
|
345
|
+
// would throw it at the process.
|
|
346
|
+
request.on('error', (error) => {
|
|
347
|
+
const detail = error.code ?? error.message;
|
|
348
|
+
settle({
|
|
349
|
+
href: url.href,
|
|
350
|
+
status: 0,
|
|
351
|
+
failure: describe(redirects > 0 ? `${detail} after ${redirects} redirects` : detail),
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
request.end();
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
// An http target is named in the request line and the request itself goes to
|
|
358
|
+
// the proxy, which is what Playwright does. An https target cannot be named
|
|
359
|
+
// that way, so the agent opens a tunnel with `CONNECT` and TLS runs over it.
|
|
360
|
+
// Proxy credentials travel differently on the two routes, on the URL for the
|
|
361
|
+
// first and as `Proxy-Authorization` for the second, and both match Playwright.
|
|
362
|
+
function proxiedRequest(url, proxy, options, skipProxiedTls, signal, onResponse) {
|
|
363
|
+
if (url.protocol === 'https:') {
|
|
364
|
+
const agent = new https_proxy_agent_1.HttpsProxyAgent(proxy);
|
|
365
|
+
// The agent dials the proxy itself, so this is the only handle on that
|
|
366
|
+
// connection. A signal in the request options does not reach it.
|
|
367
|
+
agent.connectOpts.signal = signal;
|
|
368
|
+
return https.request(url, {
|
|
369
|
+
...options,
|
|
370
|
+
// Playwright before 1.59.0 never verifies the origin certificate on
|
|
371
|
+
// this route, whatever the caller asked for.
|
|
372
|
+
// TODO(v25): drop once minSupportedPlaywrightVersion reaches 1.59.0.
|
|
373
|
+
...(skipProxiedTls ? { rejectUnauthorized: false } : {}),
|
|
374
|
+
agent,
|
|
375
|
+
}, onResponse);
|
|
376
|
+
}
|
|
377
|
+
// Playwright's Happy Eyeballs agent also dials the proxy on this route (its
|
|
378
|
+
// CONNECT route above drops it, so that one stays default). Same cast as the
|
|
379
|
+
// direct probe: @types/node's RequestOptions omits the option.
|
|
380
|
+
return (proxy.protocol === 'https:' ? https : http).request(proxy, {
|
|
381
|
+
...options,
|
|
382
|
+
path: url.href,
|
|
383
|
+
autoSelectFamily: true,
|
|
384
|
+
}, onResponse);
|
|
385
|
+
}
|
|
386
|
+
function sleep(ms) {
|
|
387
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
388
|
+
}
|
|
@@ -30,8 +30,7 @@ export interface ConfigurationGeneratorSchema {
|
|
|
30
30
|
addPlugin?: boolean;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export interface NormalizedGeneratorOptions
|
|
34
|
-
extends ConfigurationGeneratorSchema {
|
|
33
|
+
export interface NormalizedGeneratorOptions extends ConfigurationGeneratorSchema {
|
|
35
34
|
addPlugin: boolean;
|
|
36
35
|
directory: string;
|
|
37
36
|
linter: LinterType;
|
|
@@ -2,6 +2,21 @@ import { type CreateNodes } from '@nx/devkit';
|
|
|
2
2
|
export interface PlaywrightPluginOptions {
|
|
3
3
|
targetName?: string;
|
|
4
4
|
ciTargetName?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Maximum time in milliseconds the inferred web server readiness task waits
|
|
7
|
+
* for a server to be ready before failing. Overrides the `timeout` each
|
|
8
|
+
* Playwright `webServer` configures. Defaults to that configured timeout,
|
|
9
|
+
* or 60000 when neither is set.
|
|
10
|
+
*/
|
|
11
|
+
webServerTimeout?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Whether to infer a task that waits for the web server to be ready before the
|
|
14
|
+
* Playwright test tasks run. When false, no readiness task is inferred and the
|
|
15
|
+
* tests fall back to Playwright's own `reuseExistingServer` probe. Defaults to
|
|
16
|
+
* true.
|
|
17
|
+
*/
|
|
18
|
+
waitForWebServer?: boolean;
|
|
5
19
|
}
|
|
6
20
|
export declare const createNodes: CreateNodes<PlaywrightPluginOptions>;
|
|
7
21
|
export declare const createNodesV2: CreateNodes<PlaywrightPluginOptions>;
|
|
22
|
+
export declare function _clearWarnedUnparseableCommands(): void;
|