0.0.4 • Published 4 months ago

@giacomorebonato/vavite v0.0.4

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

This is a fork of https://github.com/cyco130/vavite

Va vite!

vavite is a set of tools for developing and building server-side applications with Vite.

Vite, despite being a frontend tool, has support for transpiling server-side code. The feature is intended for building server-side rendering (SSR) applications. But there's no reason why it can't be leveraged for building server-side applications that are not necessarily related to SSR.

Vite's official SSR guide describes a workflow where Vite's development server is used as a middleware function in a server application made with a Connect compatible Node.js framework (like Express). If your server-side code needs transpilation (e.g. for TypeScript), this workflow requires you to use another set of tools (say ts-node and nodemon) for development and building. vavite enables you to use Vite itself to transpile your server-side code.

Examples

Installation and usage

Install vite and vavite as development dependencies (npm install --save-dev vite vavite) and add vavite to your Vite config:

import { defineConfig } from "vite";
import { vavite } from "@giacomorebonato/vavite";

export default defineConfig({
  plugins: [
    vavite({
      // Options, see below
    }),
  ],
});

Using handlerEntry

This option provides a simpler workflow if you don't need to control the server setup during development.

If you provide the handlerEntry option instead of a serverEntry, vavite will expect you to default export a request handler (with the (req, res, next) signature) from the handlerEntry file. During development, it will be used as a middleware function in the Vite development server, allowing you to handle requests. For production, vavite will build a standalone server application if standalone option is set to true. You can set it to false if you intend to import and use your handler function in a separate server application.

If you're building a standalone SSR application, you can set clientAssetsDir to the directory that will contain your client-side assets abd the sirv package will be used to serve them during production. If serveClientAssetsInDev to true (the default), they will be served during development too. You can set bundleSirv to false to import sirv in runtime instead of bundling it. In that case you will have to install it as a dependency. In either case, you can export sirvOptions from the handler entry file to customize the behavior of sirv.

If you set serverEntry in addition to handlerEntry, it will be used as the entry point in production but it will not be used in development. In that case, sirv will not be used and you will have to handle the serving of client assets in production yourself if needed.

vavite/http-dev-server is not available (or necessary) when using the handler mode.

Accessing Vite's dev server

You can import viteDevServer from "@giacomorebonato/vavite/vite-dev-server" to access the Vite development server instance. It will allow you to access methods such as ssrFixStacktrace and transformIndexHtml in your application.

Using serverEntry

Note that setting serverEntry without handlerEntry is a less reliable method and some things don't work on some operating systems.

By setting the serverEntry option without setting the handlerEntry, you can use vavite to develop and build Node.js application with any Node.js framework that allows you to provide your own http.Server instance. To integrate with Vite's dev server during development, you import httpDevServer from "@giacomorebonato/vavite/http-dev-server" and use it in place of a http.Server instance. How to do this depends on the framework:

Some frameworks expose their request listener: For example in Express, app is itself the request listener and in Koa you can access it with app.callback():

import express from "express";
import httpDevServer from "@giacomorebonato/vavite/http-dev-server";

const app = express();

// Configure your server here
app.get("/", (req, res) => {
  res.send("Hello, world!");
});

if (import.meta.env.PROD) {
  // For production, start your server
  // as you would normally do.
  app.listen(3000, "localhost", () => {
    console.log("Server started on http://localhost:3000");
  });
} else {
  // For development, use httpDevServer.
  httpDevServer!.on("request", app);
}

Other frameworks don't expose their request listener but instead allow you to provide a server instance. See the examples for Express, Koa, Fastify, and Hapi to see specific implementations.

In SSR application, you can enable the serving of client assets by setting serveClientAssetsInDev option to true. For production, you will have to serve the assets yourself, e.g. by using express.static.

Lazy loading route handlers

One of the most important advantages of Vite is its on-demand nature: Modules are only transpiled when they are actually used. By default, vavite reloads your server entry every time one of its dependencies changes. Since the server entry is the the root of the dependency tree, this means any change in your server-side code will trigger a full reload. Although it works, it doesn't tap into the full potential of Vite.

A typical Node.js server application lifecycle consists of two phases. The first is the initialization phase where you create and configure your server instance and the second is the request listening phase where the application services incoming requests as they come in. Typically, the initialization code changes less often and request listeners change more often and more granularly.

vavite can be used to lazy load request handlers to avoid re-executing the initialization code unnecessarily: If you set the configuration option reloadOn to "static-deps-change" (instead of the default "any-change"), vavite will not reload the server entry when its dynamically imported dependencies change. For example, if you have an Express route listener like this:

import routeHandler from "./route-handler";

app.get("/my-route", routeHandler);

You can avoid re-executing your initialization code by refactoring it like this:

app.get("/my-route", async (req, res, next) => {
  // Omitting error handling for clarity
  const routeHandler = (await import("./route-handler")).default;
  routeHandler(req, res, next);
});

This way, changes to your route handlers will not force a server reload and your route handler will only be transpiled and loaded when a request to the path "/my-route" comes in, greatly improving development-time performance.

If this lazy loading pattern feels too wordy, you can refactor it into a function suitable for your server framework. One possible implementation for Express could be:

function lazy(
  importer: () => Promise<{ default: RequestHandler }>,
): RequestHandler {
  return async (req, res, next) => {
    try {
      const routeHandler = (await importer()).default;
      routeHandler(req, res, next);
    } catch (err) {
      next(err);
    }
  };
}

// When reloadOn option is set to "static-deps-change",
// changes to the route handlers will not trigger a reload.
app.get"/foo", lazy(() => import("./routes/foo")));

app.get("/bar", lazy(() => import("./routes/bar")));

app.get("/baz", lazy(() => import("./routes/baz")));

Multiple builds

Developing applications that perform server-side rendering (SSR) with Vite requires two separate build steps: one for the client and one for the server. This package comes with a CLI program named vavite for orchestrating multiple Vite builds.

vavite extends the Vite configuration with a buildSteps property, which is an array of build step definitions. A build step definition is an object with a name property (which is simply a string naming the build step), and an optional config property which will be merged into the Vite configuration for the build step. For example, a client build followed by a server build can be defined like this:

import { defineConfig } from "vite";
import { vavite } from "@giacomorebonato/vavite";

export default defineConfig({
  plugins: [
    vavite({
      // Options
    }),
  ],
  buildSteps: [
    {
      name: "client",
      config: {
        build: {
          outDir: "dist/client",
          rollupOptions: {
            // Client entry
            input: "/client",
          },
        },
      },
    },
    {
      name: "server",
      config: {
        build: {
          // Server entry
          ssr: "/server",
          outDir: "dist/server",
        },
      },
    },
  ],
});

If you include the vavite plugin and a buildSteps option in your Vite config, the plugin will error out if you try to build with vite build. You're expected to use the vavite command as a drop-in replacement for vite build instead. This will first call resolveConfig with the mode parameter set to "multibuild" to extract the build steps. Setting buildSteps in subsequent steps has no effect.Then the build steps will be executed in the order they are defined.

Sharing information between builds

vavite will call the buildStepStart hook on each plugin when a build step starts and pass it the information about the current step and data forwarded from the previous step. The buildStepEnd hook will be called when the build step ends and its return value will be forwarded to the next step. If a promise is returned, it will be awaited first.

If no build steps are defined, buildStepStart and buildStepEnd will not be called.