Madogiwa Blog

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

Lighthouse CIとCircleCI ArtifactsでRailsアプリケーションのLighthouseのスコアを計測するMEMO

フロントエンドまわりの改善の指標としてLighthouseを使ってパフォーマンス等のスコアを参考にすることがあるかと思うのですが、 なかなかchromeで開いて手動で測定するのは手間だったのでCircle CIで実行して結果をArtifactsで確認できるようしたら結構良かったのでメモしておきます📝

計測した環境はrails 6.0.3.2です🚃

Lighthouse CIでcliでLighthouseを実行できるようにする

まずはLighthouseをcliで実行できるように@lhci/cliをinstallします。

github.com

yarn add -D @lhci/cli@0.4.x

※公式ドキュメントはnpm install -g @lhci/cli@0.4.xをCircleCIのstepで実行するような形になっていましたが開発環境でも見れると便利そうだったのでpackage.jsonで管理するようにしてみました。

その後はプロジェクトのルートにlighthouserc.jsを配置します。

/* 
NOTE: 
- collect
  * settingsは一旦capybara側の設定と合わせた
  * startServerCommandは既存の開発用のportと競合しないように3001で起動
- uploard
  * targetはデフォルトだと`lhci`で結果格納用の環境が必要なのでfilesystemでlocalに保存するようにした
  * outputDirはgitignoreで指定できるおうに固定のディレクトリにした
詳細はこちら: [https://github.com/GoogleChrome/lighthouse-ci/blob/v0.4.4/docs/configuration.md:title]
*/
module.exports = {
  ci: {
    collect: {
      url: ["http://localhost:3001"],
      startServerCommand: `bin/rails s -p 3001`,
      startServerReadyPattern: "Puma starting",
      settings: {chromeFlags: 'headless --disable-gpu --no-sandbox --disable-dev-shm-usage'},
      numberOfRuns: 3
    },
    upload: {
      target: 'filesystem',
      outputDir: './.lighthouseci_result'
    },
  },
};

