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歳過ぎても大丈夫だよ。そして若いうちから少々でも仕事でカネにまみれて清濁知れたら、年取ってから少々権限増えて忖度に囲まれても、浮ついて滑稽なことになったりはしない。

JP_Stripes 東京#11

f:id:masataka_k:20190222204551p:plain

最近続けて参加しているJP_Stripe東京に参加しました。Stripe Issuingを実装された方が登壇すると聞いていたのでワクワク参加。実は私も米国のアカウントを使ってAPI招待してもらってますので、一通り実装してみてました。ただ、API叩けてもそれ以上にユーザー管理や認証を作らなければならないし、UIも作らないとアプリとして成り立ちません。多忙にてなかなかそこに手が付いていなかった。そんな中、作りたかったものが製品レベルにすでに作りこまれてました。

以下、メモ貼り付け。

JP_Stripe Connect

  • 初の全国大会
  • 3/21 大阪で開催

Stripe アップデート

Staple 〜Stripe IssuingとAtlasを使ってる〜

  • クラウドキャスト株式会社
  • 国内キャッシュレス環境
  • ポジショニング
    • 法人、SMB向け。
    • 法人決済市場は900兆。個人の3倍。
    • 全ての事業ペイメントを集約したい。入社1日目に従業員へ配布され、会社と従業員とのお金のやり取りをシンプルに
    • 会社と従業員とのお金のやり取りが非効率。現金と紙(レシート/領収書)、特に経費精算。
    • 法人キャッシュレスで解決したい。
      • コーポレートカードを発行
        • 与信の問題
        • 振込手数料:個人が立て替えているから発生する。
    • 国内キャッシュレスのレイヤー
      • 国際ブランド
      • プロセッシング・API
      • フロントエンド、ブランド
  • Stripe Issuing
    • カード発行をAPIで行える、革命的!
    • 物理カードおよび仮想カードを発行できる
    • 始めるのに時間がかからない、長期の契約が不要、高額な手数料がいらない
    • 日本ではまだ使えない。招待オンリー

f:id:masataka_k:20190222205356p:plain

  • Stripe Atlas
    • 米国で会社の設立を簡単に行うサービス
    • WEB上で情報入力。書類を作ってくれる。銀行口座や納税者番号も払い出し
    • 初年度$1,000、次年度以降$500ぐらい必要。

Twilio + Stripeの事例

  • DIGITALJET(岡山) 古里さん
  • 「電話でペイ」Twilio
    • カード番号入力を音声で案内。番号・有効期限・セキュリティコードを入力
    • 決済を行う
  • XMLを書くだけで可能になる。日本円決済も設定するだけ。
  • 手数料は使った分、決済成立毎にTwillioが15円。Stripeが決済金額の3.6%

StripeでのCVCアタック対抗策

  • Cloud9 Holdings 森さん
  • Jupiterという電話占いサービス(Stripe禁止業務かもの危機)
  • Stripeではどうするか
    • Elementsでカード情報を送る場合:Tokenをそのまま使えばカード情報を登録されてないので都度入力。
  • Stripe Radar
    • ルールを設定することで、怪しい決済を防げる。
  • CVCアタック対策
    • CVCの間違いはAPIの戻りで返ってくる
    • カード情報入力から決済までを一括トランザクションにすればRadarで引っ掛けて永久ブロック可能
    • カード登録と決済を別トランザクションにすると、CVCエラーの回数を数えて決済を止める機能を実装する必要がある。