2020/11版:TypeScript+UmbrellaJS+PostCSSの開発環境を yarn で構築する

13 min read読了の目安(約12000字

今回は開発環境ということで minify や uglify などは行わないところまでとします。
また、 git や npm のローカルの準備は既に終わってるものとします。

TL;DR

yarn を初期化して必要なパッケージを入れる

yarn init -y
mkdir public
mkdir -p src/ts
mkdir src/css
yarn add tailwindcss postcss postcss-cli postcss-import postcss-nested --dev
yarn add typescript sirv-cli ttypescript @zoltu/typescript-transformer-append-js-extension rimraf yarn-run-all --dev

typescript / ttypescript 初期設定

yarn run tsc --init

tsconfing.json 編集

{
  "compilerOptions": {
    "target": "ES2017",
    "module": "es2015",
    "sourceMap": true,
    "outDir": "./public/js",
    "rootDir": "src/ts",
    "removeComments": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "plugins": [
      {
        "transform": "@zoltu/typescript-transformer-append-js-extension/output/index.js",
        "after": true,
      }
    ]
  }
}

TailwindCSS / PostCSS 初期設定

yarn run tailwindcss init -p

tailwind.config.js

module.exports = {
  future: {
    // removeDeprecatedGapUtilities: true,
    // purgeLayersByDefault: true,
  },
  purge: [],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}

postcss.config.js

module.exports = (ctx) => ({
  map: ctx.options.map,
  plugins: [
    require('postcss-import'),
    require('tailwindcss'),
    require('postcss-nested'),
    require('autoprefixer'),
  ],
})

テスト用のファイルを用意する

ts, css は冒頭に書いた前回までの記事を参照してもらうとして、
index.html はこんなのを用意しておきます

<!DOCTYPE html>
<html>
  <head>
    <title>index</title>
    <link href="./css/global.css" rel="stylesheet" type="text/css">
    <link href="./css/tailwindcss.css" rel="stylesheet" type="text/css">
    <script type="module" src="./js/index.js"></script>
  </head>
  <body>
    <h1>FooBar</h1>
    <p id="foo"></p>
    <p class="text-purple-600"><strong>strong</strong><p>
    <div class="btn">btn</div>
  </body>
</html>

出力テスト

yarn postcss src/**/*.css -d public/css
yarn ttsc
yarn sirv public --port 8080

出力結果

うん!良い感じ!!

Umbrella JS を導入する

今回はDOM操作ライブラリとして Umbrella JS を導入します。
Umbrella JS はjQueryライクなDOM操作、イベント、Ajaxサポート中心の軽量ライブラリですね。

yarn add umbrellajs --dev

Umbrella JS、私はわりと好きなのですが TypeScript で開発するにはしんどい問題があります。
それは、 DefinitelyTyped で @types/umbrellajs がまだ提供されていないのです。

なので自分でどうにかするしかないですね。💦

今回取ろうとした手段1:無視してanyのままやる

//@ts-ignore
import u from "umbrella";

はい。OKです。トランスパイルが通ります。

今回取った手段2:自分で型定義ファイルを用意する

今回はこちらを選択しました。
1でもよかったのですがコード補完が全く効かないのは「なんで私はTypeScriptを書いているんだっけ?」という気持ちになるのでこちらを選択することにしました。

1. @Types 配下に umbrella.d.ts を用意する

./src/ts/@types/umbrella.d.ts
(./src/@types/umbrella.d.ts でもOK)

/* ======================= */
/* 全然足りてません!未完成です!! */
/* ======================= */
declare module "umbrella" {
  interface umbrellaElement<T extends HTMLElement> extends T  {
    addClass(val: string): void;
    length: number;
  }

  interface umbrellaElementList<T extends HTMLCollection> extends T {
    nodes: Array;
  }

  function u<T extends HTMLElement>(val: string):umbrellaElement<T>;

  function u<T extends HTMLCollection>(val: string):umbrellaElementList<T>;

  export default u;
}

interface に Generics 適用するところで詰まったけどここに助けられた

この型定義ファイルを使うときに Input タグとかを使う時は
u<HTMLInputElement> とするなどしてください

dtsmake や DefinitelyTyped でも推奨されている dts-gen も試してみたのですが
満足のいく結果が得られなかったので今回は自作することにしました。
ある程度定義が揃ったら DefinitelyTyped に出して貢献したいですね……。

2. import すればOK

src/ts/index.ts

import { addBaz } from "./baz"
import u from "umbrella";

window.onload = function () {
  const app = document.getElementById("foo") as HTMLParagraphElement;
  
  app.innerHTML ="Hello Typescript World! foo";
  addBaz(app);

  u<HTMLParagraphElement>("#foo").addClass('main');
  console.log("#foo nodes:", u<HTMLParagraphElement>("#foo").length);
};

3. トランスパイル後のJavaScriptでも読み込めるようにする

TypeScript は import の解決まではやってくれないので
import u from "umbrella"; をトランスパイル後にも参照できるように
自分でどうにかしてやる必要があります。
また、 @zoltu/typescript-transformer-append-js-extension は相対パスは解決してくれるものの node_modules から参照しているものは処理の対象外にしているようです。
なので、ここはまた ttypescript の力を借りて別のプラグインを入れましょう。

yarn add ts-transform-import-path-rewrite --dev

yarn add したら例のごとく tsconfig.json に追記します。
"transform": "ts-transform-import-path-rewrite" のブロックを追加、
umbrella を相対パスでの ./umbrella.js の参照に置換します。

    "plugins": [
      { 
        "transform": "ts-transform-import-path-rewrite", 
        "import": "transform",
        "alias": {
            "^(umbrella)$": "./$1.js"
        },
        "after": true,
        "afterDeclarations": true, 
        "type": "config" 
      },
      {
        "transform": "@zoltu/typescript-transformer-append-js-extension/output/index.js",
        "after": true,
      }
    ]

これで yarn ttsc でトランスパイルしてやると、こうなります。OKですね。
src/ts/index.ts

import u from "umbrella";

public/js/index.js

import u from "./umbrella.js";

4. node_modules 配下のファイルを public/js 配下にコピーする

参照が出力できたのであとは実体を用意してやるようにします。
public/js に パッケージに含まれている umbrella.esm.jsumbrella.js にリネームしてコピーするようにしましょう。

yarn add cpy-cli --dev
# 試してみる
yarn cpy node_modules/umbrellajs/umbrella.esm.js public/js/ --rename=umbrella.js

これで public/js/umbrella.js も用意できるようになりました! OKですね!

今回はわざわざ cpy-cli を使いましたがコレでもいけそうですね

node -e "require('fs').copyFileSync('./node_modules/umbrellajs/umbrella.esm.js', './public/js/umbrella.js')"

さすがに、ここまでやりだすと webpack とか使った方が早いなという気持ちになってきます😂

yarn run の scripts を書く

今回は色々ありますが全部順次実行するようにしましょう。

  "scripts": {
    "cleanjs": "rimraf ./public/js/",
    "cleancss": "rimraf ./public/css/",
    "clean": "run-s cleanjs cleancss copyumbrella",
    "postcss": "postcss src/**/*.css -d ./public/css",
    "watchpostcss": "postcss src/**/*.css -d ./public/css --watch",
    "copyumbrella": "cpy node_modules/umbrellajs/umbrella.esm.js public/js/ --rename=umbrella.js",
    "ttsc": "ttsc",
    "watchts": "ttsc --watch",    
    "cleanwatch": "run-p clean watchpostcss watchts",
    "serve": "sirv public --port 8080",
    "build": "run-s cleanjs cleancss copyumbrella postcss ttsc",
    "dev": "run-p cleanwatch serve"
  },

読めばわかりますが簡単に解説を入れておくと

  • cleanjs
    • rimraf ./public/js/
    • ./public/js/ ディレクトリを削除
  • cleancss
    • rimraf ./public/css/
    • ./public/css/ ディレクトリを削除
  • clean
    • run-s cleanjs cleancss copyumbrella
    • ディレクトリをお掃除(削除)して public/js/umbrella.js をコピーして作成
  • postcss
    • postcss src/**/*.css -d ./public/css
    • postcss でトランスパイル
  • watchpostcss
    • postcss src/**/*.css -d ./public/css --watch
    • postcss でトランスパイルして watch モード
  • copyumbrella
    • cpy node_modules/umbrellajs/umbrella.esm.js public/js/ --rename=umbrella.js
    • public/js/umbrella.js をコピーして作成
  • ttsc
    • ttsc
    • TypeScript を ttypescript でトランスパイル
  • watchts
    • ttsc --watch
    • TypeScript を ttypescript でトランスパイルして watch モード
  • cleanwatch
    • run-p clean watchpostcss watchts
    • cleanしてwatchモードを並列で起動(cleanの完了をまってないのでよくない気がする)
  • serve
    • sirv public --port 8080
    • 静的ファイル配信サーバー起動
  • build
    • run-s cleanjs cleancss copyumbrella postcss ttsc
    • お掃除してcssとtsをトランスパイル
  • dev
    • run-p cleanwatch serve
    • お掃除してcssとtsをトランスパイルしてwatchモードとサーバ起動

最終的な package.json はこんな感じです。

{
  "name": "my_project",
  "version": "1.0.0",
  "main": "index.js",
  "repository": "git@github.com:<userid>/my_project.git",
  "author": "your name",
  "license": "MIT",
  "scripts": {
    "cleanjs": "rimraf ./public/js/",
    "cleancss": "rimraf ./public/css/",
    "clean": "run-s cleanjs cleancss copyumbrella",
    "postcss": "postcss src/**/*.css -d ./public/css",
    "watchpostcss": "postcss src/**/*.css -d ./public/css --watch",
    "copyumbrella": "cpy node_modules/umbrellajs/umbrella.esm.js public/js/ --rename=umbrella.js",
    "ttsc": "ttsc",
    "watchts": "ttsc --watch",    
    "cleanwatch": "run-p clean watchpostcss watchts",
    "serve": "sirv public --port 8080",
    "build": "run-s cleanjs cleancss copyumbrella postcss ttsc",
    "dev": "run-p cleanwatch serve"
  },
  "devDependencies": {
    "@zoltu/typescript-transformer-append-js-extension": "^1.0.1",
    "cpy-cli": "^3.1.1",
    "postcss": "^8.1.7",
    "postcss-cli": "^8.2.0",
    "postcss-import": "^13.0.0",
    "postcss-nested": "^5.0.1",
    "rimraf": "^3.0.2",
    "sirv-cli": "^1.0.8",
    "tailwindcss": "^1.9.6",
    "ts-transform-import-path-rewrite": "^0.3.0",
    "ttypescript": "^1.5.12",
    "typescript": "^4.0.5",
    "umbrellajs": "^3.2.3",
    "yarn-run-all": "^3.1.1"
  }
}

コマンドはこんな感じです。

```bash
# お掃除してトランスパイルしてファイルを揃える
yarn build

# お掃除してcssとtsをトランスパイルしてwatchモードとサーバ起動
# Ctrl+c で停止
# TypeScript ファイル/cssファイルを編集して保存すればトランスパイルが走る
# ブラウザで確認しているときは保存後、ハード再読み込みしましょう
yarn dev

以上です。おつかれさまでした。
今回のプロジェクトファイル一式はこちらです。

https://github.com/JUNKI555/yarn_run_practice04

なんで gulp/webpack 使わないの?

yarn でイケたからです。でも今回はちょっとしんどかった……😂

なんで babel 入れてないの?

IE11対応の予定はないし 前述のES2015対応状況などをみるに私の要件には不要と判断したからです

なんでこんな構成にしたの?

今作ってるサービスのプロトタイプが Ruby on Rails で ERBテンプレートエンジンを使うんですが
フロント側をちょっとDOMいじったり TailwindCSS 使ったりするのに
こういう構成にしました。
なので普段の開発のときは yarn build しか使わないです。
(ファイル配信はRailsがやってくれるので。)
そのうち Rails と一緒にしたものも記事にします。

なんで Blitz.js とか使わないの?

私もそう思います。

その他の参考サイト

Unbrella JS の example によく出てくる Fetch API 関連