2023-07-18 00:17:59 +00:00
|
|
|
import { spawn } from 'node:child_process';
|
|
|
|
import { fileURLToPath } from 'node:url';
|
2022-07-27 15:50:48 +00:00
|
|
|
import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js';
|
2023-09-22 14:58:00 +00:00
|
|
|
import * as net from 'node:net';
|
2022-07-27 15:50:48 +00:00
|
|
|
export { fixLineEndings } from '../../../astro/test/test-utils.js';
|
2023-05-17 13:23:20 +00:00
|
|
|
/**
|
2023-09-22 14:58:00 +00:00
|
|
|
* @typedef {{ stop: Promise<void>, port: number }} WranglerCLI
|
2023-05-17 13:23:20 +00:00
|
|
|
* @typedef {import('../../../astro/test/test-utils').Fixture} Fixture
|
|
|
|
*/
|
|
|
|
|
2022-07-27 15:50:48 +00:00
|
|
|
export function loadFixture(config) {
|
|
|
|
if (config?.root) {
|
|
|
|
config.root = new URL(config.root, import.meta.url);
|
|
|
|
}
|
|
|
|
return baseLoadFixture(config);
|
|
|
|
}
|
2022-07-27 20:15:19 +00:00
|
|
|
|
2022-07-27 20:17:38 +00:00
|
|
|
const wranglerPath = fileURLToPath(
|
|
|
|
new URL('../node_modules/wrangler/bin/wrangler.js', import.meta.url)
|
|
|
|
);
|
2022-07-27 20:15:19 +00:00
|
|
|
|
2023-09-22 14:58:00 +00:00
|
|
|
let lastPort = 8788;
|
|
|
|
|
2023-07-17 12:30:02 +00:00
|
|
|
/**
|
2023-08-15 17:10:51 +00:00
|
|
|
* @returns {Promise<WranglerCLI>}
|
2023-07-17 12:30:02 +00:00
|
|
|
*/
|
2023-09-22 14:58:00 +00:00
|
|
|
export async function runCLI(
|
|
|
|
basePath,
|
|
|
|
{
|
|
|
|
silent,
|
|
|
|
maxAttempts = 3,
|
|
|
|
timeoutMillis = 2500, // really short because it often seems to just hang on the first try, but work subsequently, no matter the wait
|
|
|
|
backoffFactor = 2, // | - 2.5s -- 5s ---- 10s -> onTimeout
|
|
|
|
onTimeout = (ex) => {
|
|
|
|
new Error(`Timed out starting the wrangler CLI after ${maxAttempts} tries.`, { cause: ex });
|
|
|
|
},
|
|
|
|
}
|
|
|
|
) {
|
|
|
|
let triesRemaining = maxAttempts;
|
|
|
|
let timeout = timeoutMillis;
|
|
|
|
let cli;
|
|
|
|
let lastErr;
|
|
|
|
while (triesRemaining > 0) {
|
2023-09-22 15:00:18 +00:00
|
|
|
cli = await tryRunCLI(basePath, {
|
|
|
|
silent,
|
|
|
|
timeout,
|
|
|
|
forceRotatePort: triesRemaining !== maxAttempts,
|
|
|
|
});
|
2023-09-22 14:58:00 +00:00
|
|
|
try {
|
|
|
|
await cli.ready;
|
|
|
|
return cli;
|
|
|
|
} catch (err) {
|
|
|
|
lastErr = err;
|
|
|
|
console.error((err.message || err.name || err) + ' after ' + timeout + 'ms');
|
|
|
|
cli.stop();
|
|
|
|
triesRemaining -= 1;
|
|
|
|
timeout *= backoffFactor;
|
|
|
|
}
|
2023-08-15 17:10:51 +00:00
|
|
|
}
|
2023-09-22 14:58:00 +00:00
|
|
|
onTimeout(lastErr);
|
|
|
|
return cli;
|
|
|
|
}
|
2023-08-15 17:10:51 +00:00
|
|
|
|
2023-09-22 14:58:00 +00:00
|
|
|
async function tryRunCLI(basePath, { silent, timeout, forceRotatePort = false }) {
|
|
|
|
const port = await getNextOpenPort(lastPort + (forceRotatePort ? 1 : 0));
|
|
|
|
lastPort = port;
|
|
|
|
|
|
|
|
const fixtureDir = fileURLToPath(new URL(`${basePath}`, import.meta.url));
|
|
|
|
const p = spawn(
|
|
|
|
'node',
|
|
|
|
[
|
|
|
|
wranglerPath,
|
|
|
|
'pages',
|
|
|
|
'dev',
|
|
|
|
'dist',
|
|
|
|
'--port',
|
|
|
|
port,
|
|
|
|
'--log-level',
|
|
|
|
'info',
|
|
|
|
'--persist-to',
|
|
|
|
'.wrangler/state',
|
|
|
|
],
|
|
|
|
{
|
|
|
|
cwd: fixtureDir,
|
|
|
|
}
|
|
|
|
);
|
2022-07-27 20:15:19 +00:00
|
|
|
|
|
|
|
p.stderr.setEncoding('utf-8');
|
|
|
|
p.stdout.setEncoding('utf-8');
|
|
|
|
|
|
|
|
const ready = new Promise(async (resolve, reject) => {
|
2023-08-15 17:13:43 +00:00
|
|
|
const failed = setTimeout(() => {
|
2023-09-22 14:58:00 +00:00
|
|
|
p.kill('SIGKILL');
|
2023-08-15 17:13:43 +00:00
|
|
|
reject(new Error(`Timed out starting the wrangler CLI`));
|
|
|
|
}, timeout);
|
2022-07-27 20:15:19 +00:00
|
|
|
|
2023-09-22 14:58:00 +00:00
|
|
|
const success = () => {
|
|
|
|
clearTimeout(failed);
|
|
|
|
resolve();
|
|
|
|
};
|
2022-07-27 20:15:19 +00:00
|
|
|
|
2023-09-22 14:58:00 +00:00
|
|
|
p.on('exit', (code) => reject(`wrangler terminated unexpectedly with exit code ${code}`));
|
|
|
|
|
|
|
|
p.stderr.on('data', (data) => {
|
2022-07-27 20:17:38 +00:00
|
|
|
if (!silent) {
|
2023-09-22 14:58:00 +00:00
|
|
|
process.stdout.write(data);
|
2022-07-27 20:15:19 +00:00
|
|
|
}
|
2023-09-22 14:58:00 +00:00
|
|
|
});
|
|
|
|
let allData = '';
|
|
|
|
p.stdout.on('data', (data) => {
|
|
|
|
if (!silent) {
|
|
|
|
process.stdout.write(data);
|
2022-07-27 20:15:19 +00:00
|
|
|
}
|
2023-09-22 14:58:00 +00:00
|
|
|
allData += data;
|
|
|
|
if (allData.includes(`[mf:inf] Ready on`)) {
|
|
|
|
success();
|
|
|
|
}
|
|
|
|
});
|
2022-07-27 20:15:19 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
2023-09-22 14:58:00 +00:00
|
|
|
port,
|
2022-07-27 20:15:19 +00:00
|
|
|
ready,
|
|
|
|
stop() {
|
2023-06-21 13:09:49 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
2023-09-22 14:58:00 +00:00
|
|
|
const timer = setTimeout(() => {
|
|
|
|
p.kill('SIGKILL');
|
|
|
|
}, 1000);
|
|
|
|
p.on('close', () => {
|
|
|
|
clearTimeout(timer);
|
|
|
|
resolve();
|
|
|
|
});
|
2023-06-21 13:09:49 +00:00
|
|
|
p.on('error', (err) => reject(err));
|
|
|
|
p.kill();
|
|
|
|
});
|
2022-07-27 20:17:38 +00:00
|
|
|
},
|
|
|
|
};
|
2022-07-27 20:15:19 +00:00
|
|
|
}
|
2023-09-22 14:58:00 +00:00
|
|
|
|
|
|
|
const isPortOpen = async (port) => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let s = net.createServer();
|
|
|
|
s.once('error', (err) => {
|
|
|
|
s.close();
|
|
|
|
if (err['code'] == 'EADDRINUSE') {
|
|
|
|
resolve(false);
|
|
|
|
} else {
|
|
|
|
reject(err);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
s.once('listening', () => {
|
|
|
|
resolve(true);
|
|
|
|
s.close();
|
|
|
|
});
|
2023-09-22 15:00:18 +00:00
|
|
|
s.listen(port, '0.0.0.0');
|
2023-09-22 14:58:00 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const getNextOpenPort = async (startFrom) => {
|
|
|
|
let openPort = null;
|
|
|
|
while (startFrom < 65535 || !!openPort) {
|
|
|
|
if (await isPortOpen(startFrom)) {
|
|
|
|
openPort = startFrom;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
startFrom++;
|
|
|
|
}
|
|
|
|
return openPort;
|
|
|
|
};
|