「一个插件,两种配置」:Astro + Svelte 双环境下的 unplugin-icons 配置

瞎折腾 / Astro / vite / Svelte 无分类

在 Astro 项目中集成 Svelte 组件时,框架生态间的细微冲突常常令人头疼。最近在写 Astro 的时候发现了一个问题:同一个 unplugin-icons 插件,在 .astro.svelte 文件中需要不同的编译器配置。然而翻了半天没发现什么单独给 Svelte 配置 vite 插件的解决方案,到底怎么办!

问题源于一个常见需求:在一个以 Astro 为主框架的博客系统中,部分交互组件采用 Svelte 实现。当尝试在 Svelte 组件中使用 unplugin-icons 时,控制台却抛出莫名的编译错误——在 Astro 文件中运行良好的图标组件,到了 Svelte 环境中却无法正常解析。

11:45:28 [ERROR] [InvalidComponentArgs] Invalid arguments passed to <arrow-up> component.
  Hint:
    Astro components cannot be rendered directly via function call, such as `Component()` or `{items.map(Component)}`.
  Error reference:
    https://docs.astro.build/en/reference/errors/invalid-component-args/
  Stack trace:
    at [[REDACTED]]\node_modules\.pnpm\astro@5.7.10_jiti@2.4.2_lightningcss@1.29.2_rollup@4.40.1_typescript@5.8.3\node_modules\astro\dist\runtime\server\astro-component.js:11:13
    [...] See full stack trace in the browser, or rerun with --verbose.

究其根源,unplugin-icons 需要根据目标框架指定编译器类型。若在 Astro 项目中配置 compiler: "astro",Svelte 组件中的图标导入会因编译器不匹配而失效;反之,若设为 svelte,则会破坏 Astro 文件的渲染。

所以去阅读了 unplugin-icons 的源代码。虽然对 Vite 插件不是很熟悉,但是我们可以在 index.ts 里发现:

async load(id) {
  const config = await resolved
  const code = await generateComponentFromPath(id, config) || null
  if (code) {
    return {
      code,
      map: { version: 3, mappings: '', sources: [] } as any,
    }
  }
},

这个应该就是 unplugin-icons 的核心逻辑了。generateComponentFromPath 看起来是一个进行编译的东西,瞎猜应该都能知道 config 里存了 compiler 的信息!这个地方思路就明了了,我们可以创建一个 Vite 插件,重写 load 函数,把不同后缀的文件路由到不同配置的插件(分别调用各自的 load 函数)。

我们首先创建两个独立的插件实例,分别针对 Astro 和 Svelte 进行初始化:

// astro.config.mjs
import icons from "unplugin-icons/vite";

const icoAstro = icons({ compiler: "astro" });
const icoSv = icons({ compiler: "svelte" });

接着,在 Vite 插件中实现一个路由,通过拦截模块加载请求,判断文件类型并动态选择对应的编译器实例:

// @ts-check
import { defineConfig } from "astro/config";

export default defineConfig({
	vite: {
		plugins: [{
			name: "unplugin-icon-autofit",
			...icoSv,
			async load(id) {
				return await (id.endsWith(".svelte")
					? // @ts-ignore 是的这里类型没有任何问题
					  icoSv.load(id)
					: // @ts-ignore
					  icoAstro.load(id));
			},
		}],
	},
});

需要注意的是,这个插件函数返回的是一个 vite.Plugin<any> | vite.Plugin<any>[] 一样的东西,但实际上这个东西只是在类型文件里这么写的,真实情况是返回了一个 vite.Plugin<any>。不过无论如何我们让 TypeScript 忽略这个东西就好了。不过需要注意的是,这个东西算是一个侵入式的 hack,如果 unplugin-icons 有更新的话,应该再三确认会不会把这里改爆。

Upd1: 这里好像设置成 { ...icoSv } 才不会爆。可能是有什么神奇的兼容机制吧。

Copyright (C) Imken Luo
This post is licensed under CC-BY-NC-SA 4.0.