VSCodeでJest

最近はどうしてもコード書く機会が少ないので、JetBrains製品のサブスクリプションを維持するのを諦めてこれまで食わず嫌いだったVSCodeに移行したのですが、改めてコード書き始めるとちょいちょいハマったのでメモっておかねば。for 未来の私。

課題

TypeScript + Jestが今までの作法で動かなかった。

解決

必要パッケージを追加する。本体に必要なパッケージ群は別に、Jestで必要なものたちは...

$ yarn add typescript jest ts-jest @types/jest

http://mk.hatenablog.com/archive/2019/05/17

おなじみts-jestはpreset一発で細々としたjest設定が終わり超楽。ドキュメントによるとプリセットは混在する.jsの処理の違いで複数用意されている。テストコードがTypeScriptオンリーなら値は"ts-jest"。そういえばこれ、前にやりました

// package.jsonの関係あるとこだけ抜粋
{
    "jest": {
        "preset": "ts-jest"
    },
    "devDependencies": {
        "@types/jest": "^24.0.19",
        "jest": "^24.9.0",
        "ts-jest": "^24.1.0",
        "typescript": "^3.6.3"
    }
}

ここで問題発生。VSCodeのIntellisenseが期待通りに動作せず、グローバルスコープの、describe・it・jestの名前解決ができない。WEBを調べるとプロジェクトルートにjsconfig.jsonを書けとか見つけたので、VSCodeのドキュメントの該当ページにあたって設定してみたりするも改善しない。

その後にtsconfig.jsonを試行錯誤する中で、以下が当たりだった。 "typeRoots": [ "./node_modules/@types" ] で型ファイルを明示的に取り込んでる。非グローバルはimportでコード上に明示されるのでそもそもに解決できるけど、暗黙となってしまうグローバルの型はここで探索する。これまで経験してきたJetBrains製品(CLion、WebStorm、GoLand)では、"types": [ "jest", "node" ] で読み込んでたのだけど、現在の私のVSCode環境ではそれが上手くなく、 ”typeRoots" にプロジェクトローカルなパス設定となった。もちろん "types" でも方法はあるのだと思うが、こちらの方が堅い上に万一の追加パッケージに柔軟なように考えるのでいいかな。

さらに "types"では、アプリケーション本体に必要なパッケージ群で複数取り込まれていた @types/node で重複エラーが出るという謎動作が出た。パッケージ識別名で捉えるのではなくパスで誤解なく指示するのは、こちらの解決にも効いてる。

// tsconfig.jsonの現在
{
    "compilerOptions": {
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "lib": [ "es6", "esnext" ],
        "module": "commonjs",
        "target": "es5",
        "sourceMap": true,
        "strict": true,
        "typeRoots": [ "./node_modules/@types" ]
    },
    "exclude": [ "node_modules" ]
}

ちなみにlambdaアプリケーションをwebpackでビルドしているので上記その他の設定です。

VSCodeでdeno

愛用するGoLandのサブスクリプションが切れてしまいました。すでにGoLand乗り換えの際にWebStormは捨ててたので、JetBrainsのIDEは無くなった。継続すればいい話なのですが、最近は違う種類の仕事で時間取られて開発作業に関わることが少なく、あってもAtomで仕様書いたりSketchでワイヤー引いたりする程度だったので維持するのをやめました。

そんな中でdenoのプラグインVSCodeにあると本家サイトに書いてあったのを見て、VSCodeにしようかなと思います。退路を断つためAtomはアンインストール。VSCodeを入れて、チュートリアルサイトの学習動画を見て、会社にあったSoftware Design 4月号のVSCode特集をナナメ読み中。

f:id:masataka_k:20190727131558p:plain

プラグインで、deno対応を入れたら、ちゃんとdeno独特なimportを解決してくれました。node上のTypeScriptと異なるのはここだけなのでもう問題ない。デバッガは...どうなるのかな。テスト書けばいいとはいえブレイクポイント置いてインクリメンタルデバッグもできたら最高なのだが。

denoがHomeBrew入りしてたよ