下記のような形でスクリプトを定義してyarn run lighthorseで実行できるようにしときます。

  "scripts": {
    "lighthorse": "lhci autorun",

これでCLIでlighthouseを実行して結果を確認できるようになりました🎉 .lighthouseci_result/配下のhtmlファイルをブラウザで開くといい感じに確認出来ます✨

$ yarn run lighthorse
yarn run v1.19.2
$ lhci autorun
✅  .lighthouseci/ directory writable
✅  Configuration file found
✅  Chrome installation found
Healthcheck passed!

Started a web server with "bin/rails s -p 3001"...
Running Lighthouse 3 time(s) on http://localhost:3001
Run #1...done.
Run #2...done.
Run #3...done.
Done running Lighthouse!

Dumping 3 reports to disk at /dogfeeds/.lighthouseci_result...
Done writing reports to disk.

Done running autorun.
Done in 48.15s.

$ ls .lighthouseci_result/
localhost-_-2020_08_06_05_59_35.report.html  localhost-_-2020_08_06_05_59_49.report.html  localhost-_-2020_08_06_06_00_03.report.html  manifest.json
localhost-_-2020_08_06_05_59_35.report.json  localhost-_-2020_08_06_05_59_49.report.json  localhost-_-2020_08_06_06_00_03.report.json

CircleCIでLighthouseを実行してArtifactsで結果を確認できるようにする

あとはCircleCIでyarn run lighthouseを実行して.lighthouseci_resultに保存された結果をArtifactsとしてCIの結果画面から確認できるようにします👍

Artifactsの使い方は過去に記事にしてるので、そちらを確認してみてください。

madogiwa0124.hatenablog.com

ポイントは、本番環境と合わせるために下記のような形でcommandsでparametersを定義してNODE_ENVの値を渡せるようにしてあげて、lighthouseの計測前にrails webpacker:compileを本番用のビルドを実行するようにしてあげるようにしました。

commands:
  build_webpack:
    parameters:
      env:
        type: string
        default: test
    steps:
      - run:
          name: build webpack
          command: NODE_ENV=<<parameters.env>> bundle exec rails webpacker:compile

全体のCircleCIの設定ファイルは下記のような感じです。

version: 2.1

web: &web
  - image: circleci/ruby:2.7.0-node-browsers
    environment:
      RAILS_ENV: test
      PGHOST: 127.0.0.1
      DATABASE_USER: circleci
      DATABASE_PASSWORD: password
db: &db
  - image: circleci/postgres
    environment:
      POSTGRES_USER: circleci
      POSTGRES_PASSWORD: password

executors:
  web:
    docker:
      - <<: *web
  web-db:
    docker:
      - <<: *web
      - <<: *db

commands:
  attach_current:
    steps:
      - attach_workspace:
          at: .
  install_node_deps:
    steps:
      - run:
          name: install node dependencies
          command: yarn install
  cache_node_deps:
    steps:
      - save_cache:
          name: Cache node dependencies
          paths:
            - ./node_modules
          key: v1-node-dependencies-{{ checksum "yarn.lock" }}
  restore_node_deps:
    steps:
      - restore_cache:
          name: Restore node dependencies
          keys:
            - v1-node-dependencies-{{ checksum "yarn.lock" }}
            - v1-dependencies-
  configure_bundler:
    steps:
      - run:
          name: Configure Bundler
          command: |
            echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
            source $BASH_ENV
            gem install bundler -v $BUNDLER_VERSION
  install_ruby_deps:
    steps:
      - run:
          name: install dependencies
          command: bundle install --jobs=4 --clean --path ./vendor/bundle
  cache_ruby_deps:
    steps:
      - save_cache:
          name: Cache ruby dependencies
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
  restore_ruby_deps:
    steps:
      - restore_cache:
          name: Restore ruby dependencies
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-
  rails_migration:
    steps:
      - run:
          name: run migration
          command: |
            bundle exec rake db:create
            bundle exec rake db:schema:load
  build_webpack:
    parameters:
      env:
        type: string
        default: test
    steps:
      - run:
          name: build webpack
          command: NODE_ENV=<<parameters.env>> bundle exec rails webpacker:compile
  store_lighthorse_atifacts:
    steps:
      - store_artifacts:
          path: .lighthouseci_result
jobs:
  build:
    executor:
      name: web
    steps:
      - checkout
      - persist_to_workspace:
          root: .
          paths:
            - .
  node_build:
    executor:
      name: web
    steps:
      - attach_current
      - restore_node_deps
      - install_node_deps
      - cache_node_deps
  ruby_build:
    executor:
      name: web
    steps:
      - attach_current
      - configure_bundler
      - restore_ruby_deps
      - install_ruby_deps
      - cache_ruby_deps
  lighthourse:
    executor:
      name: web-db
    steps:
      - attach_current
      - restore_ruby_deps
      - restore_node_deps
      - configure_bundler
      - install_ruby_deps
      - install_node_deps
      - rails_migration
      # 本番と同様のbuildでlighthouseのチェックを行うためpruductionでビルド
      - build_webpack:
          env: production
      - run:
          name: lighthourse
          command: yarn run lighthorse
      - store_lighthorse_atifacts
workflows:
  version: 2
  build:
    jobs:
      - build
      - ruby_build:
          requires:
            - build
      - node_build:
          requires:
            - build
      - lighthourse:
          requires:
            - ruby_build
            - node_build

おわりに

個人のサービスで計測できるようにしたのですが、下記のように結構改善しました✨

before

f:id:madogiwa0124:20200806150902p:plain

after

f:id:madogiwa0124:20200806150926p:plain

やはり定量的に定期的に計測できるようにすると改善のモチベーションが上がっていきますね💪

参考資料

github.com

Ruby on Rails: リクエストのformatによって実行するControllerを分岐させるroutesの設定方法MEMO

Ruby on Railsでエンドポイントを変えずにformatで実行するControllerを分ける方法でちょっと悩んだので解決方法をメモしておきます。

やりたかったこと

個人のサービスでRSS用のXMLを表示するのと普通にHTMLを返却する処理を下記のようにrespond_toで分岐するようにしていたのですが、

# routes
Rails.application.routes.draw do
  resources :posts
end

# controller
class PostsController < ApplicationController
  def index
    @posts = Post.all
    respond_to do |format|
      format.html
      format.rss {
        # RSS特有の処理
        render layout: false 
      }
    end
  end
end

RSS用にに表示する内容とHTMLで画面に表示する内容に差が出てきてContollerを分けてくなってきたのですが、RSS用のエンドポイントは外部に公開していて変更するのは影響が大きいので、routes.rbformatで実行するContorollerを分岐出来ないかと悩んでしました🤔

解決方法

routesではconstraints内でlamdaを使って条件に合致するかどうかをチェックすることができるようで、これを使って解決しました💡

次のようにlambdaを使うことができます。get 'foo', constraints: lambda { |req| req.format == :json } このルーティング指定は明示的なJSONリクエストにのみ一致します。 Rails のルーティング - Railsガイド

修正したコードが下記です。constraints: lambda { |req| req.format == :rss }formatrssのときのみにrss/posts#indexが実行するようにしてあげて、Rss::PostsControllerを追加しています。respond_toの分岐もなくなってスッキリしました👏

Rails.application.routes.draw do
  resources :posts, constraints: { format: :html } # `.rss`に反応しないようにformatをhtmlのみに制限
  get '/posts', to: 'rss/posts#index', constraints: lambda { |req| req.format == :rss } # lambdaを使って.rssのみに制限
end

# controller
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

# controller(rss)
class Rss::PostsController < ApplicationController
  def index
    @posts = Post.all
    # Rss特有の処理
    render layout: false
  end
end

constraints: { format: :rss }では制限出来ないので注意してください。Railsガイドによると下記とのことです。

get 'foo'、constraints: { format: 'json' }はGET /fooと一致します。これはデフォルトでformatがオプションであるためです。 Rails のルーティング - Railsガイド

おわりに

constraints: lambda { }の形で実行するControllerを判定するの、何も考えずに使用すると煩雑になって大変そうですが、リクエストを元に自由に実行するContollerを決めれて便利そうですね👀

参考

railsguides.jp

SimpleCovとCircleCIのArtifactsを使ってカバレッジ測定結果を確認するMEMO

rubyを使ったアプリケーションのテストコードのカバレッジ測定にはSimpleCovを使うことが多いと思いますが、CircleCIのArtifactsを使うと簡単にCircleCIの管理画面上でSimpleCovの結果を確認できたので、そのへんの手順をメモしておきます📝

github.com

Codecovを使った方がPRにコメントしれくれたりと色々便利な面もあるかもですが、お手軽にSimpleCovを使ってプロジェクトのカバレッジを測定できるようになるのはいいのかなと✨

SimpleCovでカバレッジを測定する

今回はRailsRspecの環境で有効化する手順を記載します。

まずはGemfilegroup :testsimplecovを追記してbundle installします。

group :test do
  gem 'simplecov', require: false
end

そしてspec_helperを下記のような記述を追加します。※SimpleCovの設定周りは公式のドキュメントを参考に任意の値に変更してください。

SimpleCov.start do # カバレッジ測定を有効化
  enable_coverage :branch # Branch coverageを有効化
  add_filter '/spec/' # spec配下は測定対象外
  add_filter do |source_file|
    source_file.lines.count < 5 # 5行未満のファイルは対象外
  end

  # model、controllerのファイルはgroupとして見やすくする
  add_group 'Models', 'app/models' く
  add_group 'Controllers', 'app/controllers'
end

この状態でbundle exec rspecでテストを実行するとcoverage配下に結果が格納されるようになります。

$ cd coverage
$ tree
.
|____index.html
|____.last_run.json
|____.resultset.json
|____assets
| |____0.12.2
| | |____images
| | | |____ui-icons_cd0a0a_256x240.png
| | | |____ui-icons_888888_256x240.png
| | | |____ui-bg_glass_75_dadada_1x400.png
| | | |____ui-icons_2e83ff_256x240.png
| | | |____ui-bg_flat_75_ffffff_40x100.png
| | | |____ui-bg_glass_75_e6e6e6_1x400.png
| | | |____ui-bg_glass_65_ffffff_1x400.png
| | | |____ui-bg_glass_95_fef1ec_1x400.png
| | | |____ui-icons_222222_256x240.png
| | | |____ui-bg_highlight-soft_75_cccccc_1x100.png
| | | |____ui-bg_glass_55_fbf9ee_1x400.png
| | | |____ui-icons_454545_256x240.png
| | | |____ui-bg_flat_0_aaaaaa_40x100.png
| | |____application.css
| | |____loading.gif
| | |____colorbox
| | | |____border.png
| | | |____loading.gif
| | | |____controls.png
| | | |____loading_background.png
| | |____favicon_red.png
| | |____favicon_green.png
| | |____favicon_yellow.png
| | |____DataTables-1.10.20
| | | |____images
| | | | |____sort_asc_disabled.png
| | | | |____sort_both.png
| | | | |____sort_desc_disabled.png
| | | | |____sort_desc.png
| | | | |____sort_asc.png
| | |____magnify.png
| | |____application.js
|____.resultset.json.lock

index.htmlをブラウザで開くとカバレッジの測定結果を確認できます✨

f:id:madogiwa0124:20200725155343p:plain

CircleCIのArtifactsでSimpleCovの結果を格納する

Artifactsとは

アーティファクトには、ジョブが完了した後もデータが維持され、ビルドプロセス出力の長期ストレージとして使用できます。 アーティファクトAmazon S3 に保存され、プライベートプロジェクト用の CircleCI アカウントを使用して保護されます。 https://circleci.com/docs/ja/2.0/artifacts/

Job実行時に生成されたファイル等をCircleCIが管理するS3上にアップロードする機能のようです👀 

publicリポジトリの場合はArtifactsで公開したファイルが未ログインでも見れる状態になっているようなので注意してください。

discuss.circleci.com

👇詳しくはこちら

circleci.com

artifactsを使って測定結果をアップロードして確認する

CircleCIのconfigファイルでstore_artifactsを使 うことでアップロードすることができます。

設定ファイルは省略していますが下記のような感じでrspecを実行したあとにcommandsで定義したstore_coverage_atifactsを実行してcovarage配下のファイルをArtifactsにアップロードしています。

commands:
  store_coverage_atifacts:
    steps:
      - store_artifacts:
          path: coverage


jobs:
  ruby_test:
    steps:
      - run:
          name: run tests
          command: bundle exec rspec spec/
      - store_coverage_atifacts

これでCircleCIの画面上からSimpleCovで測定してカバレッジを参照できるようになりました🎉

参考資料

qiita.com

Ruby on Rails: cssの管理をAssets PipelineからWebpakerに移行するときのメモ

個人のアプリケーションを今まではデフォルトのアセットまわり(CSS等)をSprocketsJavaScriptWebpackerでビルドするような形にしてたのですが、 今回はstylesheetをWebpackerで管理するようにしたので、そのへんの手順をメモしておきます📝

対応したアプリケーションのRailsのバージョンは6.0.3.2、Webpackerのバージョンは5.1.1です。

.cssファイルをWebpackerで管理する

今回は.css(.scss)ファイルをWebpackerで管理するための手順をまとめていきます。

assets配下の.cssファイルをjavascript配下に移動

まずはSproketsで管理しているapp/assets配下の.cssファイルをWebpackerで管理するためにapp/javascript配下に移動します。 ※webpacker.ymlでentryまわりの変更している場合は、その設定に従って移動先を指定してください。

今回はWebpackerの公式のドキュメントがapp/javascript/stylesheets配下においてそうだったので、そこに移動するようにしました🙋

# https://github.com/rails/webpacker/blob/master/docs/css.md
app/  
  javascript/  
    stylesheets/  
      application.scss  
      posts.scss  
      comments.scss

entryの.js.cssファイルをimportしてWebapckerのビルド対象にする

Webpacker(Webpack)はentryでimportされたファイルをビルド対象とするので必要な.cssファイルを entryの.js(デフォルトだとapp/javascript/packs配下のjs)でimportしてあげます。 ※Webpackerはデフォルトでsass-loadercss-loader等がinstallされて設定もよしなにしてくれるようです。

// app/javascript/application.js
import '../stylesheets/application.scss'

Webpacker v5からはpacks配下にentryのjsと同名のcssを配置すると自動でimportしてくれるようになったようです、便利✨

By Webpacker convention (as of Webpacker v5), this will bundle application.js and application.scss as part of the same entry point (also described as a multi-file entry point in the webpack docs). https://github.com/rails/webpacker/blob/master/docs/css.md#importing-css-as-a-multi-file-pack-webpacker-v5

これでbin/webpack時にimportしたcssファイルがビルドされて読み込まれるようになりました🙌

しかしデフォルトだとhead内に直接ビルドされたstyleが記述されてしまいます。 styleが比較的軽量な場合は問題ないのですが、S3から.cssファイルを配信するため別ファイルで出力したいケースは多いかと思います。

.cssファイルを個別のファイルとしてビルドするようにする

Webpackerにはmini-css-extract-pluginを使用してimportしたファイルを個別ファイルで出力する機能が提供されています。

使用するためにはwebapcker.ymlextract_csstrueに設定することで.cssファイルを個別のファイルとして出力することができます。

default: &default
  # Extract and emit a css file
  extract_css: true

development: &default
  # Extract and emit a css file
  extract_css: true

// その他環境でも必要に応じて同様に設定

Viewでビルドされた.cssファイルを読み込む

個別に出力された.cssファイルはjavascript_pack_tagと同様にView側の読み込む必要があります。

cssの場合はstylesheet_pack_tagを使って読み込みを行います。

<%= stylesheet_pack_tag 'application' %>

これで.cssファイルをWebapckerでビルドして個別ファイルに出力し、それをView側で読み込むことができるようになりました🎉

おまけ:eslintの対象からCSSまわりを除外する

package.jsonのlintまわりの設定を下記のような形にしていたのですが、javascript配下のcssがeslintの対象となってしまい。。。🤔

  "scripts": {
    "lint": "eslint app/javascript/**/* --ext .vue,.js,.ts",
    "lint-fix": "eslint app/javascript/**/* --ext .vue,.js,.ts --fix",

色々調べてみたところ--extオプションがファイル指定の場合に効かないようで、、、eslintignoreで指定するようにしたのですが、

app/javascript/stylesheets

Note: --ext is only used when the arguments are directories. If you use glob patterns or file names, then --ext is ignored. https://eslint.org/docs/user-guide/command-line-interface#ext

これでは、eslintがeslintignoreと実行時の指定のどちらを優先すればいいかわからず、warning File ignored because of a matching ignore pattern. Use "--no-ignore" to override.の警告が発生してしまいます😢

If you pass a specific file to ESLint, then you will see a warning indicating that the file was skipped. https://eslint.org/docs/user-guide/configuring#ignored-file-warnings

最終的には下記のようなディレクトリ指定にして--extオプションが効くようにして対応しました🙇‍♂️

  "scripts": {
    "lint": "eslint app/javascript --ext .vue,.js,.ts",
    "lint-fix": "eslint app/javascript --ext .vue,.js,.ts --fix",

おまけ: Sproketsの無効化

アセットまわりもWebpackerでビルドするようにするとSproketsはもう必要ないので無効化してあげると依存gemも減らせていい感じです。

※ちなみに新規にアプリケーションを作る場合は--skip-sproketsを指定してrails newすればOKなので楽です👌

Sproketsのrequireをやめる

application.rbrequire 'rails/all'をしているとsprockets/railtierequireされてしまうので、

 %w(
   #...
   sprockets/railtie
 ).each do |railtie|
   begin
     require railtie
# https://github.com/rails/rails/blob/6-0-stable/railties/lib/rails/all.rb#L18

sprockets/railtieを除いて個別にrequireするようにします。

require 'active_record/railtie'
require 'active_storage/engine'
require 'action_controller/railtie'
require 'action_view/railtie'
require 'action_mailer/railtie'
require 'active_job/railtie'
require 'action_cable/engine'
require 'action_mailbox/engine'
require 'action_text/engine'
require 'rails/test_unit/railtie'

config系のファイルからassetsまわりの設定値を削除する

その後config/enviroments配下の各ファイルからconfig.assets関係の設定を削除していきます。

各設定値の詳細はこちら

railsguides.jp

(config.assets.enabledfalseにしといた方がいいのかな?🤔)

私の個人アプリだとこの辺の設定を削除しました。

# config/environments/development.rb
config.assets.debug = true # 削除
config.assets.quiet = true # 削除

# config/environments/production.rb
config.assets.compile = false # 削除

そして、config/initializers/assets.rbも削除します。

sass-railsをGemfileから削除する

最後にGemfileからsass-railsを削除してbundle installします。※uglifier等も残っていたら削除します。

gem 'sass-rails', '>= 6' # 削除

これでSproketsも無効化できました🎉

おわりに

今回はstyleまわりを例にAssets PipelineからWebpakerに移行する手順をちょっと整理してみました。(今回行ったのは.cssだけだったのでシンプルでしたが、実際の案件ではこんなにすんなりはいかなさそう。。。)

Webpack移行の前段としてSproketsとの併用をやめて、Webpacker単体の環境に移行しておくと移行対象が減るので、いいかもですね👀

参考資料

webpacker/css.md at master · rails/webpacker · GitHub

stackoverflow.com

ECMAScript proposalsのTemporalを使って日時の操作をしてみるMEMO🕛

ピュアなJavaScriptで日時の操作をするのつらすぎると思っていたのですが、ECMAScript proposalsのTemporalという日時を扱うためのAPIがあるようで、ちょっと触ってみたのでメモしておきます📝

公式のドキュメントはこちら👇

tc39.es

Temporalを使ってみる

⚠Temporalはproposalなので、今回書いた内容が最新の内容と違う可能性があるので使うときは最新の公式ドキュメントを参照したほうが良いと思います🙇‍♂️

準備

Temporalはnpmで公開されていているので簡単に試すことができます📦

$ npm install --save proposal-temporal

install後にimportしてあげれば🙆‍♂️

// Temporalをimport
import pkg from 'proposal-temporal';
const { Temporal } = pkg;

日時を表すオブジェクトを生成する

Temporal.DateTimeのオブジェクトを生成することで、その後色々な日時まわりの処理を行うことが出来るようです🕛

Temporal.now.dateTime()を使うと現在日時のオブジェクトが取得できて、Temporal.DateTime.fromに年月日...を表す数値、または ISO 8601形式の文字列を渡すことで任意の日時のオブジェクトを取得出来るみたいですね👀

// 現在時刻を取得
const now = Temporal.now.dateTime()
console.log("now", now.toString())

// 任意の日時でオブジェクトを生成
const temporalObj = new Temporal.DateTime(2000,1,1)
console.log('temporalObj', temporalObj.toString())
const temporalObjFrom = Temporal.DateTime.from('2020-01-01T12:30')
console.log('temporalObjFrom', temporalObjFrom.toString())

結果

now 2020-07-11T18:22:58.651378622 
temporalObj 2000-01-01T00:00
temporalObjFrom 2020-01-01T12:30

期間を指定して日時の加減算を行う

N日後やN日前といった期間を指定した日付の加減算もTemporal.dateTimeminusplusを使うことによってできるようです✨(これが欲しかった・・・!)

const now = Temporal.now.dateTime()

console.log("now", now.toString())
console.log("1 seccond ago",   now.minus({seconds: 1}).toString())
console.log("1 seccond since", now.plus({seconds: 1}).toString())
console.log("1 minute ago",    now.minus({minutes: 1}).toString())
console.log("1 minute since",  now.plus({minutes: 1}).toString())
console.log("1 hour ago",      now.minus({hours: 1}).toString())
console.log("1 hour since",    now.plus({hours: 1}).toString())
console.log("yestorday",       now.minus({days: 1}).toString())
console.log("tomorrow",        now.plus({days: 1}).toString())
console.log("1 week ago",      now.minus({weeks: 1}).toString())
console.log("1 week since",    now.plus({weeks: 1}).toString())
console.log("1 month ago",     now.minus({months: 1}).toString())
console.log("1 month since",   now.plus({months: 1}).toString())
console.log("1 year ago",      now.minus({years: 1}).toString())
console.log("1 year since",    now.plus({years: 1}).toString())

結果

now 2020-07-11T18:22:58.651378622
1 seccond ago 2020-07-11T18:22:57.651378622
1 seccond since 2020-07-11T18:22:59.651378622
1 minute ago 2020-07-11T18:21:58.651378622
1 minute since 2020-07-11T18:23:58.651378622
1 hour ago 2020-07-11T17:22:58.651378622
1 hour since 2020-07-11T19:22:58.651378622
yestorday 2020-07-10T18:22:58.651378622
tomorrow 2020-07-12T18:22:58.651378622
1 week ago 2020-07-04T18:22:58.651378622
1 week since 2020-07-18T18:22:58.651378622
1 month ago 2020-06-11T18:22:58.651378622
1 month since 2020-08-11T18:22:58.651378622
1 year ago 2019-07-11T18:22:58.651378622
1 year since 2021-07-11T18:22:58.651378622

期間オブジェクトを使って日時の加減算を行う

加減算は期間を表すTemporal.Durationのオブジェクトをminusplusの引数に渡してあげても可能なようです✨

const now = Temporal.now.dateTime()
console.log("now", now.toString())
const aDay = new Temporal.Duration(0, 0, 0, 1)
console.log("aDaySince", now.plus(aDay).toString())
console.log("aDayAgo", now.minus(aDay).toString())
aDaySince 2020-07-12T18:22:58.651378622
aDayAgo 2020-07-10T18:22:58.651378622

日付のフォーマットを指定

IntlをimportしてTemporal.dateTimetoLocaleStringの引数にフォーマットを指定するオブジェクトを渡してあげることで簡単なフォーマットの変換も行えるみたいですね📅

フォーマットを指定するオブジェクトはこちらを参照してください。

https://tc39.es/ecma402/#datetimeformat-objects

const { Intl } = pkg;
const now = Temporal.now.dateTime()
// MM/ddの形式に変換
console.log("formated now", now.toLocaleString(undefined, { month: "2-digit",day: "2-digit" }))

結果

formated now 07/11

おわりに

普段の開発ではmoment.js等を使用してしまうことが多かったのですが、このような処理が標準導入されると、めちゃめちゃありがたいですね😭✨

安定してきたらmoment.jsではなくTemporalを使うようにしとくと移行も楽なのかなと少し思いました。

その他のproposalも下記にまとまっているようなので見てみると面白そうですね👀

github.com

参考

blogs.igalia.com

Webpacker 5.1以降で.vueファイルをbuild時にTypeScriptの型チェックを実行する方法MEMO

Webpackerのversion5.1からts-loaderではなく、@babel/preset-typescriptを使うようになりました✨

github.com

これによりwebapckのbuild実行時の型チェックをfork-ts-checker-webpack-pluginを使うように変更になりました🤖

github.com

しかし、デフォルトだとbuild時の型チェックが実行されなくなり、さらに.vueファイルを対象に含めるにはひと手間必要だったので、そのへんをメモしておきます📝

The default installation only transpiles your TypeScript code using Babel. https://github.com/rails/webpacker/blob/master/docs/typescript.md#optional-adding-compile-time-type-checking

目次

Webpackerでbuild時に型チェックを実行する方法(.vueは含まない)

公式ドキュメントにもbuild時に型チェックを行う設定例が記載されています👀

// config/webpack/development.js
// ref:https://github.com/rails/webpacker/blob/master/docs/typescript.md#optional-adding-compile-time-type-checking
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const path = require("path");

environment.plugins.append(
  "ForkTsCheckerWebpackPlugin",
  new ForkTsCheckerWebpackPlugin({
    typescript: {
      tsconfig: path.resolve(__dirname, "../../tsconfig.json"),
    },
    async: false,
  })
);

しかし、この設定内容だとVue用の設定が不足していて.vueが型チェックの対象となりません😭

Webpackerでbuild時に.vueを含めて型チェックを実行する方法

fork-ts-checker-webpack-pluginを使ってbuild時に.vueの型チェックを有効にするにはtypescript.extensionsvue: trueを指定する必要があります・・・!

TypeScript extensions options
vue: If true, it enables Vue Single File Component support. https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#typescript-extensions-options

下記のような設定で無事にbuild時に.vueファイルの型チェックが行われるようになりました🙌

const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const path = require("path");

environment.plugins.append(
  "ForkTsCheckerWebpackPlugin",
  new ForkTsCheckerWebpackPlugin({
    typescript: {
      // fork-ts-checker-webpack-pluginの公式ドキュメントではtsconfigのパスはconfigFileで指定するとのことだったのでそれに従った
      configFile: path.resolve(__dirname, "../../tsconfig.json"), 
      extensions: {
        vue: true, // .vueファイルの型チェックを有効化
      }
    },
    async: false,
  })
);

Webpacker難しい・・・!!

circleci ver2.1の新機能`executor`と`commands`を使ってconfigファイルをスッキリさせるMEMO

今更ながらcircleciのVer 2.1の新機能であるexecutorcommandsを使ってcircleciのconfigファイルをスッキリさせてWorkflowもいい感じに見直したので、そのへんをメモしておきます📝

circleci ver2.1新機能executorcommandsとは?

executorcommandsとはcircleciの2.1から追加された新機能です✨

circleci.com

今回は、それぞれの簡単な説明と実際に自分の個人開発で使っているcircleciのconfigを改善してみたので、そのへんを次から書いていきます👍

executor

executorは公式のドキュメントにも記載のある通りjobの実行環境を指定することが出来ます。今まではdockerで記載して、それぞれでimageを記載しているような感じになってしまっていましたが、executorを使うとスッキリと実行環境をjobごとに指定出来ますね🙌

Executors define the environment in which the steps of a job will be run, allowing you to reuse a single executor definition across multiple jobs. https://circleci.com/docs/reference-2-1/#executors

使い方はこんな感じでしょうか⚙

version: 2.1

executors:
  alice:
    docker:
      - image: ruby:2.7.0-alpine
  bob:
    docker:
      - image: node:14.5.0-alpine

jobs:
  alice:
    executor:
      name: alice
    steps:
      - run:
          command: "echo hello!"
  bob:
    executor:
      name: bob
    steps:
      - run:
          command: "echo goodbye!"

実行環境が見やすく定義出来ますね✨

commands

commandsは公式のドキュメントにも記載のある通り、commandsを使うとstepで実行可能なコマンドを定義して再利用することが出来ます🙌

並列性を高めるためにjobを分けたりするときにyamlの定義参照等を使わなくてもスッキリとかけるようになったのかなと思います✨

A command definition defines a sequence of steps as a map to be executed in a job, enabling you to reuse a single command definition across multiple jobs. https://circleci.com/docs/reference-2-1/#commands

使い方はこんな感じでしょうか⚙

version: 2.1

executors:
  alice:
    docker:
      - image: ruby:2.7.0-alpine
  bob:
    docker:
      - image: node:14.5.0-alpine

commands:
  say_hello:
    steps:
      - run:
          name: say hello!!
          command: echo hello!

jobs:
  alice:
    executor:
      name: alice
    steps:
      - say_hello
  bob:
    executor:
      name: bob
    steps:
      - say_hello

いい感じにコマンドが再利用出来ますね✨

見直した結果のBefore/After

今回個人で開発しているRailsアプリケーションで使っていたcircleciのconfigファイルを先程説明した機能で、ちょっと改善してみました⚙

環境は下記のような感じです。

image memo
ruby 2.7.0 with node.js (rails 6.0.3)
db postgresql

Before

改善前はjsまわりとrubyまわりでworkflowを分けていましたが、それだけだったので、どこで落ちたのかわかりにくい + 並列数が上げても並列に動かしにくかったなと💦

あとはjobのstep数が多く + 具体的なコマンドが書かれてしまっていたので可読性が良くなったなと😅

workflow

f:id:madogiwa0124:20200704201734p:plain

config

version: 2

default: &default
  # specify the version you desire here
  - image: circleci/ruby:2.7.0-node-browsers
    environment:
      RAILS_ENV: test
      PGHOST: 127.0.0.1
      DATABASE_USER: circleci
      DATABASE_PASSWORD: password

jobs:
  build:
    docker:
      - <<: *default
      - image: circleci/postgres
        environment:
          POSTGRES_USER: circleci
          POSTGRES_PASSWORD: password
    steps:
      - checkout
      - persist_to_workspace:
          root: .
          paths:
            - .
  node_build:
    docker:
      - <<: *default
    steps:
      - attach_workspace:
          at: .
      - restore_cache:
          keys:
          - v1-node-dependencies-{{ checksum "yarn.lock" }}
          - v1-dependencies-
      - run:
          name: install dependencies
          command: yarn install
      - save_cache:
          paths:
            - ./node_modules
          key: v1-node-dependencies-{{ checksum "yarn.lock" }}
      - run:
          name: run code analyze
          command: yarn lint
  ruby_build:
    docker:
      - <<: *default
      - image: circleci/postgres
        environment:
          POSTGRES_USER: circleci
          POSTGRES_PASSWORD: password
    steps:
      - attach_workspace:
          at: .
      - run:
          name: Configure Bundler
          command: |
            echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
            source $BASH_ENV
            gem install bundler -v $BUNDLER_VERSION
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-
      - run:
          name: install dependencies
          command: bundle install --jobs=4 --retry=3 --path ./vendor/bundle
      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
      - run:
          name: run rubocop
          command: bundle exec rubocop
      - run:
          name: run brakeman
          command: bundle exec brakeman
      - run:
          name: run migration
          command: |
            bundle exec rake db:create
            bundle exec rake db:schema:load
      - run:
          name: run tests
          command: |
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"
            bundle exec rspec $TEST_FILES
workflows:
  version: 2
  build:
    jobs:
      - build
      - ruby_build:
          requires:
            - build
      - node_build:
          requires:
            - build

After

改善後はjs側はライブラリのinstallと静的解析で分けて、ruby側はライブラリのインストールと静的解析とテストで分けてみました🤖

これでjs側とruby側、またruby側も静的解析とテストで並列にCIを回すことができるようになりそうです🙌

あとはexecutorで実行環境をまとめたので、どの環境で動くのかが明確になったのと、commandsを使って実行コマンドを整理したのでstepの中がスッキリして見やすくなったかなと思います✨

workflow

f:id:madogiwa0124:20200704203025p:plain

config

version: 2.1

web: &web
  - image: circleci/ruby:2.7.0-node-browsers
    environment:
      RAILS_ENV: test
      PGHOST: 127.0.0.1
      DATABASE_USER: circleci
      DATABASE_PASSWORD: password
db: &db
  - image: circleci/postgres
    environment:
      POSTGRES_USER: circleci
      POSTGRES_PASSWORD: password

executors:
  web:
    docker:
      - <<: *web
  web-db:
    docker:
      - <<: *web
      - <<: *db

commands:
  attach_current:
    steps:
      - attach_workspace:
          at: .
  install_node_deps:
    steps:
      - run:
          name: install node dependencies
          command: yarn install
  cache_node_deps:
    steps:
      - save_cache:
          name: Cache node dependencies
          paths:
            - ./node_modules
          key: v1-node-dependencies-{{ checksum "yarn.lock" }}
  restore_node_deps:
    steps:
      - restore_cache:
          name: Restore node dependencies
          keys:
            - v1-node-dependencies-{{ checksum "yarn.lock" }}
            - v1-dependencies-
  configure_bundler:
    steps:
      - run:
          name: Configure Bundler
          command: |
            echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
            source $BASH_ENV
            gem install bundler -v $BUNDLER_VERSION
  install_ruby_deps:
    steps:
      - run:
          name: install dependencies
          command: bundle install --jobs=4 --clean --path ./vendor/bundle
  cache_ruby_deps:
    steps:
      - save_cache:
          name: Cache ruby dependencies
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
  restore_ruby_deps:
    steps:
      - restore_cache:
          name: Restore ruby dependencies
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-

jobs:
  build:
    executor:
      name: web
    steps:
      - checkout
      - persist_to_workspace:
          root: .
          paths:
            - .
  node_build:
    executor:
      name: web
    steps:
      - attach_current
      - restore_node_deps
      - install_node_deps
      - cache_node_deps
  node_lint:
    executor:
      name: web
    steps:
      - attach_current
      - restore_node_deps
      - install_node_deps
      - run:
          name: run code analyze
          command: yarn lint
  ruby_build:
    executor:
      name: web
    steps:
      - attach_current
      - configure_bundler
      - restore_ruby_deps
      - install_ruby_deps
      - cache_ruby_deps
  ruby_lint:
    executor:
      name: web
    steps:
      - attach_current
      - restore_ruby_deps
      - configure_bundler
      - install_ruby_deps
      - run:
          name: run rubocop
          command: bundle exec rubocop
      - run:
          name: run brakeman
          command: bundle exec brakeman
  ruby_test:
    executor:
      name: web-db
    steps:
      - attach_current
      - restore_ruby_deps
      - configure_bundler
      - install_ruby_deps
      - run:
          name: run migration
          command: |
            bundle exec rake db:create
            bundle exec rake db:schema:load
      - run:
          name: run tests
          command: bundle exec rspec spec/
workflows:
  version: 2
  build:
    jobs:
      - build
      - ruby_build:
          requires:
            - build
      - ruby_lint:
          requires:
            - ruby_build
      - ruby_test:
          requires:
            - ruby_build
      - node_build:
          requires:
            - build
      - node_lint:
          requires:
            - node_build

参考資料

tech.recruit-mp.co.jp

circleci.com