Can Vite serve up-to-date compiled source at a URL?

47 Views Asked by At

I've got a vite project that's serving a tiny web app with web components.

For some technical reasons, I'd like to be able to hit a URL and get vite to serve me an up-to-date version of a source file. For example, if the file is in ./src/some-file.mjs I would like to be able to have a web component that hits

<!-- any URL is fine, this is just an example -->
<my-component src="localhost:3000/src/some-file.mjs"></my-component>

I'd also be ok with referencing the file directly:

<!-- any URL is fine, this is just an example -->
<my-component src="./src/some-file.mjs"></my-component>

The challenge there is that I'm using dynamic import inside my-component and treating the value of the src attribute as a url (which will be the normal case outside of development... maybe a CDN or similar).

I'd like HMR-- so when I update the file it triggers a reload and the web-component re-renders and the new source is downloaded and the ui is updated.

So far I've gotten it to serve the ./dist dir. It doesn't give me HMR, and it also caches pretty hard so even when I rebuild dist, I have to restart the server to serve the new content. Not ideal.

import { defineConfig } from 'vite';
import litPlugin from 'rollup-plugin-lit-css';

export default defineConfig({
  plugins: [litPlugin()],
  build: {
    lib: {
      entry: 'src/one-file.mjs',
      name: 'OneFile',
      formats: ['es', 'umd'], // Generate ES module and UMD builds
      fileName: (format) => `one-file.${format}.js`
    },
    rollupOptions: {
      // Additional Rollup options if needed
    }
  },
  server: {
    // Serve files from the ./dist directory
    fs: {
            // Disable strict serving behavior to allow serving from outside root
      strict: false,
            // Specify directories that should be accessible to the server
      allow: ['./dist']
    }
  }
});

My best guess about how to get this is to see if Vite can serve compiled source from a URL, then target that URL with the src attribute.

Any tips?

1

There are 1 best solutions below

0
On

Fundamentally, your problem boils down to a custom setup. Out-of-box, Vite cannot achieve this kind of workflow. But fortunately, Vite provides official guide for custom backend which you can utilize. In summary, your problem can be broken down into following concrete steps:

  • Use Vite to build the library. (Since, you are bundling library, do not use dev mode, instead use the vite build with watch mode. This shall ensure that compiled files are generated on the disk.) Also, write simple a plugin that will generate a version.txt file which you can poll.
  • Note that using Vite in build mode with additional watch means that your compilation will happen automatically on file change, but you will not be able to use Vite's custom server and it also implies no HMR.
  • Second step is to write a custom server to server your compiled file. You can make use of simple Node.js server to do this.
  • Third step is to write some sort of polling mechanism with cache busting technique. A simple timestamp is good enough to achieve this.

A very simple Vite configuration would be as following. Note the use of empty watch object passed to the build configuration:

import fs from 'node:fs/promises';
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    // Enable watch mode for build
    watch: {},

    lib: {
      entry: './src/lib.js',
      formats: ['es'],
    },
  },

  plugins: [
    {
      name: 'my-cache-plugin',
      async closeBundle() {
        await fs.writeFile('dist/version.txt', `${Date.now()}`);
      },
    },
  ],
});

Second, we will create a static file server with Node's http module. The important part is to ignore the timestamp that we will be sending from the browser along with a file request.

import http from 'node:http';
import send from 'send';

const server = http.createServer(function onRequest(req, res) {
  // Extract the file to serve by excluding query parameters
  // E.g. `/version.txt?timestamp=123456` =>  /version.txt
  const url = new URL(req.url, 'http://localhost:3000');
  const { pathname } = url;

  if (pathname === '/') {
    // Serve index.html
    send(req, '/index.html', { root: '.' }).pipe(res);

    return;
  }

  // Send the file
  send(req, pathname, { root: './dist' }).pipe(res);
});


server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

Finally, let's write some client-side code to do the polling and reload when the content changes:

// Somewhere in your Web component

let version = 0;

async function poll(callback) {
  // Check if Vite has compiled the new version of the library
  // The `version.txt` is updated with fresh timestamp on each compilation pass by our custom Vite plugin.
  const response = await fetch(`/version.txt?t=${Date.now()}`);
  const fetchedVersion = response.text();

  if (version !== fetchedVersion) {
    // Reload the page or do something different
    await callback();
    version = fetchedVersion;
  }

  setTimeout(poll, 250);
}

export class MyComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          border: 1px solid black;
        }
      </style>
      <slot></slot>
    `;

    poll(async () => {
      // The library has been updated. Do something!!!
      const NewModule = await import(this.getAttribute('src'));
    });
  }
}

customElements.define('my-component', MyComponent);

Now, this is the gist of the problem. You can enhance this with more customizations like:

  • Use custom HMR with a custom plugin to notify browser side code of the compilation changes.
  • Use two Vite instances. One for your library compilation and another for your demo application. (Vite's default library mode will probably not work for you as you need a workflow which works in Dev as well as production mode where your URL will probably be some external JS file from CDN. Using two instances will allow you to have some degree of HMR.
  • Programmatically invoke Vite with custom server and use HTTP push notification for informing about change. You can call both Vite and custom server from same script with a custom vite plugin which will tell custom server a file is modified and compilation has finished.
  • If the only reason you need HMR during development is to avoid page refresh for style changes, then you can write a custom plugin that sends new style data a string and you can swap that inside your component's shadow root or document's <head> tag. If you need global replacement, the trick is to use <style> tag with some fixed id with which you can delete and update the existing styling with the new styling supplied by the HMR updates. In other cases, you can trigger full page refresh and persist the state in sessionStorage or localStorage which you can then read again after full page reload.