Madogiwa Blog

主に技術系の学習メモに使っていきます。

TailwindCSSのエントリー単位で利用するconfigを切り替える方法メモ📝

管理画面ではdaisyUI使うがユーザー画面では使いたくないみたいなケースでTalwindCSSのエントリー毎に利用するtailwind.config.jsを切り替えたくて、やり方を調べたのでメモ📝

結論から言うと管理画面で利用しているadmin/tailwind.css@configで利用したいtailwind.config.jsを指定してあげれば大丈夫だった 🙆

@config Use the @config directive to specify which config file Tailwind should use when compiling that CSS file. https://tailwindcss.com/docs/functions-and-directives#config

以下のような感じでadmin/tailwind.cssで指定してあげれば 🆗

admin/tailwind.css

@config "your_root_path/tailwind.admin.config.js";
@tailwind base;
@tailwind components;
@tailwind utilities;

ユーザー側のtailwind.config.jsではcontentでadmin側で指定しているものは除外するようにするとサイズも無駄に大きくならなくて良さそうでした🍃

tailwind.config.js

module.exports = {
  content: [
    './views/**/*',
    '!./app/views/admin/**/*' // adminで使われているものはcontentの対象から除外する
  ],
  plugins: [daisyui],
  daisyui: {
    themes: ["light"],
  },
};

参考

zenn.dev

github.com

TailwindCSSでデザイントークンのユーティリティクラスのみを含んだcssをビルドする方法MEMO📝

TailwindCSSを使うとtailwind.config.jsを使ってデザイントークンに即したユーティリティクラスを生成することができます。

tailwindcss.com

TailwindCSSのようなユーティリティクラスベースでのスタイリングにすることはページ間の一貫性を守りやすいというメリットはありますが、 リセットCSSやデザイントークン以外のユーティリティクラス等まで導入するとなると、影響範囲が大きくなり既存プロジェクトに導入するとなると色々と大変になってしまうこともあるかと思います。

TailwindCSSを導入していなくても、TailwindCSSで生成したデザイントークンを含んだCSSを利用することで一定のメリットを享受できそうに思ったので、TailwindCSSで全てのデザイントークンのユーティリティクラスのみを含んだcssをビルドする方法をメモしておきます📝

TailwindCSSで全てのデザイントークンのユーティリティクラスのみを含んだcssをビルドする方法

サンプルのconfigファイル

以下のようなtailwind.config.jsがあったケースで考えていきます。

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{html,js}'],
  theme: {
    colors: {
      blue: "#1fb6ff",
      red: "#ff4949",
    },
    fontFamily: {
      sans: ["Graphik", "sans-serif"],
      serif: ["Merriweather", "serif"],
    },
    extend: {},
  },
  plugins: [],
};

デザイントークンのユーティリティクラスのみを含んだcssをビルドする

デザイントークンとして定義されたtextColorのスタイルを行うユーティリティクラスを利用したいときには以下のように設定を修正します。

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{html,js}'],,
+ safelist: [{ pattern: /./ }], // 利用有無に限らず全て出力する
+ corePlugins: ["textColor"], // textColorのみを出力流う。
  theme: {
    colors: {
      blue: "#1fb6ff",
      red: "#ff4949",
    },
    fontFamily: {
      sans: ["Graphik", "sans-serif"],
      serif: ["Merriweather", "serif"],
    },
    extend: {},
  },
  plugins: [],
};

safelistを全てマッチする正規表現を入れることで全てのユーティリティクラスを出力対象とし、 corePluginstextColorを指定することでデザイントークンとして定義されたtextColorのスタイルを行うユーティリティクラスのみを出力するようにしています。

corePluginsで指定できる値は以下に記載されています。

Here’s a list of every core plugin for reference: https://tailwindcss.com/docs/configuration#core-plugins

以下のようなコマンドでビルドすると、

$ npx tailwindcss -i ./tailwind.css -o ./main.css

以下のようなtextColor系のユーティリティクラスに対するCSSだけが生成されます。

.text-blue {
  color: #1fb6ff
}

.text-blue\/0 {
  color: rgb(31 182 255 / 0)
}

.text-blue\/10 {
  color: rgb(31 182 255 / 0.1)
}

.text-blue\/100 {
  color: rgb(31 182 255 / 1)
}

.text-blue\/15 {
  color: rgb(31 182 255 / 0.15)
}

