開発ガイドラインとGetting started

内向きに開発ガイドラインとGetting Startedを兼ねたようなものを書こうとしたが、目次だけ書き出したけどこれでも環境作るところだけ、この後にコーディング編とテスト編とリポジトリ&CI&デプロイ編があって、そもそもコーディング編にはSPA章とサーバレス章と自前WEBサーバを含むローカルツール章があり、さらにSPA章には画面出すところまでとFluxとコンポーネントルーターAPI通信とイベントハンドリングがあり、フォーム&バリデーションやi18nやレスポンシブがある。。。さらに続く。 いつか暇になって、かつ書いたら効果が高い時にもっと目次も練って書こうと思う。おそらくそんな時は来ないだろうとも思いつつ。

  • node環境構築
    • brewインストール
    • nインストール: brewによる
    • nodeインストール: nによる
    • yarnインストール: brewにより--without-nodeスイッチ付き
  • プロジェクトの作成
    • 空フォルダにpackage.jsonを作る
    • ソースフォルダと出力先フォルダを決める
    • もしくはcliツールの利用: create-react-app、vue-cli、serverlessなど
  • package.jsonの基本的な書き方
    • プロジェクト設定: version、private、license
    • yarn addおよび-Dスイッチ付き
  • トランスパイルとリントの設定
    • yarn add typescript tslint tslint-config-airbnb -D
    • tsconfig.jsonの書き方
      • compilerOptions.target
      • compilerOptions.module
      • compilerOptions.outDir: 出力先フォルダ
      • compilerOptions.strict
      • compilerOptions.libおよびcompilerOptions.typesの意味
      • include: ソースフォルダ
      • exclude
      • compilerOptions.baseUrlとcompilerOptions.pathsの意味
    • tslint.jsonの書き方
      • extends: [tslint-config-airbnb]
      • rulesの意味
      • おまけ:ソースインラインでリント設定を抑制する方法
  • (SPAであれば)バンドラーの設定
    • cliでプロジェクトを作った場合
      • create-react-appの場合
      • vue-cliの場合
    • yarn add webpack webpack-dev-server awesome-typescript-loader tslint-loader -D
    • ルールの書き方
    • DevServerの設定
  • (SPAであれば) WEBブラウザ設定
  • テストツールの設定
    • yarn add jest ts-jest @types/jest @types/node -D
    • package.jsonにjest追加
      • moduleFileExtensions
      • testMatch
      • transform
  • package.jsonでのscriptsの書き方
    • node実行
    • ライブラリのbin
      • tsc
      • jest
      • webpack
    • シェルコマンド
    • rimrafなどの定番ツール
    • 自作JS
  • 環境の維持
    • brewでのupgradeとcleanup
    • nでのアップデートと旧版への切り替えおよび削除
    • yarn upgradeとyarn remove

考察

しかし考えるに、チェック項目として目次だけ書くのも有用かもしれない。その点で目次ツリーの大きさを抑制するためには以下の分岐をまず決める事なんだろう。

  • プラットフォーム:node(python、go)
  • node -> 言語:TypeScript(node、Babel)
  • TypeScript -> 対象:SPA(サーバレス、ローカル)
  • SPA -> フロントエンド:Vue(React、Angular)

でTypeScriptまで決めておけば先の目次みたいな感じになりますな。

gulp v4によるフルTypeScriptなビルド環境

SPAを作るには、webpack等の、色々寄せ集めて一つのJSにバンドルしてくれるツールを用いてビルドしますが、サーバサイドやローカル実行ツールのプロジェクトはそもそもビルドする必要すらありません。しかし、何らかの都合でビルドをしたい場合もあるでしょう。

  • JS以外のファイルをコンパイルしたい
    • TypeScriptで書いてる
    • SCSSとかコンパイルする
    • ドキュメントを生成する
    • テストは除いてリリースビルドを作る
  • ファイルをコピーしたり、ダウンロードしたりして構成を整える
    • フォントファイル!私はこれ
  • 難読化する

自分にニーズが無い時にはピンと来ないのですが、必要になったらどうにかしないといけない。そこでGulp、そしてv4。

https://gulpjs.com/

聞くところによると3年の沈黙を経て、昨年末ギリギリにv4.0.0がリリースされたそうです。gulp使ったことあるのは3年ぐらいは前だったかも。後方互換を保ちつつも、イマドキ風に生まれ変わりました。そしてこれがまたTypeScriptと相性が良いことが判明。