たまにbrew upgradeを打ってますが、今日たまたま気がついた。denoがHomeBrewのFormulaeになってました。何度かあった破壊的変更も最近はなく、手元で書いた自分的便利ツール群が元気にdenoで動いています。

$ brew info deno
deno: stable 0.9.0 (bottled)
Command-line JavaScript / TypeScript engine
https://deno.land/
Not installed
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/deno.rb
==> Dependencies
Build: llvm ✘, ninja ✘, node ✔, rust ✘
==> Requirements
Required: macOS >= 10.11==> Analytics
install: 753 (30 days), 826 (90 days), 1,031 (365 days)
install_on_request: 751 (30 days), 824 (90 days), 1,029 (365 days)
build_error: 0 (30 days)

enzymeがまたしばらくみないうちに変わってた

mk.hatenablog.com

前回は15ヶ月ぶりでしたが、さらに今回は5ヶ月後。enzymeのセットアップ周りが改善されて過去の面倒な作業がなくなってる。ついでにts-jestもちょっと変わってる。

// enzyme.setup.js
const enzyme =  require('enzyme');
const Adapter = require('enzyme-adapter-react-16');

enzyme.configure({ adapter: new Adapter() });

こんなセットアップファイルを用意します。enzymeのサイトの通り。ES6だと動く環境選ぶので、ES5が間違いない。

// package.json
{
    "scripts": {
        "test": "jest",
    },
    "jest": {
        "preset": "ts-jest",
        "moduleNameMapper": {
            "@/(.+)": "<rootDir>/src/$1"
        },
        "setupFilesAfterEnv": [
            "./enzyme.setup.js"
        ]
    },
    "devDependencies": {
        "@types/enzyme": "^3.9.2",
        "@types/jest": "^24.0.13",
        "@types/ramda": "^0.26.8",
        "@types/react": "16.8.17",
        "enzyme": "^3.9.0",
        "enzyme-adapter-react-16": "^1.13.0",
        "jest": "^24.8.0",
        "react-test-renderer": "^16.8.6",
        "ts-jest": "^24.0.2",
        "typescript": "^3.4.5"
    },
    "dependencies": {
        "ramda": "^0.26.1",
        "react": "^16.8.6",
        "react-dom": "^16.8.6"
    }
}

必要なところだけ絞ると、上記の感じ。JSDomを引っ張ってくるような細かな作業が一切なくなってました。jest.moduleNameMapperは、tsconfig.jsonで似たようなことを書いてあるにも関わらず設定を引いてくれないので、こちらでも書いてます。

が、しかし。これはCreate React App(CRA)を用いてない場合。CRAは色々やってくれて便利で、テスト環境も整えてくれているのだけどそれだけではすまない場合もある。Enzymeの場合がそれ。

CRAを利用している場合

Enzymeに限らず、Jestのセットアップファイルは、src/setupTest.tsもしくはsetupTest.jsで書いておくと、react-scriptsが無設定で引っ掛けてくれる。楽チンな一方で、jest.moduleNameMapperなどの仮想で絶対パスを作る機能はreact-scriptsによって利用禁止にされている。CRAはほぼ無設定でReactのボイラープレートを整えてくれて、さらにその後の開発環境にもなってくれているけど、こういったところでブラックボックスや謎仕様はあるので悩ましくなってきました。

denoの開発速度が上がってきた

denoの開発速度が上がってきた。

github.com

毎週マイナー上げてくって。これまでTypeScriptのコンパイラ設定が不可能だったのだけど、出たばかりの0.4.0からtsconfig.jsonを指定できるようになってました。それ以前に破壊的改変があって、先の紹介コードはCLIが変わったために動かなくなっちゃった。これらは大きい変更ですが、今後は何はなくともどんどんマイナー上げてくって言うので想像するに、そろそろ1.0.0への着地が見えてきたってことなのかな?GW入ってから尿路結石が発症して痛くて、アナウンスや議論を追わずツマミ食いしてるので事情は知らない。

この前のコードをちょっと変える。

#!/usr/bin/env deno run --allow-read --allow-write
import { renames } from './renames_lib.ts';
renames();

