jpkleemans / vite-svg-loader

Vite plugin to load SVG files as Vue components
MIT License
555 stars 59 forks source link

Storybook build - Unknown variable dynamic import: ./{filename}.svg?component #47

Closed bissolli closed 2 years ago

bissolli commented 2 years ago

I have a library built using Vite (in library mode) + Vue3 + Storybook + TypeScript and I am facing problems building the project and/or building Storybook when Vite-SVG-Loader is used.

The component that uses a SVG:

<script setup lang="ts">
import { computed, defineAsyncComponent } from "vue";

const props = withDefaults(defineProps<{ name: string }>(), {
  name: 'add'
});

const iconComponent = computed(() => {
  return defineAsyncComponent(
    () => import(`./assets/${props.name}.svg?component`)
  );
});
</script>

<template>
  <component :is="iconComponent" />
</template>

Scenario 1 By keeping the ?component on the URL for the defineAsyncComponent function, vite build and start-storybook works perfectly fine but although build-storybook runs fine, when you try to access a component on storybook that uses the async loaded SVG you get the following error:

Unknown variable dynamic import: ./assets/add.svg?component Error: Unknown variable dynamic import: ./assets/add.svg?component at http://127.0.0.1:8080/assets/iframe.d31911ec.js:930:5989 at new Promise () at variableDynamicImportRuntime0 (http://127.0.0.1:8080/assets/iframe.d31911ec.js:930:5886) at http://127.0.0.1:8080/assets/iframe.d31911ec.js:930:6317 at pe (http://127.0.0.1:8080/assets/iframe.d31911ec.js:119:21340) at setup (http://127.0.0.1:8080/assets/iframe.d31911ec.js:119:22194) at callWithErrorHandling (http://127.0.0.1:8080/assets/iframe.d31911ec.js:119:848) at setupStatefulComponent (http://127.0.0.1:8080/assets/iframe.d31911ec.js:119:68463) at setupComponent (http://127.0.0.1:8080/assets/iframe.d31911ec.js:119:68106) at Ht (http://127.0.0.1:8080/assets/iframe.d31911ec.js:119:47236)

Scenario 2 On the other hand, by removing the ?component, everything Storybook related works but the vite build throws the following error:

Invalid value "umd" for option "output.format" - UMD and IIFE output formats are not supported for code-splitting builds. error during build: Error: Invalid value "umd" for option "output.format" - UMD and IIFE output formats are not supported for code-splitting builds. at error (./node_modules/rollup/dist/shared/rollup.js:198:30) at validateOptionsForMultiChunkOutput (./node_modules/rollup/dist/shared/rollup.js:16207:16) at Bundle.generate (./node_modules/rollup/dist/shared/rollup.js:16041:17) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async ./node_modules/rollup/dist/shared/rollup.js:23679:27 at async catchUnfinishedHookActions (./node_modules/rollup/dist/shared/rollup.js:23121:20) at async doBuild (./node_modules/vite/dist/node/chunks/dep-27bc1ab8.js:39180:26) at async build (./node_modules/vite/dist/node/chunks/dep-27bc1ab8.js:39011:16) at async CAC. (./node_modules/vite/dist/node/cli.js:738:9)

My project configuration

Packages used:

Vite setup

import { fileURLToPath, URL } from "url";
import { defineConfig } from "vite";
import path from "path";
import vue from "@vitejs/plugin-vue";
import ViteSvgLoader from "vite-svg-loader";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), ViteSvgLoader()],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  build: {
    lib: {
      entry: path.resolve(__dirname, "src/main.ts"),
      name: "DesignSystem",
      fileName: (format) => `index.${format}.js`,
    },
    rollupOptions: {
      external: ["vue"],
      output: {
        globals: {
          vue: "Vue",
        },
      },
    },
  },
});

Storybook main.js

const ViteSvgLoader = require('vite-svg-loader')

module.exports = {
  ...
  "framework": "@storybook/vue3",
  "core": {
    "builder": "storybook-builder-vite"
  },
  async viteFinal(config, { configType }) {
    config.plugins.push(ViteSvgLoader());
    return config;
  },
}

Any ideas?

bissolli commented 2 years ago

Found the issue!!

The import statement doesn't support backticks because linking, where modules are loaded, is a pre-runtime process. From what I'm guessing, since template literals signal an intent for interpolation which would be executed at runtime, only vanilla string literals are allowed.

The solution is just to go with the conventional way to concatenate strings.

async () => import("./assets/" + props.name + ".svg?component")
// instead of
() => import(`./assets/${props.name}.svg?component`)
bissolli commented 2 years ago

I will reopen this issue because it's actually still there! Maybe this is due to the build.lib options enabled in Vite! It seems that the SVG files are not exposed along with the built files.

bissolli commented 2 years ago

That is it! After running some test this is the problem.

When using the lib mode along with defineAsyncComponent, the SVG is not exported.

Just by removing the defineAsyncComponent, it will work as intended.

Need to find a way to make that happen.

dpschen commented 2 years ago

@bissolli I had a similar problem. Based on your work I was able to get it done:

I had to use vite-plugin-glob here. It might work without the plugin if you don't need to ?component suffix (vites glob import doesn't support that).

<!-- Icon.vue -->
<template>
  <component :is="iconComponent" />
</template>