.text-blue\/20 {
  color: rgb(31 182 255 / 0.2)
}

.text-blue\/25 {
  color: rgb(31 182 255 / 0.25)
}

.text-blue\/30 {
  color: rgb(31 182 255 / 0.3)
}

.text-blue\/35 {
  color: rgb(31 182 255 / 0.35)
}

.text-blue\/40 {
  color: rgb(31 182 255 / 0.4)
}

.text-blue\/45 {
  color: rgb(31 182 255 / 0.45)
}

.text-blue\/5 {
  color: rgb(31 182 255 / 0.05)
}

.text-blue\/50 {
  color: rgb(31 182 255 / 0.5)
}

.text-blue\/55 {
  color: rgb(31 182 255 / 0.55)
}

.text-blue\/60 {
  color: rgb(31 182 255 / 0.6)
}

.text-blue\/65 {
  color: rgb(31 182 255 / 0.65)
}

.text-blue\/70 {
  color: rgb(31 182 255 / 0.7)
}

.text-blue\/75 {
  color: rgb(31 182 255 / 0.75)
}

.text-blue\/80 {
  color: rgb(31 182 255 / 0.8)
}

.text-blue\/85 {
  color: rgb(31 182 255 / 0.85)
}

.text-blue\/90 {
  color: rgb(31 182 255 / 0.9)
}

.text-blue\/95 {
  color: rgb(31 182 255 / 0.95)
}

.text-red {
  color: #ff4949
}

.text-red\/0 {
  color: rgb(255 73 73 / 0)
}

.text-red\/10 {
  color: rgb(255 73 73 / 0.1)
}

.text-red\/100 {
  color: rgb(255 73 73 / 1)
}

.text-red\/15 {
  color: rgb(255 73 73 / 0.15)
}

.text-red\/20 {
  color: rgb(255 73 73 / 0.2)
}

.text-red\/25 {
  color: rgb(255 73 73 / 0.25)
}

.text-red\/30 {
  color: rgb(255 73 73 / 0.3)
}

.text-red\/35 {
  color: rgb(255 73 73 / 0.35)
}

.text-red\/40 {
  color: rgb(255 73 73 / 0.4)
}

.text-red\/45 {
  color: rgb(255 73 73 / 0.45)
}

.text-red\/5 {
  color: rgb(255 73 73 / 0.05)
}

.text-red\/50 {
  color: rgb(255 73 73 / 0.5)
}

.text-red\/55 {
  color: rgb(255 73 73 / 0.55)
}

.text-red\/60 {
  color: rgb(255 73 73 / 0.6)
}

.text-red\/65 {
  color: rgb(255 73 73 / 0.65)
}

.text-red\/70 {
  color: rgb(255 73 73 / 0.7)
}

.text-red\/75 {
  color: rgb(255 73 73 / 0.75)
}

.text-red\/80 {
  color: rgb(255 73 73 / 0.8)
}

.text-red\/85 {
  color: rgb(255 73 73 / 0.85)
}

.text-red\/90 {
  color: rgb(255 73 73 / 0.9)
}

.text-red\/95 {
  color: rgb(255 73 73 / 0.95)
}

デザイントークンのユーティリティクラスのみを含んだcssをビルド設定を別ファイルに書く

以下のようにしてデザイントークンのユーティリティクラスのみを含んだcssをビルド設定を分けることもできます。

const defaultConfig = require("./tailwind.config.js");

/** @type {import('tailwindcss').Config} */
module.exports = {
  ...defaultConfig,
  safelist: [{ pattern: /./ }],
  corePlugins: ["textColor", "fontFamily"],
};

その場合のビルドコマンドは以下です。

$ npx tailwindcss -i ./tailwind.css -o ./main.css -c ./tailwind.build.config.js

参考

github.com

tailwindcss.com

Ruby: 外部からprivateなメソッドをpublicにする方法メモ📝

好ましくはないですが、通常Rubyでprivateなメソッドを呼び出したい時にはsendを使うケースが多いです。

docs.ruby-lang.org

しかしライブラリの破壊的変更等によりpublicだっだメソッドがprivateになる等、すでに大量に依存しているケースですべてをsendに直すのは大変なケースもあります。

そういうケースではModule#publicを使うと任意のprivateメソッドをpublicにできて便利だったので使い方をメモ📝

docs.ruby-lang.org