$ yarn add gulp@next gulp-typescript typescript ts-node @types/gulp @types/node@9 del -D

まず関連パッケージをインストールしますが、この時にgulpは@next、@types/nodeは@9とバージョン明示しないと本日時点ではNG。gulpはlatestがまだ3系なのは良いとして、@types/nodeの本日時点のlatestである10.0.0はTypeScriptをコンパイルできない。理由は@types/node@10.0.0とTypeScript本体のlib.d.tsでURLなど一部宣言が被って爆死しちゃうから。いずれ@types/nodeが治すでしょうけど、今日時点では@types/node@9で、治れば @types/gulp -> @types/vinyl-fs: "" -> @types/node: "" とバージョン指定してない連鎖のための10.0.0混入だからインストールする必要もなくなる。

@types/gulpは勇み足なのか本体がまだ3系をメインにしているのに普通に4系のものが入る。

// script/build.ts
import { src, dest, symlink } from 'gulp';
import * as ts from 'gulp-typescript';
const del = require('del');

(function () {
    // クリーンアップ
    del('lib');

    // コンパイル
    src(['src/**/*.ts', '!src/**/__tests__/*.ts'])
        .pipe(ts({
            target: 'es6',
            module: 'commonjs',
            strict: true,
            lib: ['es6', 'dom'],
        }))
        .pipe(dest('lib'));

    // 小サイズファイルコピー
    src('src/**/*.txt').pipe(dest('lib'));

    // 大サイズファイルはリンク
    src('src/**/*.otf').pipe(symlink('lib'));
})();

ビルドファイルは伝統のgulpfile.jsではなく何でも良いので適当な名前の.tsを作ります。ここではbuild.tsにしました。v4でgulp.taskを使わなくてよくなったのでこんな感じにかけます。プラグインもこれまでのものが使えます。ts-node入れてるのに.tsをビルドしててパッと見は奇妙なのですが、これが必要な時もある。

// package.json
{
    "scripts": {
        "build": "ts-node script/build.ts"
    },
    "devDependencies": {
        "@types/gulp": "^4.0.5",
        "@types/node": "9",
        "del": "^3.0.0",
        "gulp": "^4.0.0",
        "gulp-typescript": "^4.0.2",
        "ts-node": "^6.0.1",
        "typescript": "^2.8.3"
    }
}

package.jsonのscriptで、ts-nodeの呼び出しを書きます。以上。プロジェクトルートにscriptディレクトリを作ってそこにコマンドを一つづつファイルで書いて置いておけば良いし、共通のものなんかは関数エキスポートしてどっかでまとめて取り込んで組み立てればいいし。実行は当然以下の通り。

$ yarn build

すげー簡単。超素敵。。。だが冷静にGulpの使い所ってどれだけあるんだろうか。さらにこのぐらいのことを書くのにほとんど似たような素のnode.jsでかけるのに余計なものを詰め込んでまでTypeScriptが要るだろうか?

課題

gulp-typescriptが、オプションでpathsが効かないなど、本家tscと挙動が違う。

結論

gulpじゃなくてもいいや。

// package.json
{
    "scripts": {
        "build": "rimraf lib && tsc -p tsconfig.prod.json && cp src/font/*.otf lib/font",
        "lint": "tslint --project ./ './src/**/*.ts'",
        "test": "jest"
    }
}

元に戻った。例えばこんなこともしてます。

const _ = require('lodash');
const fs = require('fs');

(function () {
    const packageJson = _.omit(
        require('./package.json'),
        ['scripts', 'jest', 'devDependencies'],
    );
    _.set(packageJson, 'bin.helloworld', 'index.js');
    if (fs.existsSync('dist/package.json')) {
        fs.unlinkSync('dist/package.json');
    }
    fs.writeFileSync('dist/package.json', JSON.stringify(packageJson, null, 4));
})();

これは素のnode.jsなスクリプト。ちょっとビルドを整えるぐらいだったらこれでも不自由ないかとも思う。Gulpの良さはgulp.srcで対象ファイルをかき集めてきて、gulp.destでそのまま書き出せることでしょうかね。他に複雑なことをするのであれば、ファイルコピーだけでもgulpの方がわかりやすく便利なので出番が出てきます。複雑なことをするかですね。

fontkitを活用したら、ばっちりテキストのBBoxが取れました

github.com

