🚀

Astroにおけるscssのglob機能みたいなものを作ってみた

2023/04/19に公開

一時期twitterのTLで埋め尽くされた、静的ビルドツールであるAstro。
コンポーネントで完結し、同じクラス名を指定しても勝手にスコープするので上書きされる心配もありません。
そもそもscssを使用する想定ではないためか、Viteのようにglobできるプラグインがありません。

ただ個人的にはscssは使用したいのです。

https://www.npmjs.com/package/astro-use

こちら試行するにあたり、ふりふぁんさんの環境をお借りしました。ありがとうございます。
https://freefuntimes.com/?p=1063
https://twitter.com/freefuntimes?s=20

globできない問題

例えばViteではstyle.scssでprojectフォルダ内のscssファイルを読み込みたい場合は以下のようにuseして終わるだけなのですが

style.scss
@use "project";

Astroではこれだとエラーが出てしまうので、projectフォルダ内に_index.scssを作成し以下のようにファイルを指定する必要があります。

project/_index.scss
@use "hoge";
@use "fuga";
@use "hige";
//以下ファイルが増えるたび手動で追加

ファイル数が少なければそこまで手間ではないものの、増えた時は記述を追加する必要があり、また削除した際は@useも削除しないとエラーが出るので出来れば自動化したところです。

そこで「ないなら作ってしまえ」ということで、作りました。

階層

今回階層は以下のようにしています
詳細はふりふぁんさんの記事をご参照ください

/public
/src
├─┬─ /components
│ ├─ /layouts
│ ├─ /pages
│ └─ /scss ─┬─ /project ─┬─ _index.scss
│           ...          ├─ _hoge.scss
│                        ├─ _fuga.scss
│                        ├─ _hige.scss
│                        ...
│
├─ astro.config.mjs
├─ package.json
...

コード

astro.config.mjs
const { log } = console;
import { defineConfig } from "astro/config";
import path from "path";
import fs from "fs";
import chokidar from "chokidar";


//_index.scss書き込み用変数
const preDir = "./"; //ディレクトリ接頭辞
const DIR_SCSS = "src/scss"; //ディレクトリ接頭辞以降のパス
const SCSS_PATH = preDir + DIR_SCSS; //scssフォルダまでのパス
const splitScss = (/(_|.scss)/); //split用正規表現 ".scss"
let USE_STRINGS = ""; //書き込み用ファイル

/**
 * fsによる_index.scss書き込み関数
 * @param {*} dirPath path.joinの第一引数
 */
const writeUSE_STRINGS = (dirPath) => {
  fs.writeFileSync(
    path.join(dirPath, '_index.scss'), //対象ファイル
    USE_STRINGS //書き込む文字列
  );
}

/**
 * 対象フォルダ内のscssを@useした_index.scssを作成する関数
 * @param {*} dir 
 */
const writeIndexSCSS = (dir) => {
  //USE_STRINGSリセット
  USE_STRINGS = "";

  //指定フォルダへのパス(./からの相対パス)
  const DIR_PATH = `${SCSS_PATH}${dir}`;

  //ファイル一覧取得
  const files = fs.readdirSync(DIR_PATH);

  //_index.scss以外のscssファイルの@useをUSE_STRINGSに追加する
  files.forEach(file => {
    const fileName = file.split(splitScss)[2];

    if (fileName !== "index") {
      USE_STRINGS += `@use "${fileName}";` + "\n";
    }
  });

  //_index.scssにUSE_STRINGSの内容を書き込む
  writeUSE_STRINGS(DIR_PATH);
}

/**
 * chokidarを使ったファイル追加・削除の検知
 * @param {*} dir 
 */
const watcherFunc = (dir) => {
  //指定フォルダへのパス(./からの相対パス)
  const DIR_PATH = `${SCSS_PATH}${dir}`;

  //split用
  const splitText = DIR_PATH.split("./")[1] + "/_";

  //chokidarの初期化
  const watcher = chokidar.watch(DIR_PATH, {
    ignored: /[\/\\]\./,
    persistent: true
  });

  watcher.on("ready", () => {
    //準備完了
    log("ready watching by chokidar...");

    //ファイルの追加
    watcher
      .on('add', scss => {
        //追加する@useの記述
	const fileName = scss.split(splitText)[1].split(".scss")[0];
        const usePath = `@use "${fileName}";` + "\n";

        //USE_STRINGSの更新
        USE_STRINGS += usePath;

        //_index.scssにUSE_STRINGSの内容を書き込む
        writeUSE_STRINGS(DIR_PATH);
	log("added scss file and added @use to _index.scss!")
      });

    //ファイル削除
    watcher
      .on('unlink', scss => { //削除検知後、_index.scssを再生成する
        //削除する@useの記述
	const fileName = scss.split(splitText)[1].split(".scss")[0];
        const usePath = `@use "${fileName}";` + "\n";

        //USE_STRINGの更新
        USE_STRINGS = USE_STRINGS.replace(usePath, "");

        //_index.scssにUSE_STRINGSを書き込む
        writeUSE_STRINGS(DIR_PATH);
        log("deleted scss file and deleted @use from _index.scss!")
      })
    // watcher.on("all", (event, path) => log(event, path))
  });
}

/**
 * _index.scssの@use自動作成とファイルの追加・削除の検知
 * 
 * @param {*} dir scssフォルダ以降のパス(/から記述すること)
 */
const indexScssUse = (dir) => {
  writeIndexSCSS(dir);
  watcherFunc(dir);
}

//@use自動生成の対象フォルダを追加する(src/scss以降のパス)
indexScssUse("/project");

export default defineConfig({
  ...
});

何やってるか

indexScssUse("/hoge")により、npm時にsrc/scss/hoge内で関数を実行(以下はこの関数内で勝手に実行される)
1. writeIndexSCSSsrc/scss/hoge内のscssファイルを取得し、@useの形に成形し_index.scssを作成/上書きする
2. scssファイル追加時はwatcherFuncaddで対象記述を追加した_index.scssを生成する
3. scssファイル削除時はwatcherFuncunlinkで対象記述を削除後、_inde.scssを再生成する

fs.watchだと延々ループに陥りうまくいかなかったのでchokidarを使用しました
あくまでこの環境で使えるものであり、scssの中でさらにフォルダ分けしてたりする場合はこのままだと動きません

Discussion