当たり前ですが、以下の実装はprivate methodprivate_foo' called for an instance of Foo (NoMethodError)`が発生し、エラーになります。

class Foo
  def public_foo
    puts "public_foo"
  end

  private

  def private_foo
    puts "private_foo"
  end
end

puts Foo.new.private_foo
#=> test.rb:17:in `<main>': private method `private_foo' called for an instance of Foo (NoMethodError)
# puts Foo.new.private_foo
#            ^^^^^^^^^^^^
# Did you mean?  private_methods

しかし、以下のような感じでpublic :private_fooを使うと外からpublicなメソッドに変更することができます。

class Foo
  def public_foo
    puts "public_foo"
  end

  private

  def private_foo
    puts "private_foo"
  end
end

Foo.class_eval { public :private_foo }

puts Foo.new.private_foo
# => private_foo

使い所は気をつける必要がありますが、実装を外部から柔軟にコントロールできるのはRuby便利💎✨

Ruby: 個人サービスをRuby v3.3.0にアップデートした💎✨

以下の問題があったのでRuby v3.3.0へのアップデートを見送っていたのですが、

madogiwa0124.hatenablog.com

Docker Hubの公式イメージに修正が入り正常に動作するようになったようなのでRuby on Rails製の個人サービスをRuby v3.3.0にアップデートしました💎✨

その後Docker Hubのdocker-library/rubyで本記事の問題が修正されました↓。これにより、Docker HubのRuby 3.3.0イメージは正常に動くようになりました。 PR: Workaround 3.3.0 crash on aarch64 by osyoyu · Pull Request #439 · docker-library/ruby Ruby 3.3.0: aarch64-linux環境でFiber.new{ }.resumeを呼ぶと落ちる問題|TechRacho by BPS株式会社

いつも通りアップデート後にRspecを実行してテストが通ることを確認できました🍏

個人サービスレベルの規模ではありますが、 特別やったことと言えばzeitwerkの警告が出ていたのでcsvを明示的にinstallするようにしたぐらいで、 互換性が担保されているのはありがたいですね🙏

ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/kernel.rb:34: warning: csv was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add csv to your Gemfile or gemspec. Also contact author of zeitwerk-2.6.13 to add csv into its gemspec.

Ruby v3.4からcsvがdefault gemでは無くなるようですね 👀

gihyo.jp

TypeScript/Vue.js/Prettierを使った環境でESLintの新しい設定 Flat Configに移行した📝

2024/04/06 ESLint v9がリリースされました🎉

eslint.org

Flat config is now the default and has some changesとあるように、 v9からは今までとは違うFlat Configという設定方法がデフォルトになります。(ESLINT_USE_FLAT_CONFIGfalseを指定することで今まで通りの設定も使い続けることは可能)

eslint.org

今回は個人で開発しているVue.jsとTypeScriptとPrettierを利用しているサービスでFlat Configに移行してみたので、やったこととかをメモ📝

公式のmigrationガイドもあるので、こちらもご参照ください。

eslint.org

終結

元のESLintのconfigファイル .eslintrc.cjs

module.exports = {
  env: {
    node: true,
    browser: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:vue/vue3-recommended",
    "plugin:vue-scoped-css/base",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
  ],
  root: true,
  plugins: ["@typescript-eslint", "prettier"],
  parser: "vue-eslint-parser",
  parserOptions: {
    parser: "@typescript-eslint/parser",
    ecmaVersion: 2020,
    sourceType: "module",
    extraFileExtensions: [".vue"],
    project: "./tsconfig.json",
  },
  rules: {
    "prettier/prettier": "error",
    "no-console": "warn",
    "@typescript-eslint/no-unused-vars": [
      "warn",
      {
        argsIgnorePattern: "^_",
        varsIgnorePattern: "^_",
        caughtErrorsIgnorePattern: "^_",
      },
    ],
  },
  overrides: [
    {
      files: ["*.vue"],
      rules: {
        "vue/first-attribute-linebreak": "off",
        "vue/max-attributes-per-line": "off",
        "vue/singleline-html-element-content-newline": "off",
        "vue/html-self-closing": "off",
        "vue/max-lines-per-block": [
          "error",
          {
            style: 150,
            template: 200,
            script: 150,
            skipBlankLines: true,
          },
        ],
        "vue-scoped-css/enforce-style-type": ["error", { allows: ["scoped"] }],
        "@typescript-eslint/no-redundant-type-constituents": "off",
        "@typescript-eslint/no-unsafe-assignment": "off",
      },
    },
  ],
};