必要なものだけfontkitの定義ファイルを作る。

// @/renderer/headless/fontkit.d.ts
declare module 'fontkit' {
    function openSync(filename: string): Font;

    class Font {
        ascent: number;
        descent: number;
        unitsPerEm: number;
        layout(text: string): GlyphRun;
    }

    class GlyphRun {
        glyphs: Glyph[];
    }

    class Glyph {
        advanceWidth: number;
    }
}

フォントを用意します。日本語フォントなのに無料で手に入れやすい Google提供のNote Sans SJK JP をダウンロードしてソースフォルダ内に格納します。フォントの定義はCSSファイルを参考に以下の通り。

// @/renderer/headless/noto/index.ts
import * as path from 'path';

export default [
    {
        fontWeight: 100,
        src: path.resolve(__dirname, 'NotoSansCJKjp-Thin.otf'),
    },
    {
        fontWeight: 200,
        src: path.resolve(__dirname, 'NotoSansCJKjp-Light.otf'),
    },
    {
        fontWeight: 300,
        src: path.resolve(__dirname, 'NotoSansCJKjp-DemiLight.otf'),
    },
    {
        fontWeight: 400,
        src: path.resolve(__dirname, 'NotoSansCJKjp-Regular.otf'),
    },
    {
        fontWeight: 500,
        src: path.resolve(__dirname, 'NotoSansCJKjp-Medium.otf'),
    },
    {
        fontWeight: 700,
        src: path.resolve(__dirname, 'NotoSansCJKjp-Bold.otf'),
    },
    {
        fontWeight: 900,
        src: path.resolve(__dirname, 'NotoSansCJKjp-Black.otf'),
    },
];

ここからは呆気ない。計算の意味は、フォントの各定義値をググって見つかる図を見ながら理解ください。

// @/renderer/headless/index.ts
import * as _ from 'lodash';
import { openSync, Font } from 'fontkit';
import noto from '@/renderer/headless/noto'; 

export function getComputedTextWidth(font: Font, fontSize: number, text: string): number {
    const textGlyphs = font.layout(text).glyphs;
    const totalAdvanceWidth = textGlyphs.reduce(
        (previous, current) => previous + current.advanceWidth, 0);
    return totalAdvanceWidth / font.unitsPerEm * fontSize;
}

export function getTextHeight(font: Font, fontSize: number): number {
    return (font.ascent - font.descent) / font.unitsPerEm * fontSize;
}

export type NotoSansCJKJPFontWeight = 100 | 200 | 300 | 400 | 500 | 700 | 900;

export function openNotoSansCJKJPFont(fontWeight: NotoSansCJKJPFontWeight) {
    const fontInfo = _.find(noto, value => _.get(value, 'fontWeight') === fontWeight);
    return openSync(_.get(fontInfo, 'src')!);
}

テストで実行。

import * as fontkit from 'fontkit';

import { openNotoSansCJKJPFont, getComputedTextWidth, getTextHeight } from '@/renderer/headless';

describe('fontkit.spec', () => {
    test('openNotoSansCJKJPFont', () => {
        const font = openNotoSansCJKJPFont(400);
        const width = getComputedTextWidth(font, 16, 'こんにちは世界');
        const height = getTextHeight(font, 16);
        console.log({ width, height });
    });
});

結果。

 PASS  src/renderer/headless/__tests__/headless.test.ts
  ● Console

    console.log src/renderer/headless/__tests__/headless.test.ts:10
      { width: 112, height: 23.68 }

で、これはバッチリ正しい結果でした。これで完全にヘッドレスでSVG組版できる!puppeteerは面白かったけど単なる寄り道だった。

import * as d3 from 'd3';
import { Font } from 'fontkit';
import { getComputedTextWidth, getTextHeight } from '@/renderer/headless';

// 複数行テキストを左寄せで書き出す。返値は書き出したテキストのBBOX。
export function appendText(parent: d3.Selection<any, any, any, any>,
        font: Font, fontSize: number,
        value: string, x: number = 0, y: number = 0): SVGRect {
    const values = value.split('\n');
    if (values.length === 0) {
        return { x, y, width: 0, height: 0 };
    }
    const fontHeight = getTextHeight(font, fontSize);
    const text = parent.append<SVGTextElement>('text')
        .attr('dominant-baseline', 'text-before-edge')
        .attr('font-size', fontSize);
    if (values.length === 1) {
        text.text(value)
            .attr('x', x)
            .attr('y', y);
    } else {
        values.forEach((current, i) => {
            text.append<SVGTSpanElement>('tspan')
                .text(current)
                .attr('x', x)
                .attr((i === 0) ? 'y' : 'dy', (i === 0) ? y : fontHeight);
        });
    }
    const width = d3.max(values, value => getComputedTextWidth(font, fontSize, value))!;
    return { x, y, width, height: fontHeight };
}