変更点は、denoにサブコマンドが追加されたことです。denoではなく、deno runとする。ヘルプで--allow-readや--allow-writeのことが記述されてなかったので無くなったのかと思い削ったら意図した動きにはならなかった。ヘルプから今は記述が落ちてても、権限指定は変わらずに必要。

denoが楽しい

TypeScriptの実装系として爆速進化中のdenoです。ちょっとしたシェルスクリプトで書いたりするようなことを、denoで書くのが楽しい。今朝は花粉症の鼻づまりが原因で早起きしてしまったので、午前中は遊びコード書いてた。前に書いたコード断片をかき集めてきて半分ぐらい終わったのですぐ。

# ~/.bash_profile
export PATH=~/.deno/bin:~/bin:$PATH

denoのインストール後、上記の通りパスを通しておく。~/binをスクリプト置き場とする。

今はまだIDEの支援も得られないのでコードどうやって書くかと言うと、テキストエディタでゴリゴリ書くしかない。そのうち愛用中エディタATOMプラグインでtslintやPrettierを使ってみようかと思うけどまだやってない。TDDで書くために、deno_stdと呼ばれる標準のTypeScriptライブラリ中のテストツールを用いた。簡単なテストランナーとアサーションが用意されている。書いたのは写真ファイルのごちゃごちゃしたファイル名を整理するスクリプト

// ~/bin/renames_test.ts
import { test, runTests } from 'https://deno.land/std/testing/mod.ts';
import { assertEquals } from 'https://deno.land/std/testing/asserts.ts';
import { getFileDescriptor, getNumberStr, getOption } from './renames_lib.ts';

test(function NO_EXT() {
  const f = getFileDescriptor('example#1');
  assertEquals(f.skip, true);
});

test(function MULTI_DOT() {
  const f = getFileDescriptor('example.#1.jpeg');
  assertEquals(f.name, 'example.#1');
  assertEquals(f.ext, '.jpg');
});

test(function DOT_FILE_Large() {
  const f = getFileDescriptor('.IGNORE');
  assertEquals(f.skip, true);
});

test(function SKIP() {
  assertEquals(getFileDescriptor('test.bmp').skip, true);
  assertEquals(getFileDescriptor('test.jpeg').skip, false);
});

test(function GET_NUMBER_STRING() {
  assertEquals(getNumberStr('t3st#12p.png'), '012');
  assertEquals(getNumberStr('t3st#12p.png', true, 0, 4), '0003');
  assertEquals(getNumberStr('t3st#12p.png', false, 0, 4), '0012');
  assertEquals(getNumberStr('t3st#12p.png', false, 5, 4), '0017');
  assertEquals(getNumberStr('t3st#12p.png', false, -5, 4), '0007');
});

test(function GET_OPTION() {
  assertEquals(
    getOption('P', 'pre', ['CMD', '-P', 'test']),
    { flag: true, value: 'test' },
  );
  assertEquals(
    getOption('P', 'pre', ['CMD', '--pre', 'test']),
    { flag: true, value: 'test' },
  );
  assertEquals(
    getOption('P', 'pre', ['CMD', 'x']),
    { flag: false, value: '' },
  );
  assertEquals(
    getOption('P', 'pre', ['CMD', '-P', '-test']),
    { flag: true, value: '' },
  );
});

runTests();

面白いのは、testに渡すテスト関数の名前がテスト名になるところ。「NO_EXT」とか「MULTI_DOT」とかが用いられる。無駄がない。

// ~/bin/renames_lib.ts
const KNOWN_EXTS = ['.jpg', '.jpeg', '.png', '.tiff', '.gif', '.svg'];

type Descriptor = {
  ext: string;
  name: string;
  skip: boolean;
}

export function getFileDescriptor(
  fileName: string,
): Descriptor {
  const result = fileName.match(/(.*)(\..*)$/);
  if (result && result[1].length !== 0) {
    let ext = result[2].toLowerCase();
    if (ext === '.jpeg') {
      ext = '.jpg';
    }
    if (KNOWN_EXTS.includes(ext)) {
      return {
        ext,
        name: result[1],
        skip: false,
      };
    }
  }
  return {
    ext: '',
    name: fileName,
    skip: true,
  }
}

