Code.Movie | Declarative animated syntax highlighter

6 min read Original article ↗

Blog / Website Update: Migrated from Parcel to Vite

Code.Movie's project website just got a new frontend module bundler by replacing Parcel with Vite! This has no direct impact on users of any part of the Code.Movie project, but if you like reading about frontend tooling, the following is a short summary of how the migration went.

What's Parcel and what's wrong with it?

Parcel is a zero configuration build tool for web projects that has served my various side project very well for years on end. You can literally just throw vaguely HTML-shaped files at it, and it just deals with them — dependencies get resolved, required transpilers get auto-installed, optimizations are set up it has always been reasonably fast. But apparently not fast enough.

It appears that a bad case of “rewrite everything in Rust” has taken over Parcel, which left it unable to handle some pretty basic aspects of HTML and CSS. The project's complexity and oxidation in turn left me (who can barely string together a working JavaScript library) unable to contribute much beyond bug reports and, consequentially, let me looking for greener pastures elsewhere.

Why Vite?

Vite probably requires no introduction. Like Parcel, you can just point it in the general direction of some HTML files, and it will resolve and optimize all relevant dependencies. It is really quite similar to Parcel in its overall approach and therefore a natural alternative.

How does this website work?

This entire website is a single-page app (the playground) bolted to some scripts that, in aggregate, form an ad-hoc static site generator. The build process therefore consists of two stages: one turns content (HTML and markdown from various sources) into HTML that, together with CSS and JS gets fed into a process that handles bundles and optimizes everything.

This last step is where Parcel got replaced with Vite.

How did the move go?

Because Vite and Parcel are really quite similar in their general approach (eat HTML files, resolve and optimize dependencies like CSS and JS) the migration mainly consisted of swapping out one tool for another, adjusting the project's directory structure and finally replacing a few non-standard Parcel-isms with the Vite-specific counterparts:

  • Importing plain text in Parcel required the prefix bundle-text: on the module identifier, whereas Vite wants the suffix ?inline
  • A ~ prefix refers to the project root in Parcel, while Vite uses <root>

The lion's share of the migration process boiled down to fiddling with paths, which was mostly handled by just modifying the ad-hoc SSG scripts. Vite's surprising lack of built-in HTML minification was dealt with via a small plugin that integrates @minify-html/node and performs some minor HTML wrangling with posthtml:

function htmlPlugin() {
  return {
    name: "vite:plugin:codemovie:html",
    enforce: "pre",
    transformIndexHtml: {
      order: "pre",
      handler: async (input) => {
        const { html } = await posthtml([include()]).process(input, {
          sync: false,
          skipParse: false,
        });
        return minifyHtml
          .minify(Buffer.from(html), {
            minify_css: true,
            minify_js: true,
          })
          .toString();
      },
    },
  };
}

123456789101112131415161718192021

function htmlPlugin() {
  return {
    name: "vite:plugin:codemovie:html",
    enforce: "pre",
    transformIndexHtml: {
      order: "pre",
      handler: async (input) => {
        const { html } = await posthtml([include()]).process(input, {
          sync: false,
          skipParse: false,
        });
        return minifyHtml
          .minify(Buffer.from(html), {
            minify_css: true,
            minify_js: true,
          })
          .toString();
      },
    },
  };
}

Shoving every bit of HTML through two completely different tools in sequence is slightly gross, but minify-html is so fast that it's really not noticeable in the slightest.

What was the biggest snag?

The list of HTML asset sources that Vite recognizes is hard-coded, which sucks for web components or non-standard attributes:

<!-- Web component referring to a static HTML file -->
<html-import src="./somewhere/else.html"></html-import>

<!-- data-* attributes referring to images -->
<img
  src="./image-default.png"
  data-src-dark="./image-dark.png"
  data-src-light="./image-light.png"
  alt=""
/>

12345678910

<!-- Web component referring to a static HTML file -->
<html-import src="./somewhere/else.html"></html-import>

<!-- data-* attributes referring to images -->
<img
  src="./image-default.png"
  data-src-dark="./image-dark.png"
  data-src-light="./image-light.png"
  alt=""
/>

In its current state, Vite can't know that src on <html-import> or data-src-dark on <img> both point to resources that should take part in the bundling process. Parcel also has its default elements and attributes that it considers to be asset sources, but this can easily be augmented thanks to Parcel's extremely powerful plugin system. Vite's plugins are much more constrained and (as far as I can tell) can't just do their own asset discovery. For this project, rewriting the very small amount of client-side code to deal with this limitation proved to be the path of least resistance.

How did this improve this website?

Not at all. Almost everything looks and feels just like before and I don't notice any difference in build performance. Vite, like Parcel before, turns HTML, CSS, and JS into optimized HTML, CSS, and JS — without any drama. There are just fewer bugs in Vite compared to Parcel and the total project's dependency count decreased slightly. With fewer distractions this could have been done within two or three working days, starting from absolutely zero experience with Vite and without any advance research.

The key to this smooth transition was not preparation or advance research, but KISS. By not using too many of Parcel's more advanced features (like macros and node emulation), keeping away from third-party dependencies as much as possible, and sticking as close to the basics as possible, switching to any other halfway competent bundler tools was always going to be doable. The simpler the build tool's setup is, the simpler an eventual migration is going to be.

But the ad-hoc static site generator was what turned the migration into an actual cakewalk. This collection of semi-hacky Node.js scripts was effortlessly modified to deal with a new directory structure while keeping important features (generating animations build-time and pulling docs content directly from the @codemovie/code-movie package) completely unchanged. Rolling your own SSG in 2026 may look like the worst possible case of Not Invented Here but in the long term, nothing is easier to adjust to changes than a collection of narrowly-scoped scripts that have zero pretensions of being “proper software”. Survivability and adaptability are under-appreciated factors in software development and nothing is more survivable and adaptable than a few bare-bones scripts at the right place.