Custom Vite plugin for transforming SVGs only works in development mode, not in build mode

65 views Asked by At

I'm building an app with SvelteKit, which uses Vite for bundling.

I made a Vite plugin for importing SVG files (using the ?resource query after the file name) which parses the SVG file and turns it into something like this:

export default {
  width: 100,
  height: 100,
  dataUrl: "data:image/svg+xml;charset=utf-8,[...]"
}

The idea is to inline these SVGs into my code and provide some metadata that helps me display them properly. I have a valid reason for wanting to embed width and height in the bundle instead of accessing it from SVG element rendered by the browser.

This plugin works perfectly fine in dev mode (i.e. when running vite), however when I build (vite build) and look at the generated files, the SVGs are inlined as data urls instead of as modules within the bundle. My plugin's output appears to be ignored completely.

As an example, here's a snippet from the bundled output:

var yc =
    "data:image/svg+xml,%3csvg%20width='100'%20height='100'[...]"

The SVG has been inlined as raw SVG code urlencoded within a data url rather than my transformed version.

I have tried the following things to no avail:

  • Rewrite the transform method of the plugin as a load method instead
  • Use a debugger to step through the vite execution and observe the transformation process
    • As far as I can tell my plugin transforms the files correctly and the result does not get overwritten by any other plugin, however the transformed code still doesn't make it into the build.

Config files

vite.config.ts

import { sveltekit } from "@sveltejs/kit/vite";
import { svgResourcePlugin } from "./plugins/svgResourcePlugin";
import { defineConfig } from "vitest/config";

const config = defineConfig({
  plugins: [svgResourcePlugin(), sveltekit()],
  worker: {
    format: "es",
  },
  test: {
    include: ["src/**/*.{test,spec}.{js,ts}"],
  },
});

export default config;

svelte.config.js

SvelteKit is configured with the adapter @sveltejs/adapter-static which generates a static site when building.

import adapter from "@sveltejs/adapter-static";

import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),

  kit: {
    adapter: adapter({
      pages: "build",
      assets: "build",
      fallback: undefined,
      precompress: false,
      strict: true,
    }),
  },
};

export default config;

Plugin code

import { transformWithEsbuild, type Plugin } from "vite";
import { readFileSync } from "fs";
import { XMLParser } from "fast-xml-parser";

export function svgResourcePlugin(options = {}): Plugin {
  const {
    defaultExport = "url",
    svgoConfig,
    expandProps,
    svgo,
    ref,
    memo,
    replaceAttrValues,
    svgProps,
    titleProp,
  } = options;

  const cache = new Map();
  const svgRegex = /\.svg(?:\?(resource|url))?$/;

  return {
    name: "svg-resource",
    async transform(source, id, isBuild) {
      const result = id.match(svgRegex);

      if (result) {
        const type = result[1];

        if ((defaultExport === "url" && typeof type === "undefined") || type === "url") {
          return source;
        }

        if ((defaultExport === "resource" && typeof type === "undefined") || type === "resource") {
          const idWithoutQuery = id.replace(".svg?resource", ".svg");
          let result = cache.get(idWithoutQuery);

          if (!result) {
            const code = readFileSync(idWithoutQuery, "utf-8");

            const parser = new XMLParser({
              ignoreAttributes: false,
              attributeNamePrefix: "",
            });
            const jsonObj = parser.parse(code);

            if (jsonObj && jsonObj.svg) {
              const width = parseInt(jsonObj.svg.width, 10);
              const height = parseInt(jsonObj.svg.height, 10);
              const dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
                Buffer.from(code).toString(),
              )}`;

              result = `export default ${JSON.stringify({
                width,
                height,
                dataURL,
              })};`;
            } else {
              this.error(`Invalid SVG resource file: ${id}`);
            }

            if (isBuild) {
              cache.set(idWithoutQuery, result);
            }
          }

          return result;
        }
      }
    },
  };
}
1

There are 1 answers

0
kettlepot On

While messing around more with the Vite configuration, I noticed that there's a plugins field under worker.

It appears that the plugin must be placed within this field too if I want the worker code to be able to use the plugin. This appears to have fixed my issue.

Here's the updated vite.config.ts:

import { sveltekit } from "@sveltejs/kit/vite";
import { svgResourcePlugin } from "./plugins/svgResourcePlugin";
import { defineConfig } from "vitest/config";

const config = defineConfig({
  plugins: [svgResourcePlugin(), sveltekit()],
  worker: {
    format: "es",
    plugins: [svgResourcePlugin()],
  },
  test: {
    include: ["src/**/*.{test,spec}.{js,ts}"],
  },
});

export default config;