nuxt-modules / ngrok

ngrok exposes your localhost to the world for easy testing and sharing! No need to mess with DNS or deploy just to have others test out your changes
https://ngrok.nuxtjs.org/
68 stars 13 forks source link

Nuxt 3 Support #21

Open silverbackdan opened 2 years ago

silverbackdan commented 2 years ago

I've taken the liberty of using this module as a basis and creating a Nuxt 3 compatible module:

import ngrok from "ngrok";
import type { Ngrok } from "ngrok";
import chalk from "chalk";
import { defineNuxtModule } from "@nuxt/kit";
import type { Server as HttpServer } from "http";
import type { Server as HttpsServer } from "https";
import consola from "consola";

// https://github.com/bubenshchykov/ngrok
export type ModuleOptions = Partial<Ngrok.Options>;
declare module "@nuxt/schema" {
  interface NuxtConfig {
    [CONFIG_KEY]?: ModuleOptions;
  }
}
const CONFIG_KEY = "ngrok";

export default defineNuxtModule({
  meta: {
    // Usually  npm package name of your module
    name: "ngrok",
    // The key in `nuxt.config` that holds your module options
    configKey: CONFIG_KEY,
    // Compatibility constraints
    compatibility: {
      nuxt: "^3.0.0",
    },
  },
  // Default configuration options for your module
  defaults: {
    authtoken: process.env.NGROK_TOKEN,
    auth: process.env.NGROK_AUTH,
  },
  hooks: {},
  async setup(moduleOptions, nuxt) {
    // Don't start NGROK in production mode
    if (nuxt.options.dev === false) {
      return;
    }
    if (!moduleOptions.auth) {
      // eslint-disable-next-line no-console
      consola.warn(
        "[ngrok] Dev server exposed to internet without password protection! Consider using `ngrok.auth` options"
      );
    }
    // Start NGROK when Nuxt server is listening
    let url: string;

    nuxt.hook(
      "listen",
      async (_server: HttpServer | HttpsServer, listener: any) => {
        if (moduleOptions.authtoken) {
          await ngrok.authtoken(moduleOptions.authtoken);
        }
        const { port } = new URL(listener.url);
        url = await ngrok.connect({
          ...moduleOptions,
          addr: port,
        } as Ngrok.Options);

        nuxt.options.publicRuntimeConfig.ngrok = { url };
        consola.success(chalk.underline.green(`ngrok connected: ${url}`));
      }
    );

    // Disconnect ngrok connection when closing nuxt
    nuxt.hook("close", () => {
      if (url) {
        ngrok.disconnect();
        consola.success(chalk.underline.yellow("ngrok disconnected"));
        url = null;
      }
    });
  },
});

There is a bug in Nuxt 3 where if we modify the nuxt config file, the listen hook is not called again after the close hook.

Additionally, right now I seem to have to use this config:

  vite: {
    server: {
      hmr: {
        protocol: "ws",
        host: "127.0.0.1",
      },
    }
  }

This prevents an infinite reload when accessing via the ngrok domain..

This is a WIP and no sure how to resolve the vite hmr issue in a more robust way right now. Happy for someone else to take over or if there's a solution I'm happy to implement and create a PR.

silverbackdan commented 2 years ago

I have a working module for Nuxt 3 if interested:

import ngrok from "ngrok";
import type { Ngrok } from "ngrok";
import chalk from "chalk";
import { defineNuxtModule, addTemplate } from "@nuxt/kit";
import consola from "consola";
import { IncomingMessage } from "connect";
import { ServerResponse } from "http";

// https://github.com/bubenshchykov/ngrok
export type ModuleOptions = Partial<Ngrok.Options>;

const CONFIG_KEY = "ngrok";

export default defineNuxtModule({
  meta: {
    // Usually  npm package name of your module
    name: "ngrok",
    // The key in `nuxt.config` that holds your module options
    configKey: CONFIG_KEY,
    // Compatibility constraints
    compatibility: {
      nuxt: "^3.0.0",
    },
  },
  // Default configuration options for your module
  defaults: {
    authtoken: process.env.NGROK_TOKEN,
    auth: process.env.NGROK_AUTH,
  },
  hooks: {},
  setup: async (moduleOptions: ModuleOptions, nuxt) => {
    const CREATE_NGROK_TEMPLATE = (url) => {
      addTemplate({
        filename: 'ngrok.mjs',
        write: true,
        getContents: () => `export const url = ${JSON.stringify(url)}`,
      });
    }

    // Don't start NGROK in production mode
    if (nuxt.options.dev === false) {
      CREATE_NGROK_TEMPLATE(null)
      return;
    }
    if (!moduleOptions.auth) {
      // eslint-disable-next-line no-console
      consola.warn(
        "[ngrok] Dev server exposed to internet without password protection! Consider using `ngrok.auth` options"
      );
    }

    let url: string;

    nuxt.hook("listen", async (_server: any, { port }: { port: number }) => {
      if (moduleOptions.authToken) {
        await ngrok.authtoken(moduleOptions.authToken);
      }

      url = await ngrok.connect({
        ...moduleOptions,
        addr: port,
      } as Ngrok.Options);

      CREATE_NGROK_TEMPLATE(url)

      consola.success(chalk.underline.green(`ngrok connected: ${url}`));
    });

    // Disconnect ngrok connection when closing nuxt
    nuxt.hook("close", () => {
      if (url) {
        ngrok.disconnect();
        consola.success(chalk.underline.yellow("ngrok disconnected"));
        url = null;
      }
    });
  },
});

Instead of populating the public runtime config it creates a build file in .nuxt/ngrok.mjs

You can import this elsewhere

import { url as ngrokUrl } from '#build/ngrok.mjs'

That's all :)

silverbackdan commented 2 years ago

I now have an issue, I was able to solve the web socket infinite reloading because connection failed before using this in nuxt config

server: {
            hmr: {
                protocol: "ws",
                host: "127.0.0.1",
            },
        }

But this workaround no longer works over http or https.. is anyone able to offer any clues on how to make this work?