export function getNumberStr(
  name: string,
  start: boolean = false,
  offset: number = 0,
  len: number = 3,
): string {
  const result = name.match(/([0-9]+)/g);
  if (result) {
    const str1 = start ? result[0] : result[result.length - 1];
    const num = Number(str1) + offset
    const zero = new Array<string>(len - 1);
    zero.fill('0');
    const str2 = zero.join('') + num;
    return str2.slice(-(len)); // ほんまかいなコード
  }
  return '';
}

type Option = {
  flag: boolean;
  value: string;
}

export function getOption(
  s: string,
  l: string,
  args: string[],
): Option {
  let i = args.indexOf(`-${s}`);
  if (i === -1) {
    i = args.indexOf(`--${l}`);
  }
  if (i !== -1 && i < args.length - 1 && !args[i + 1].startsWith('-')) {
    return {
      flag: true,
      value: args[i + 1],
    }
  }
  return {
    flag: i !== -1,
    value: '',
  }
}

export function renames() {
  const dir = getOption('R', 'dir', Deno.args);
  const start = getOption('S', 'start', Deno.args);
  const offset = getOption('O', 'offset', Deno.args);
  const len = getOption('L', 'len', Deno.args);
  const prefix = getOption('P', 'prefix', Deno.args);
  const postfix = getOption('T', 'postfix', Deno.args);
  const dryrun = getOption('N', 'dryrun', Deno.args);

  console.log(Deno.args.join(' '));
  console.log(`dir: { flag: ${dir.flag}, value: ${dir.value} }`);
  console.log(`start: { flag: ${start.flag}, value: ${start.value} }`);
  console.log(`offset: { flag: ${offset.flag}, value: ${offset.value} }`);
  console.log(`len: { flag: ${len.flag}, value: ${len.value} }`);
  console.log(`prefix: { flag: ${prefix.flag}, value: ${prefix.value} }`);
  console.log(`postfix: { flag: ${postfix.flag}, value: ${postfix.value} }`);
  console.log(`dryrun: { flag: ${dryrun.flag}, value: ${dryrun.value} }`);

  for (const file of Deno.readDirSync(Deno.cwd())) {
    // 2019.5.21 deno v0.6.0からFileInfo.pathが廃止され、.nameに統一
    // const oldPath = file.path;
    const oldPath = file.name;
    if (file.isDirectory() && !dir.flag) {
      console.log(`${oldPath} skip: directory`);
      continue;
    }
    const descriptor = getFileDescriptor(file.name);
    if (descriptor.skip && !file.isDirectory()) {
      console.log(`${oldPath} skip: ext`);
      continue;
    }
    const o = offset.flag ? Number(offset.value) : 0;
    const l = len.flag ? Number(len.value) : 3;
    const num = getNumberStr(descriptor.name, start.flag, o, l);
    if (num.length === 0) {
      console.log(`${oldPath} skip: no number`);
      continue;
    }
    const p = prefix.flag ? prefix.value : '';
    const t = postfix.flag ? postfix.value : '';
    const newName = `${p}${num}${t}${descriptor.ext}`;
    if (dryrun.flag) {
      console.log(`Dry run: ${oldPath} -> ${newName}`);
      continue;
    }
    console.log(`${oldPath} -> ${newName}`);
    Deno.renameSync(oldPath, newName);
  }
}

グローバル変数「Deno」が、ファイル操作やネットワークI/O等のdenoが用意するAPIのエンドポイントになっている。importソースの表記はちょっと違うけど、あとは普通にTypeScript3.3。上記コードで考え込んだのは、文字列のフォーマットを行うところ。この環境で使えるスマートなやり方がわからなかったので、配列を0で初期化してjoinして文字列にくっつけてsliceする、ほんまかいなと。

#!/usr/bin/env deno --allow-read --allow-write
import { renames } from './renames_lib.ts';
renames();

最後にコマンドとしてchmod +xしたファイルを用意する。テストしやすくするために分けた。この最後の実行ファイルの名前をrenames(拡張子なし)として、あとは普通にシェルでタイプすると使える。例外処理が甘いけど、自分で使う分には便利。

若いうちにやっとくべきこと

