feat: add ability to use custom tunnel in dev cmd (#597)

* feat: add ability to use custom tunnel in dev cmd

* Added a short flag -t for the tunnel flag

* skip framework url resolution if tunnel-url is provided

* remove type annotation

---------

Co-authored-by: Eric Allam <eric@trigger.dev>
Co-authored-by: Matt Aitken <matt@mattaitken.com>
This commit is contained in:
Hemachandar
2023-10-19 20:41:08 +05:30
committed by GitHub
parent 1524c95a7a
commit 50e3192453
3 changed files with 104 additions and 45 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
add ability to use custom tunnel in dev command
+4
View File
@@ -59,6 +59,10 @@ program
"The URI path to the API handler function to use for this project.",
"/api/trigger"
)
.option(
"-t, --tunnel <url>",
"An optional custom tunnel URL. Use only if you already have an open tunnel to your local dev server."
)
.version(getVersion(), "-v, --version", "Display the version number")
.action(async (path, options) => {
try {
+95 -45
View File
@@ -28,6 +28,11 @@ export const DevCommandOptionsSchema = z.object({
envFile: z.string().optional(),
handlerPath: z.string(),
clientId: z.string().optional(),
tunnel: z
.string()
.url()
.regex(/^(http|https).+/, "only http/https URLs are accepted")
.optional(),
});
export type DevCommandOptions = z.infer<typeof DevCommandOptionsSchema>;
@@ -43,6 +48,29 @@ const formattedDate = new Intl.DateTimeFormat("en", {
let runtime: JsRuntime;
type TunnelUrl = {
type: "tunnel";
url: string;
};
type ResolvedUrl = {
type: "resolved";
hostname: string;
port: number;
};
type ServerUrl = TunnelUrl | ResolvedUrl;
type TunnelEndpoint = TunnelUrl & {
handlerPath: string;
};
type ResolvedEndpoint = ResolvedUrl & {
handlerPath: string;
};
type ServerEndpoint = TunnelEndpoint | ResolvedEndpoint;
export async function devCommand(path: string, anyOptions: any) {
telemetryClient.dev.started(path, anyOptions);
@@ -90,23 +118,23 @@ export async function devCommand(path: string, anyOptions: any) {
logger.error(
`✖ [trigger.dev] Your endpoint couldn't be verified. Make sure your app is running and try again. ${resolvedOptions.handlerPath}`
);
logger.info(` [trigger.dev] You can use -H to specify a hostname, or -p to specify a port.`);
logger.info(
` [trigger.dev] You can use -H to specify a hostname, or -p to specify a port, or -t to specify the tunnel-url pointing to the local dev server.`
);
telemetryClient.dev.failed("no_server_found", resolvedOptions);
return;
}
const { hostname, port, handlerPath } = verifiedEndpoint;
telemetryClient.dev.serverRunning(path, resolvedOptions);
// Setup tunnel
const endpointUrl = await resolveEndpointUrl(apiUrl, port, hostname);
const endpointUrl = await resolveEndpointUrl(apiUrl, verifiedEndpoint);
if (!endpointUrl) {
telemetryClient.dev.failed("failed_to_create_tunnel", resolvedOptions);
return;
}
const endpointHandlerUrl = `${endpointUrl}${handlerPath}`;
const endpointHandlerUrl = `${endpointUrl}${verifiedEndpoint.handlerPath}`;
telemetryClient.dev.tunnelRunning(path, resolvedOptions);
// Watch for changes to files and refresh endpoints
@@ -306,6 +334,7 @@ async function resolveOptions(
envFile: unresolvedOptions.envFile ?? ".env",
handlerPath: unresolvedOptions.handlerPath,
clientId: unresolvedOptions.clientId,
tunnel: unresolvedOptions.tunnel,
};
}
@@ -318,6 +347,7 @@ async function resolveOptions(
envFile: unresolvedOptions.envFile ?? envName ?? ".env",
handlerPath: unresolvedOptions.handlerPath,
clientId: unresolvedOptions.clientId,
tunnel: unresolvedOptions.tunnel,
};
}
@@ -327,40 +357,15 @@ async function verifyEndpoint(
apiKey: string,
framework?: Framework
) {
//create list of hostnames to try
const hostnames = [];
if (resolvedOptions.hostname) {
hostnames.push(resolvedOptions.hostname);
}
if (framework) {
hostnames.push(...framework.defaultHostnames);
} else {
hostnames.push("localhost");
}
const serverUrls = findServerUrls(resolvedOptions, framework);
//create list of ports to try
const ports = [];
if (resolvedOptions.port) {
ports.push(resolvedOptions.port);
}
if (framework) {
ports.push(...framework.defaultPorts);
} else {
ports.push(3000);
}
//create list of urls to try
const urls: { hostname: string; port: number }[] = [];
for (const hostname of hostnames) {
for (const port of ports) {
urls.push({ hostname, port });
}
}
//try each hostname
for (const url of urls) {
const { hostname, port } = url;
const localEndpointHandlerUrl = `http://${hostname}:${port}${resolvedOptions.handlerPath}`;
//try each url
for (const serverUrl of serverUrls) {
const url =
serverUrl.type === "tunnel"
? serverUrl.url
: `http://${serverUrl.hostname}:${serverUrl.port}`;
const localEndpointHandlerUrl = `${url}${resolvedOptions.handlerPath}`;
const spinner = ora(
`[trigger.dev] Looking for your trigger endpoint: ${localEndpointHandlerUrl}`
@@ -384,7 +389,8 @@ async function verifyEndpoint(
}
spinner.succeed(`[trigger.dev] Found your trigger endpoint: ${localEndpointHandlerUrl}`);
return { hostname, port, handlerPath: resolvedOptions.handlerPath };
return { ...serverUrl, handlerPath: resolvedOptions.handlerPath };
} catch (err) {
spinner.fail(`[trigger.dev] No server found (${localEndpointHandlerUrl}).`);
}
@@ -399,17 +405,61 @@ export function getEndpointId(runtime: JsRuntime, clientId?: string) {
} else return runtime.getEndpointId();
}
async function resolveEndpointUrl(apiUrl: string, port: number, hostname: string) {
function findServerUrls(resolvedOptions: ResolvedOptions, framework?: Framework): ServerUrl[] {
if (resolvedOptions.tunnel) {
logger.info(` Using provided tunnel URL: ${resolvedOptions.tunnel}`);
return [{ type: "tunnel", url: resolvedOptions.tunnel }];
}
//create list of hostnames to try
const hostnames = [];
if (resolvedOptions.hostname) {
hostnames.push(resolvedOptions.hostname);
}
if (framework) {
hostnames.push(...framework.defaultHostnames);
} else {
hostnames.push("localhost");
}
//create list of ports to try
const ports = [];
if (resolvedOptions.port) {
ports.push(resolvedOptions.port);
}
if (framework) {
ports.push(...framework.defaultPorts);
} else {
ports.push(3000);
}
//create list of urls to try
const urls: ResolvedUrl[] = [];
for (const hostname of hostnames) {
for (const port of ports) {
urls.push({ type: "resolved", hostname, port });
}
}
return urls;
}
async function resolveEndpointUrl(apiUrl: string, endpoint: ServerEndpoint) {
// use tunnel URL if provided
if (endpoint.type === "tunnel") {
return endpoint.url;
}
const apiURL = new URL(apiUrl);
//if the API is localhost and the hostname is localhost
if (apiURL.hostname === "localhost" && hostname === "localhost") {
return `http://${hostname}:${port}`;
// if the API is localhost and the hostname is localhost
if (apiURL.hostname === "localhost" && endpoint.hostname === "localhost") {
return `http://${endpoint.hostname}:${endpoint.port}`;
}
// Setup tunnel
const tunnelSpinner = ora(`🚇 Creating tunnel`).start();
const tunnelUrl = await createTunnel(hostname, port, tunnelSpinner);
const tunnelUrl = await createTunnel(endpoint.hostname, endpoint.port, tunnelSpinner);
if (tunnelUrl) {
tunnelSpinner.succeed(`🚇 Created tunnel: ${tunnelUrl}`);
@@ -442,7 +492,7 @@ async function createTunnel(hostname: string, port: number, spinner: Ora) {
error.message.includes("connect ECONNREFUSED 127.0.0.1:4041")
) {
spinner.fail(
`Ngrok failed to create a tunnel for port ${port} because ngrok is already running`
`Ngrok failed to create a tunnel for port ${port} because ngrok is already running.\n You may want to use -t flag to use an existing URL that points to the local dev server.`
);
return;
}