<script setup lang="ts">
import { computed, defineAsyncComponent, type Component } from "vue";

function importIcons() {
  function lowerCaseFirstLetter(str: string) {
    return str.charAt(0).toLowerCase() + str.slice(1);
  }

  function getFileNameFromPath(path: string) {
    const file = path.split("/").pop() ?? "";

    if (!file) return file;

    return file.split(".").shift() ?? "";
  }

  const iconImports = import.meta.importGlob<Component>(
    "@/assets/icons/*.svg",
    {
      query: { component: true },
      export: "default",
    }
  );

  const icons: Record<string, () => Promise<Component>> = {};
  for (const path in iconImports) {
    const iconName = lowerCaseFirstLetter(getFileNameFromPath(path));
    icons[iconName] = iconImports[path];
  }
  return icons;
}

const props = defineProps({
  name: {
    type: String,
    default: "info",
  },
});

const icons = importIcons();

const iconComponent = computed(() =>
  icons[props.name] ? defineAsyncComponent(icons[props.name]) : null
);
</script>
// .storybook/main.ts
const { loadConfigFromFile, mergeConfig } = require("vite");
const path = require("path");

const GlobPlugin = require("vite-plugin-glob");
const svgLoader = require("vite-svg-loader");

module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
  framework: "@storybook/vue3",
  core: {
    builder: "@storybook/builder-vite",
  },

  async viteFinal(config, { configType }) {
    // https://github.com/cafebazaar/emroozjs/blob/master/.storybook/main.js
    const { config: userConfig } = await loadConfigFromFile(
      path.resolve(__dirname, "../vite.config.ts")
    );

    return mergeConfig(config, {
      ...userConfig,

      // see: https://github.com/storybookjs/builder-vite/issues/298#issuecomment-1087405464
      resolve: {
        ...config.resolve,
        ...userConfig.resolve,
        alias: [
          {
            find: "@storybook/core/client",
            replacement: "@storybook/core-client",
          },
          {
            find: "@",
            replacement: path.resolve(__dirname, "../src"),
          },
          /* https://v3.vuejs.org/guide/installation.html#for-server-side-rendering */
          {
            find: "vue",
            replacement: path.resolve(
              __dirname,
              "../node_modules/vue/dist/vue.esm-bundler.js"
            ),
          },
        ],
      },

      // manually specify plugins to avoid conflict
      plugins: [
        // ...config.plugins
        svgLoader({
          defaultImport: "url",
        }),

        // https://github.com/antfu/vite-plugin-glob
        GlobPlugin({
          // enable to let this plugin interpret `import.meta.glob`
          // takeover: true,
        }),
      ],
    });
  },
};
// vite.config.ts

import { fileURLToPath, URL } from "url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import GlobPlugin from "vite-plugin-glob";
import svgLoader from "vite-svg-loader";

const pathSrc = fileURLToPath(new URL("./src", import.meta.url));

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    // sourcemap: true,
    lib: {
      entry: `${pathSrc}/lib.ts`,
      name: "my-ui",
      formats: ["es", "umd"],
      fileName: (format) => `my-ui.${format}.js`,
    },
    rollupOptions: {
      output: {
        inlineDynamicImports: true,
      },
    },
    // rollupOptions: {
    //   // make sure to externalize deps that shouldn't be bundled
    //   // into your library
    //   external: ["vue"],
    //   output: {
    //     // Provide global variables to use in the UMD build
    //     // for externalized deps
    //     globals: {
    //       vue: "Vue",
    //     },
    //   },
    // },
  },
  plugins: [
    vue(),
    svgLoader({
      defaultImport: "url",
    }),
    // https://github.com/antfu/vite-plugin-glob
    GlobPlugin({
      // enable to let this plugin interpret `import.meta.glob`
      // takeover: true,
    }),
  ],
  resolve: {
    alias: {
      "@": pathSrc,
    },
  },

  css: {
    devSourcemap: true,
    preprocessorOptions: {
      scss: {
        additionalData: `@use "${pathSrc}/styles/commonImports" as *;`,
      },
    },
  },
});
jpkleemans commented 2 years ago

@dpschen thanks for your solution!

dpschen commented 2 years ago

Maybe worth mentioning that I have problems updating to the latest version of the vite-plugin-glob. The approach above should work with v.0.2.9.

Not sure why 🤷

Edit: To be clear: fixing the mentioned breaking change of vite-plugin-glob (rename of the export option property to import) didn’t fix make the new version work for me.

felixvictor commented 2 years ago

Maybe worth mentioning that I have problems updating to the latest version of the vite-plugin-glob. The approach above should work with v.0.2.9.

Not sure why shrug

Edit: To be clear: fixing the mentioned breaking change of vite-plugin-glob (rename of the export option property to import) didn’t fix make the new version work for me.

You also have to add the restoreQueryExtension option to the plugin:

        GlobPlugin({ restoreQueryExtension: true }),
sebastiaanluca commented 1 year ago

Since vite-plugin-glob was merged back into Vite, anybody got a working solution for this? Can't seem to figure out how to dynamically load SVG icons.

It works when running in dev mode, but vite build won't load any icons with the following error in the browser console:

Script from “images/icons/megaphone.svg” was blocked because of a disallowed MIME type (“image/svg+xml”).

It's processing them when building, but I guess due to the hashes being added, it can't find the generated modules.