ameblo.jp

僭越ながら同窓にて旧知の教育評論家おおた氏の短文にいい事書いてあった。若い時に聞いても意味がワカンナイんだろうなあとも思うが、全くその通り。汗臭い思い出も甘酸っぱい思い出もそれぞれ糧として後には得られないほど貴重で大きい。微妙に老害ゾーンに入りつつある私のソフトウェアの仕事を振り返っても、説く内容について美しさが数段落ちるが構造は似ている。コードは汗臭く、カネは甘酸っぱい。

浪人したり留年したり長く親のスネをかじってて毎日ダラダラしていた学生時代、1995年初夏に就活終わった後でやってみた人生初のアルバイトがいきなり連結会計パッケージ開発だった。学校で会計を学んでいたので連結会計ならばともかく、プログラミングの方がわからない。それでも朝から晩まで貪欲に知識を吸収しながらひたすら書き続けた。ビジネストラストのWikipediaで沿革に残る1995年から96年にかけての行は、期間にして一年にもならないけど、20年以上経った今でも鮮明に覚えている。初めて触ったVisualBasicから繰り出す稚拙なロジックで会計なのにRDBMSも使わず(=使えず)に書き、ウィザードの初期名「Form1」を残したまま何十もの画面をひたすら作った。リリース後、クライマーズ・ハイが悪い作用をして就職を断りまたダラダラする。あまりに酸っぱくてその後すぐにDelphiに走り、偶然が重なりニフティサーブのBBSでFDelphiを作ったところから結局のところ現在まで繋がってる。技術もコミュニティも面白くて、のめり込むうちに気がついたら人脈ができ、連結大王で貰ったカネとでグルージェントを起業した。それまでまともに会社勤めた事がなかったけど、起業の後は切羽詰まった資金繰りでカネの事を考える毎日で、辛いも甘いも黒いも白いも様々な経験ができた。

起業して5年も経った時、ヒトでもめてカネにいろんな事があって、解決策として旧恩を頼りビジネストラスト(すでにデカくなってて、連結大王のファーストコードは書き直されて残ってなかった)にグルージェントを売った。ヒトの問題を解決し、程なくしてITバブル後の混乱も収まったのでカネ面でも安定し、そのおかげで余裕もでき、たまたまFDelphiの縁が蘇ってSeasarファウンデーションって言うコミュニティを作った(私の目線ではSeasarはFDelphi2.0)。Seasar傘下で、MayaaというJavaフレームワークを書いた。経産省未踏ソフトウェアに応募して開発人件費(実は15百万円/年)の支援が得られたのでそれこそ仕事として朝から晩まで書いてた。この辺はリアルタイムにずっとこのブログに書いてたので、読み返せば今でも土臭い感じで恥ずかしい。が、貴重なプログラミングの加圧トレーニングとなってた。その後、SeasarファウンデーションからOSS繋がりとして知己を得たサイオスへビジネストラストに無茶言ってグルージェントの株式を転売してもらって、サイオスでは役員になった。自己流で社内の空気と資金繰りだけなんとかしておけばいいそれまでの経営と違って、予算立案とか中期経営計画とか大学の教科書でしか読んだ事ないのが連続して求められる。初めは何度か失敗もしたが、小僧でも自分で会社何年もやってりゃ筋力ついてる。なんとかなった。

その後、グルージェントのビジネスモデルを大改造してSaaSサブスクリプションの会社にしたり、アメリカに4年行き、FDelphiを一緒に作った新井氏と20年ぶりの合流でまた会社作ったり、色々やってきての今。おおた氏が説くように汗臭い思い出があればなんとかなり、甘酸っぱい思い出があれば身を持ち崩さない。技術トレンドがどんどん変わってもなんとか追いつけるし、経営でも今後そうそうドブ板を踏み破ることはないと思う。

技術者人生早いうちに一年ぐらい朝から晩までひたすらコード書く時期があったら、35歳過ぎても大丈夫だよ。そして若いうちから少々でも仕事でカネにまみれて清濁知れたら、年取ってから少々権限増えて忖度に囲まれても、浮ついて滑稽なことになったりはしない。