これがやりたかったこと。ヘッドレスでSVGTextElementを書き出してそのBBoxをとる。で、取れてた、やったー!

SVGTextElementのBBoxをpuppeteerで取る

なんでこんなことをしてるのかというのは、プロジェクトがクソ忙しい中できちんと説明できないのですが、ふと面白くなっちゃってd3.jsとdagre.jsでSVGを書いてました。

要件としてはブラウザ上でSVGにてテキストが入ったダイアグラムを描く際に、レイアウトのためにテキストの描画高さや幅が欲しかった。取るのは、SVGTextElement$getBBox()で簡単に取れるのですが、これをテストするとなると一転して大変。

 まずコードだけでやる

まずは何も疑わずにjsdomで書きます。ツールはJestです。コードはTypeScript。

import { JSDOM } from 'jsdom';
import * as d3 from 'd3';

describe('dagre-d3.spec', () => {
    test('jsdom-d3', () => {
        const doc = new JSDOM().window.document.documentElement;
        const text = d3.select(doc).select('body').append('svg')
            .attr('width', 1600).attr('height', 900)
            .append<SVGTextElement>('text').attr('x', 40).attr('y', 40)
            .attr('dominant-baseline', 'text-before-edge').attr('font-size', 18)
            .text('Hello World!').node();
        console.log(text!.innerHTML); // <- これはOK
        console.log(text!.getBBox()); // <- TypeError: text.getBBox is not a function
    });
});

爆死です。原因はjsdomのSVGTextElementがgetBBoxを実装していないため。jsdomのソースコード見るとしっかりSVG周辺はオプトアウトされていました。ならばと、このjsdomのSVG欠損問題を解決する代替DOM実装を探します。そして見つけました、svgdom。

    test('d3-svgdom', () => {
        const svgdom = require('svgdom');
        const svg = svgdom.document.documentElement; // documentがいきなりsvgタグ
        const text = d3.select(svg)
            .attr('width', 1600).attr('height', 900)
            .append<SVGTextElement>('text').attr('x', 40).attr('y', 40)
            .attr('dominant-baseline', 'text-before-edge').attr('font-size', 18)
            .text('Hello World!').node();
        console.log(text!.innerHTML); // <- これはOK
        console.log(text!.getBBox()); // <- これも動く!...しかし?
    });

このコードのgetBBox()の出力結果が以下。

 PASS  src/__tests__/dagre-d3 .spec.ts
    ● Console

    console.log src/__tests__/dagre-d3.spec.ts:25
      Hello World!
    console.log src/__tests__/dagre-d3.spec.ts:26
      Box {
        left: 40,
        x: 40,
        top: 20.7607421875,
        y: 20.7607421875,
        width: 103.306640625,
        height: 24.5126953125 }

yの値が40じゃなく、どうやらdominant-baselineが効いてないようです。

また、svgdomは内部でfontkitというパッケージを使い、fontkitはフォントファイルをあらかじめ読み込んでそのグリフやらなんやらをデータ解析して計算していました。で、あらかじめ用意されているフォントは英文のものだけで、あとは自分で入れろと。フォントが違えば結果も違ってしまいますが、その辺なんかめんどいくさいのと、svgdomとfontkit共にTypeScript型定義ファイルがなくてめんどくさいのと、プロジェクト自体もマイナーすぎて信じていいか悩んでしまう。

よって、前置き長かったですがコードだけでやるのは早々に諦めました。jsdomほどのプロジェクトが実装できてないのだから、相当の難度なのでしょう。そこは頑張らなくても怒られないと思う。

諦めてヘッドレスブラウザでやる

github.com

ヘッドレスブラウザでやるのにはpuppeteerを使います。はじめPhantomJSでやろうとしたらドキュメントに色々と開発者のギブアップ宣言があって、これからはpuppeteerだというようなことを読んだので。puppeteerはChrominiumを腹に抱え込んでてヘッドレス動作させます。テストコードと繋ぐのはChrom Dev ToolsのプロトコルでChrom開発チームが頑張ってるらしい。ドキュメント読んでもモチベーションたかそうな気合の入った文言が並ぶ。

