@swell/cli
Version:
Swell's command line interface/utility
89 lines (88 loc) • 3.24 kB
JavaScript
import { bin, install, Tunnel } from 'cloudflared';
import localtunnel from 'localtunnel';
import * as fs from 'node:fs/promises';
import ora from 'ora';
import { LOCAL_PROXY_PROVIDER } from './constants.js';
let activeTunnel = null;
let cleanupRegistered = false;
async function startCloudflaredTunnel(port) {
// Stop existing tunnel if called again
if (activeTunnel) {
activeTunnel.stop();
activeTunnel = null;
}
// Ensure cloudflared binary is installed
const exists = await fs.access(bin, fs.constants.X_OK).then(() => true, (_error) => false);
if (!exists) {
const spinner = ora('Installing cloudflared tunnel (first time only)...').start();
await install(bin);
spinner.stop();
}
// Use Tunnel.quick() for quick tunnels without a Cloudflare account
const tunnel = Tunnel.quick(`http://localhost:${port}`);
activeTunnel = tunnel;
// Wait for the URL to be available
const url = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
tunnel.stop();
activeTunnel = null;
reject(new Error('Timeout waiting for cloudflared tunnel URL. Check if cloudflared can connect to Cloudflare.'));
}, 60000);
tunnel.on('url', (tunnelUrl) => {
clearTimeout(timeout);
resolve(tunnelUrl);
});
tunnel.on('error', (error) => {
clearTimeout(timeout);
activeTunnel = null;
reject(error);
});
});
// Register cleanup handlers only once, referencing activeTunnel for current tunnel
if (!cleanupRegistered) {
cleanupRegistered = true;
process.on('exit', () => activeTunnel?.stop());
process.on('SIGINT', () => {
activeTunnel?.stop();
// eslint-disable-next-line no-process-exit
process.exit();
});
process.on('SIGTERM', () => {
activeTunnel?.stop();
// eslint-disable-next-line no-process-exit
process.exit();
});
}
return url;
}
export async function getProxyUrl(port, context) {
const provider = LOCAL_PROXY_PROVIDER || 'cloudflared';
// URL template mode: resolve placeholders and return directly
if (provider.includes('://')) {
return provider
.replace('{port}', String(port))
.replace('{appid}', context?.appId || '')
.replace('{storeid}', context?.storeId || '')
.replace('{storefrontid}', context?.storefrontId || '');
}
try {
switch (provider) {
case 'local': {
return `http://localhost:${port}`;
}
case 'localtunnel': {
const tunnel = await localtunnel({ port });
return tunnel.url;
}
// eslint-disable-next-line unicorn/no-useless-switch-case
case 'cloudflared':
default: {
return await startCloudflaredTunnel(port);
}
}
}
catch (error) {
console.error(error);
throw new Error(`Unable to start tunnel on port ${port} (${provider}): ${error.message}`, { cause: error });
}
}