Flat Configに移行後 eslint.config.js

import js from "@eslint/js";
import ts from "typescript-eslint";
import vue from "eslint-plugin-vue";
import vueParser from "vue-eslint-parser";
import vueCss from "eslint-plugin-vue-scoped-css";
import prettierConfig from "eslint-plugin-prettier/recommended";
import globals from "globals";

export default [
  { ignores: [/* ignores */] },
  js.configs.recommended,
  ...ts.configs.recommendedTypeChecked,
  ...vue.configs["flat/recommended"],
  ...vueCss.configs["flat/base"],
  prettierConfig,
  {
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        ecmaVersion: 2020,
        parser: ts.parser,
        extraFileExtensions: [".vue"],
        sourceType: "module",
        project: ["./tsconfig.json"],
      },
      globals: {
        ...globals.browser,
        ...globals.node,
      },
    },
  },
  {
    rules: {
      "prettier/prettier": "error",
      "no-console": "warn",
      "@typescript-eslint/no-unused-vars": [
        "warn",
        {
          argsIgnorePattern: "^_",
          varsIgnorePattern: "^_",
          caughtErrorsIgnorePattern: "^_",
        },
      ],
    },
  },
  {
    files: ["*.vue", "**/*.vue"],
    rules: {
      "vue/first-attribute-linebreak": "off",
      "vue/max-attributes-per-line": "off",
      "vue/singleline-html-element-content-newline": "off",
      "vue/html-self-closing": "off",
      "vue/max-lines-per-block": [
        "error",
        {
          style: 150,
          template: 200,
          script: 150,
          skipBlankLines: true,
        },
      ],
      "vue-scoped-css/enforce-style-type": ["error", { allows: ["scoped"] }],
      "@typescript-eslint/no-redundant-type-constituents": "off",
      "@typescript-eslint/no-unsafe-assignment": "off",
    },
  },
];

やったこと

.eslintignoreからFlat Config内のignoresに移行する

.eslintignoreは以下の通りignoresで指定するように変更になっています。

To ignore files with flat config, you can use the ignores property in a config object. https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files

以下のようにignoresプロパティに.eslintignoreの内容を文字列の配列に変換して指定するようにしました。

export default [
  { ignores: [ /* ignores files */ ] },

各種関連ライブラリの導入方法をFlat Configに対応したものに修正する

元々はextends等を使って導入していた各種ライブラリをFlat Config対応の導入方法が別途README等に記載されていたので、それに従って利用するように修正しました。

typescript-eslint.

  • ...tseslint.configs.recommended turns on our recommended config.

https://typescript-eslint.io/getting-started#step-2-configuration

eslint-plugin-vue

import pluginVue from 'eslint-plugin-vue'
export default [
  ...pluginVue.configs['flat/recommended'],

https://eslint.vuejs.org/user-guide/#configuration-eslint-config-js

eslint-plugin-prettier

const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended');
module.exports = [
  eslintPluginPrettierRecommended,
];

https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-new-eslintconfigjs

またFlat Configに対応してないライブラリがある場合には、従来のextendspluginsを使って利用することもできるようです。

github.com

global、parser周りの設定をlanguageOptionsに移行する

.vueファイルに対してTypeScript関連も含めて静的解析させるためにparser周りの設定以下を参照しlanguageOptionsに移行しました。

基本的にはlanguageOptionsの中にparserOptionsがあるので、以前のparservue-eslint-parserを使いつつparserOptionsparser@typescript-eslint/parserを使い、extraFileExtensionsも指定することができました。

  {
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        ecmaVersion: 2020,
        parser: ts.parser,
        extraFileExtensions: [".vue"],
        sourceType: "module",
        project: ["./tsconfig.json"],
      },
      globals: {
        ...globals.browser,
        ...globals.node,
      },
    },
  },

また、envプロパティはlanguageOptions内のプロパティとしてglobalsを利用して定義するようになっているので対応しました。

In flat config files, the globals, and parserOptions are consolidated under the languageOptions key; the env property doesn’t exist https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options

github.com

overridesを削除する

今まではglobベースでのルールの切り替えにはoverridesを利用していましたがFlat Configではデフォルトで機能が提供されているようなので

By default, flat config files support different glob pattern-based configs in exported array. You can include the glob pattern in a config object’s files property. https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options