puppeteerはブラウザ利用ですから、読み込ませるHTMLを用意します。HTMLからコンソールに書き出すとテストの方からイベントで取れる。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>puppeteer</title>
</head>
<body>
<svg width="1600" height="900">
    <text id="hello_world" x="40" y="40" dominant-baseline="text-before-edge"
            font-family="Roboto" font-size="18" fill="black">Hello World!</text>
</svg>
<script>
    const bbox = document.getElementById('hello_world').getBBox();
    console.log(`console.log => ${bbox.x}, ${bbox.y}, ${bbox.width}, ${bbox.height}`);
</script>
</body>
</html>

テストは以下のように。コンソールを繋ぐのと同時にテスト側からコードをぶち込む方法を並存させています。

import puppeteer from 'puppeteer';
import * as path from 'path';

describe('puppeteer', () => {
    test('puppeteer file', async () => {
        const browser = await puppeteer.launch();
        const page = await browser.newPage();
        // コンソールを覗く
        page.on('console', msg => console.log(msg.text()));
        // fileプロトコルで読み出す
        await page.goto(`file://${path.resolve(__dirname, './index.html')}`);
        // テストの方からセレクターでエレメントを持ってくる
        const result = await page.$eval('#hello_world', (selected) => {
            const bbox = (selected as SVGTextElement).getBBox();
            // プリミティブ型じゃ無いとプロトコルに乗らないので文字列に加工する
            return `$eval => ${bbox.x}, ${bbox.y}, ${bbox.width}, ${bbox.height}`;
        });
        console.log(result);
        await browser.close();
    });
});

結果は以下の通り。

  PASS  src/__tests__/puppeteer.spec.ts
  ● Console

    console.log src/__tests__/puppeteer.spec.ts:9
      console.log => 40, 40, 97.03125, 21
    console.log src/__tests__/puppeteer.spec.ts:18
      $eval => 40, 40, 97.03125, 21

yの値がさっきと違って想定通りの40なのが、やっぱり。ブラウザ使う方がそりゃ安心かなと。

速度の問題もない

このpuppeteerを使う実験の方ですが、ローカルのファイルをfileプロトコルで読みだしてると超速いです。これをhttp/httpsを用いてネット越しに取ってくるとすぐタイムアウトしちゃう。localhostを立てても遅く、そもそも面倒です。クロスサイトにならず全部ローカルに置けるならセキュリティ問題も無い。しかし!

そして課題。大問題。

テストの方でimportしても、それはブラウザの方に送られない。。。なんか方法ありそうだけど、コードの依存関係丸ごとオンザフライで送り込む方法が用意できていないので、テスト準備が至極大変になります。これでTDDな単体テストでグルグル回すというのは無理ゲー。なんか方法ありそうだけどなー。

念のためそもそも

そもそも、getBBox()をしないのであれば一番初めのjsdomで十分にテストできます。d3.jsをまっすぐに使うだけならgetBBox()にぶつかることないと思います。私がぶつかったのは、dagre.jsでレイアウトを整えるのにgetBBox()もしくはgetComputedTextLength()が使いたかったから。

結局SVGTextElementのBBoxは単体テストで気軽に取れないのか。。。fontkitのとこまで戻る?TypeScriptの型定義ファイル作るところからか。。。

可搬性の高いGoのVendoring環境整備その後、dep編

mk.hatenablog.com

direnvでGOPATHを通し、特にまずvendorフォルダを優先することでgo getにていい感じにVendoringするようにしてました。が、しかし、しばらく離れているうちに様子が変わってしまいましたね。

