🍣

Vue SFCのstyleタグから別のvueファイルのstyleを参照する

に公開

元々はコンポーネントをextendするとscoped styleが適用されない問題の解決を試みた所、

Comp.vue
<script>
export default {
  data: () => ({
    text: 'Hello'
  })
};
</script>
<template>
  <p class="text">
    {{ text }}
  </p>
</template>
<style scoped>
.text {
  background-color: yellow;
  padding: 10px;
  font-size: 1.3rem;
}
</style>
ExtendedComp.vue
<script>
import Comp from './Comp.vue';
export default {
  extends: Comp,
  data: () => ({
    text: 'Hello extended'
  })
};
</script>

Scoped Style Won't Apply
vite.config.jsのpluginsにloadを書けばビルド前のSFCを弄れる事を知り、大体似たような感じでextend元のコンポーネントのstyleタグを持って来れれば解決するなーと思って書いたら上手く行った。
Scoped Style Works
具体的にはvite.config.js に以下のようなpluginを自前で定義し、

vite.config.js
import { fileURLToPath, URL } from 'node:url'
import path from 'node:path'
import { readFileSync } from 'fs'
import { parseDocument } from 'htmlparser2'
import { findAll, getElementsByTagName, appendChild, removeElement } from 'domutils'
import { render } from 'dom-serializer';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
    {
      async load(id) {
        if (id.endsWith('.vue')) {
          const source = readFileSync(id).toString();
          // It doesn't matter what parses the SFC, but let's use an HTML parser used in vue here.
          const dom = parseDocument(source);
          const stls = findAll(elem => elem.tagName === 'style' && elem.attribs?.src?.endsWith('.vue'), dom.children);
          if (stls.length === 0) {
            return source;
          }
          for (const stl of stls) {
            const src = stl.attribs.src;
            // If the style tag that references the .vue is left,
            // a compile error will occur due to duplicate style tags.
            // so remove it here.
            removeElement(stl);
            const absPath = path.resolve(path.dirname(id), src);
            // `resolve.alias` can be resolved with `this.resolve`,
            // but relative paths are not resolved, so I do it like this.
            const resolved = (await this.resolve(absPath)) || (await this.resolve(src));
            const source = readFileSync(resolved.id).toString();
            const vueDom = parseDocument(source);
            const vueStls = getElementsByTagName('style', vueDom.children);
            for (const vueStl of vueStls) {
              appendChild(dom, vueStl);
            }
          }
          return render(dom);
        }
      }
    },
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

extendしたコンポーネントから参照したいstyleを持つvueファイルを指定する

ExtendedComp.vue
<script>
import Comp from './Comp.vue';
export default {
  extends: Comp,
  data: () => ({
    text: 'Hello extended'
  })
};
</script>
<style src="./Comp.vue"></style>

※別にextendしなくても参照出来る

Discussion