npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ulu/vite-plugin-virtual-modules

v1.1.0

Published

A Vite plugin to create virtual modules from loader files at build time, with support for HMR, file watching, and query parameters.

Readme

@ulu/vite-plugin-virtual-modules

This plugin allows you to easily create "virtual modules" (modules whose contents are generated at build time) using standard JavaScript files as "loaders".

At its core, you write a Node.js module that runs during the Vite build process. This module's job is to generate the code for a new, virtual module that your application can then import.

This is especially useful for things like build-time data fetching for SSG apps, injecting build-time constants, or generating modules from custom file types. It avoids bringing server-side code and dependencies into your browser bundle.

Features:

  • Use standard ES modules to create virtual modules.
  • HMR updates for your virtual module when you modify the loader.
  • Watch other files for changes to trigger a reload of the virtual module.
  • Pass query parameters to your loader module.

If you encounter bugs or have a feature request, feel free to open an issue on github.

Table of Contents:

A Simple Example

  1. Create a "loader" module. This code runs in Node.js.

    // build-time.js
    export default function() {
      return {
        load() {
          // This string will become the content of the virtual module
          const moduleContent = `export default "Built at: ${new Date().toLocaleString()}";`;
          return moduleContent;
        }
      }
    }
  2. Import the virtual module in your app.

    // main.js
    import buildTimestamp from "./build-time.js?virtual-module";
    
    // Logs: "Built at: 10/24/2025, 10:30:00 AM" (for example)
    console.log(buildTimestamp);

The build-time.js file is executed by Node.js, and the string it returns becomes the buildTimestamp module your app imports. The loader itself is never sent to the browser.

Vite Setup

// vite.config.js
import { defineConfig } from "vite";
import virtualModules from "@ulu/vite-plugin-virtual-modules";

export default defineConfig({
  plugins: [
    virtualModules({
      // See options below
    })
  ]
});

Usage

Advanced Example: Data Fetching

A common use case is to fetch data from a CMS at build time for a static site. This prevents your data-fetching logic and any associated dependencies from being included in the final browser bundle.

Below is an example of a loader module that fetches users from an API and provides the result as a JSON module.

// fetch-users.js (this is the loader module)

import { toJsonModule } from "@ulu/vite-plugin-virtual-modules";
import { getContent, contentUpdated } from "./some-service.js";

// This function receives a context object (see API section)
export default function({ reload, isServe }) {
  return {
    async load() {
      try {
        const result = await getContent("users");
        const users = await result.json();

        // During development, we can set up HMR.
        // Here, we imagine `contentUpdated` is a function from our service
        // that calls a callback when the CMS content changes.
        if (isServe) {
          contentUpdated(() => reload());
        }
        
        // Use the helper to safely create a JSON module
        return toJsonModule(users);
      } catch (error) {
        console.error(error);
        throw error;
      }
    }
  }
}

Now, to use this data, you import the file with the special ?virtual-module suffix.

// user-view.js (in your application)

import users from "./fetch-users.js?virtual-module";

// `users` is now the JSON array: [ { user }, { user }, ... } ]
console.log(users); 

// ... Use the data in your components

Using Queries

You can pass URL queries when importing a virtual module to change its output.

// dog-view.js
import dogs from "./fetch-animals.js?virtual-module&type=dog";

console.log(dogs); // JSON: [ { dog }, { dog } ]

The loader module can access these queries via the context object.

// fetch-animals.js
import { toJsonModule } from "@ulu/vite-plugin-virtual-modules";

export default function({ queries }) {
  return {
    async load() {
      if (queries.type) {
        const animals = await animalsByType(queries.type);
        return toJsonModule(animals);
      }
    }
  }
}

API

Importing a Virtual Module

To trigger the plugin, add the ?virtual-module suffix to your import path.

// The suffix tells Vite to process this import with this plugin
import myModule from "./path/to/loader.js?virtual-module";

// You can also add queries
import myQueriedModule from "./path/to/loader.js?virtual-module&foo=bar";

The toJsonModule Helper

This plugin exports a helper function, toJsonModule, to make it easy to create a module that default exports JSON data.

import { toJsonModule } from "@ulu/vite-plugin-virtual-modules";

const myData = { key: "value", other: [1, 2] };
const moduleCode = toJsonModule(myData);
// moduleCode is now:
// 'export default JSON.parse("{\"key\":\"value\",\"other\":[1,2]}")'

You might wonder why it uses JSON.stringify twice. This is a necessary trick to safely embed a JSON string inside a JavaScript string. It ensures that all quotes and special characters are correctly escaped, so JSON.parse() will work reliably in the final module.

Loader Module Structure

The "loader module" is the file you create that generates the virtual module. It must have a default export that is a function. This function returns an object that configures the virtual module's content and behavior.

export default function(context) {
  // `context` is an object with helpful properties (see below)
  
  return {
    // REQUIRED
    // A function that returns the code for the virtual module.
    // Can be async.
    // Arguments:
    // - watchedFiles: Array of relative paths to files that changed (if `watch` is used).
    // - data: Data passed to `reload(data)` or the event object from a file watcher trigger.
    //   - File watcher data: `{ event: "change", file: "path/to/file", timestamp: 1234567890 }`
    // - context: The same context object passed to the factory function (for convenience).
    load({ watchedFiles, data, context }) {
      return "export default 'hello world'";
    },

    // OPTIONAL
    // Watch files for changes and trigger HMR.
    // Paths are relative to this loader module file.
    watch: ["some/files/**/*.txt"],
    
    // OPTIONAL
    // Options passed directly to the `chokidar` watcher.
    watchOptions: {},
    
    // OPTIONAL
    // Events that should trigger a reload.
    watchEvents: ["add", "unlink", "change"]
  }
}

Context Object

Your loader module's default function will be called with a context object containing:

  • id: The full import ID string (e.g., /path/to/loader.js?virtual-module&foo=bar).
  • filePath: The absolute path to the loader module file.
  • importPath: The path used by Node to import the module (can change for HMR).
  • queries: An object containing the URL queries from the import ID.
  • isServe: A boolean indicating if the Vite dev server is running (true for vite serve).
  • command: The current Vite command ('serve' or 'build').
  • reload: A function to programmatically trigger a reload (HMR) of this module.

Plugin Options

Options that can be passed when adding this plugin in vite.config.js.

// vite.config.js
import virtualModules from "@ulu/vite-plugin-virtual-modules";

export default {
  plugins: [
    virtualModules({
      // Regex to identify virtual module imports.
      suffix: /\?virtual-module(&.*)*$/,
      
      // Default events that trigger a reload when watching files.
      // Can be overridden in the loader module.
      watchEvents: ["add", "unlink", "change", "unlinkDir", "addDir"],
      
      // Default options passed to Chokidar for file watching.
      // Can be overridden in the loader module.
      watchOptions: {}
    })
  ]
};

Change Log

Change Log