HUGO(https://github.com/gohugoio/hugo)のソース見たときに、リポジトリのルートで未知のGopkg.lockファイルを見つけ、何だろうと調べたのがdep気づきのきっかけです。

github.com

そのほか、Serverless.comでGo言語版のテンプレートを実行しようとしたらdepを使った構成が推奨されていたりと、何度かdep体験が連続しました。depのリリース履歴を見るとファーストリリースは2017年5月で、その後9月のリリースぐらいからじわっと来てたのかなと見て取れます。既にbrewでぶっこめるようにもなってて素敵ですが、brew info depで見ると2018年1月の0.4.1からの登録でした。まさに普及はこれからですね。

dep環境にするには、まずGOPATHを決める。デフォルトに環境変数で決め打ってOKなのですが私は既存環境のこともあるので大好きdirenvで設定。

# ~/go/.envrc
export GOPATH="`pwd`"

前に行ってたようなvendorフォルダとプロジェクトルートを両方通さなければならないということはなく、どっかにGOPATHをまず通す。上記では~/goフォルダに通した。

$ tree -a -L 3 ./go
./go
├── .envrc
├── bin
├── pkg
│   └── dep
│       └── sources
└── src
    ├── aws-hugo
    │   ├── .gitignore
    │   ├── .idea
    │   ├── Gopkg.lock
    │   ├── Gopkg.toml
    │   ├── Makefile
    │   ├── codebuild
    │   ├── codecommit
    │   ├── generate.go
    │   ├── s3
    │   ├── serverless.yml
    │   └── vendor
    └── renames
        ├── Gopkg.lock
        ├── Gopkg.toml
        ├── renames.go
        ├── renames_test.go
        └── vendor

GOPATHフォルダの下に、srcフォルダを作り、その中にプロジェクトを作ります。プロジェクトルートでmainパッケージを置いてその下にパッケージ名で階層掘ってく。

あとは、Makefileの冒頭や手打ちで、dep ensure を実行するとソースコード中に書かれたimportを解決すべくVendoringをdepが頑張る。たまにアップデートするには、dep ensure -updateやdep prune。

問題は、バージョン固定で外部を持ってきたいときがあるならdepでワークフロー組めない。勝手に解決方式のdepではなく自らの意識で持ってくるgo getをdirenvと組み合わせて使うのが変わらずいいと思う。私は全部depに移行でいいや。便利だし。

GoLand...っておい

www.jetbrains.com

しばらく前のSPAプロジェクトを久しぶりに開いてみたのです。同じプロジェクトでTypeScriptのクライアントとGoのサーバを両方書けるようにしたWebStormで作った環境が、どうも何かおかしい。GoのIDEサポートが動いていないのです。

結論として、WebStormに導入していたJetBrailns謹製Go言語プラグインが有料製品に昇格したために元のプラグインが無効にされていた。。。30日無料でGoLandを使い始めました。この決断の前にはAtomで一通りやってみたのですが、どうにも欲しい機能がなかったりする。もしかしたら解決方法あるかもしれないけど、それを探すのが苦痛だったので、GoLandをダウンロードしました。

課金するかはわからない。Pythonは継続して書かなかったのでAtomで間に合わせて、PyCharmには手を出さなかった。。。しかしGoは過去資産があるんだよなー。

AWS Lambdaでのネイティブコード実行

なんと、まだ日本にいます。12月下旬からなので既に2ヶ月以上。流石にそろそろ帰米します。

さて、AWS lambdaのこと。

Lambda Execution Environment and Available Libraries - AWS Lambda

If you are using any native binaries in your code, make sure they are compiled in this environment. Note that only 64-bit binaries are supported on AWS Lambda.

公式のドキュメントの冒頭、初め何を言ってるのか分からなかった。ネイティブバイナリって何のことだろうかと。

調べると要はLambdaの設置Zipパッケージの中にネイティブバイナリを置いておいて、Pythonやnode.jsで書いたLambda関数から呼べるということでした。スピンアップ効率にはちょっと疑問あるけど、腹に抱えこんでさえいればコマンド叩けるんだね。

hugo-lambda/RunHugo.js at bb3709a27a0c19fd6bdfb712305a8ecfdc4c3a59 · ryansb/hugo-lambda · GitHub

    function runHugo(next) {
        console.log("Running hugo");
        var child = spawn("./hugo", ["-v", "--source=" + tmpDir, "--destination=" + pubDir], {});
        child.stdout.on('data', function (data) {
            console.log('hugo-stdout: ' + data);
        });
        child.stderr.on('data', function (data) {
            console.log('hugo-stderr: ' + data);
        });
        child.on('error', function(err) {
            console.log("hugo failed with error: " + err);
            next(err);
        });
        child.on('close', function(code) {
            console.log("hugo exited with code: " + code);
            next(null);
        });
    },

この例示で抱え込まれている「hugo」はGoで書かれているOSSなので、LambdaがGoに対応した今ではもっと直接的なアプローチにも道が開きましたが、これは一つのベストプラクティスだと思う。