以下のように、overridesを利用せずにそのまま書くことができます。

export default [
  {
    files: ["*.vue", "**/*.vue"],
    rules: { /* overrides rules */ }

実行オプションの--extは削除されたのでglobでの指定に移行する

元々は以下のような形で拡張子を絞って実行していたのですが、

$ eslint app/javascript spec/javascript --ext .vue,.js,.ts

以下の通りFlat Configではサポートされていないのでglobベースの指定に修正しました。

The following CLI flags are no longer supported with the flat config file format:

  • --ext

https://eslint.org/docs/latest/use/configure/migration-guide#cli-flag-changes

$ eslint 'app/javascript/**/*.{js,ts,vue}' 'spec/javascript/**/*.{js,ts,vue}'

おわりに

Flat Confg最初はあまり分かっておらず従来の方が読みやすいような?とか思ってたんですが、理解できるとコード量も少なくスッキリ書けそうなのと、自然なJavaScriptに近い形になっており分割とか管理もしやすそう📝✨

参考

zenn.dev

zenn.dev

Viteでnode_modules内の`.vue`ファイルをビルド対象に含める方法MEMO

ライブラリ内のVue SFCコンポーネントを直接ビルドして利用したい等、Viteではnode_modules内のvueファイルと直接importして利用すると以下のようなエラーが発生します。

<template>
  <Component />
</template>
<script lang="ts" setup>
import Component from "madogiwa-ui/src/components/Component.vue";
</script>
error during build:
Error: [vite]: Rollup failed to resolve import "node_modules/my-packages/src/components/Component.vue" from "app/javascript/components/Component.vue?vue&type=script&lang.ts".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
`build.rollupOptions.external`
    at viteWarn (file:///node_modules/vite/dist/node/chunks/dep-Bh2jsKCM.js:67408:27)
    at onRollupWarning (file:///node_modules/vite/dist/node/chunks/dep-Bh2jsKCM.js:67436:9)
    at onwarn (file:///node_modules/vite/dist/node/chunks/dep-Bh2jsKCM.js:67140:13)
    at file:///node_modules/rollup/dist/es/shared/node-entry.js:18303:13
    at Object.logger [as onLog] (file:///node_modules/rollup/dist/es/shared/node-entry.js:19950:9)
    at ModuleLoader.handleInvalidResolvedId (file:///node_modules/rollup/dist/es/shared/node-entry.js:18893:26)
    at file:///node_modules/rollup/dist/es/shared/node-entry.js:18851:26

色々調べていたのですが、ViteというかRollupはデフォルトではnode_moduleのファイルはビルド済み扱いとなるようです。そのため強制的にビルドするためのオプションがViteにはあり、

By default, linked packages not inside node_modules are not pre-bundled. Use this option to force a linked package to be pre-bundled. https://vitejs.dev/config/dep-optimization-options.html#optimizedeps-include

以下のように利用することでライブラリ内の.vueファイルをビルドして利用することができました 🎉

export default defineConfig({
  plugins: [vue()],
    optimizeDeps: {
+     include: ["my-package"],
    },

ただ外部ライブラリを直接ビルドするのは、babelの設定等々のビルド環境が違うことから想定外の挙動をしたりといった可能性もなきにしもあらずでリスクもありますが、 alias等がライブラリ側と利用側で違ってたりしても動かないので単純にプロジェクト内のVue SFCをパッケージに切り出してメンテナンスしやすいようにするとか、 そういった目的とかでは便利そうですね📝

Ruby on Rails: Capybaraで特定の要素が無くなるのを待つ方法MEMO

Capybaraで、読み込み中を表すコンポーネントが消えるのを待ってからスクショを撮りたいみたいな時に特定の要素が消えるのを待つ方法をMEMO

結論だけ言うと以下のようにスクリーンショットを習得する前にローダーが無くなることを判定すれば良い。

page.has_no_css?(".page-loader")
page.save_screenshot("tmp/foo/bar.png")

www.rubydoc.info

RSpecのmatcherとして提供されているものはhas_link?とかhas_content?とかで大体pageから呼び出せる。

便利!

決まりきったローダーがあるならmoduleに切り出してhelperにしておくと便利そう。

module WaitLoadingComponentHelper
  def wait_loading_component(page)
    page.has_no_css?(".page-loader")
  end
end

参考

blog.willnet.in