Humanity

Edit the world by your favorite way

Express で書いてるコードを TypeScript 化した時に対処したこと

tyru.hatenablog.com

の続き。

今度は express-generator で自動生成されたバックエンドのコードを TypeScript 化してみる。

tsconfig.json に “allowJs”: true を指定する

express-generator のコードは bin/www から app.js を require() する形になっているが、 app.ts から app.js を生成したところ、app.js で module.exports = app しているのに 空の Object {} がエクスポートされてしまう。 おそらく bin/www も TypeScript の仕組みに乗っからないと(TypeScript でトランスパイルしないと) require() できないのだと何となく予測したので、bin/www も .ts 化しようと思った… けどめんどいので –allowJs(引数)または “allowJs”: true(tsconfig.json)というので .js ファイルもトランスパイルできるらしい。 あと bin/www は shebang も付いてるので外してやる必要がある。

{
  "compilerOptions": {
    ...
    "allowJs": true,
    ...
  },
  ...
}

ちなみに resolve の extensions に .js を指定しないと require() の時に .js を module として扱ってくれないので注意。

module.exports = {
  // ...
  resolve: {
    // note if using webpack 1 you'd also need a '' in the array as well
    extensions: ['.ts', '.js']
  },
  // ...
};

target: “node” を指定する

otiai10.hatenablog.com

module.exports = {
  // ...
  target: 'node',
  // ...
};

よく分かってないけど target には以下を指定できる。デフォルトは web。

configuration

  • “web” Compile for usage in a browser-like environment (default)
  • “webworker” Compile as WebWorker
  • “node” Compile for usage in a node.js-like environment (use require to load chunks)
  • “async-node” Compile for usage in a node.js-like environment (use fs and vm to load chunks async)
  • “node-webkit” Compile for usage in webkit, uses jsonp chunk loading but also supports build in node.js modules plus require(“nw.gui”) (experimental)
  • “electron” Compile for usage in Electron – supports require-ing Electron-specific modules.
  • “electron-renderer” Compile for electron renderer process, provide a target using JsonpTemplatePlugin, FunctionModulePlugin for browser environment and NodeTargetPlugin and ExternalsPlugin for commonjs and electron bulit-in modules. Note: need webpack >= 1.12.15.

webpack-node-externals を使って require() を展開しないようにする

webpack config のプロパティに externals というものがある。 これは 見つけた require() の中でトランスパイル時に require() 先のモジュールを展開するか(埋め込むか)決めるプロパティ。 externals にファイル名が指定されていれば require() は展開されず、実行時に node によって require される。

しかし一つ一つ指定していくのは面倒。 そこで webpack-node-externals というプラグインがあり、これがエクスポートする nodeExternals() という関数を実行して次のように externals に指定してやれば node_modules 以下のライブラリは一括で展開しないよう指定できる。

WebPackでnodeランタイム向けにビルドする | // sakura note

結果

前回のも含めて以下のようになった。

var path = require('path');
var glob = require('glob');
var nodeExternals = require('webpack-node-externals');

function getFrontendEntries() {
  // Move init.ts to the first element
  var INIT_TS = './src/front/init.ts';
  var entries = glob.sync("./src/front/**/*.ts").sort(function (a, b) {
    return a === INIT_TS ? -1 :
           b === INIT_TS ? 1 :
           (a > b ? 1 : a === b ? 0 : -1)
  });
  return entries;
}

function getBackendEntries() {
  // Transpile './src/back/(.+).[jt]s' into '$1'
  var entries = {};
  glob.sync("./src/back/**/*.?s").forEach(function (entry) {
    entries[entry.replace(/.*src\/back\/(.+)\.[jt]s$/, '$1')] = entry;
  });
  return entries;
}

module.exports = [
  // Frontend
  {
    // 前回の module.exports
    // http://tyru.hatenablog.com/entry/2017/02/19/210947

    entry: getFrontendEntries(),
    output: {
      path: path.resolve("./public/javascripts"),
      // filename: '[name].js'
      filename: 'bundle.js'
    },
    resolve: {
      // note if using webpack 1 you'd also need a '' in the array as well
      extensions: ['.ts', '.js']
    },
    module: {
      // loaders will work with webpack 1 or 2; but will be renamed "rules" in
      // future
      loaders: [
        {
          exclude: /(node_modules)/,
          test: /\.ts$/,
          loader: 'ts-loader'
        },
        {
          test: /\.html$/,
          loader: 'html-loader?minimize'
        },
        {
          test: /\.scss$/,
          loaders: ['style-loader', 'css-loader', 'sass-loader']
        }
      ]
    }
  },

  // Backend
  {
    // 今回追加するもの

    target: 'node',
    externals: [nodeExternals()],
    entry: getBackendEntries(),
    output: {
      path: path.resolve("."),
      filename: '[name].js'
    },
    resolve: {
      // note if using webpack 1 you'd also need a '' in the array as well
      extensions: ['.ts', '.js']
    },
    module: {
      // loaders will work with webpack 1 or 2; but will be renamed "rules" in
      // future
      loaders: [
        {
          exclude: /(node_modules)/,
          test: /\.[jt]s$/,
          loader: 'ts-loader',
          options: {
            transpileOnly: true
          }
        }
      ]
    }
  }
];

出力されるコードは必ず .js がつくので、npm start で起動するスクリプトにも .js を付けてやる。

{
  ...
  "scripts": {
    ...
    "start": "node ./bin/www.js",
    ...
  },
  ...
}

参考記事

TypeScript 入門 + Express 再入門した

モチベーション

以下の記事を見て作りたくなったからです(ありがとうございます)。



以下は入門した時のメモ。

Pug (旧 Jade)

Express

TypeScript

WebPack

TypeScript + Webpack 使った時に出たエラーと対処したこと

loaders に loader を指定する時は test プロパティのパターンで絞り込むこと

html-loader を使おうと思って .html のファイルを require() したら、ts-loader に渡って解釈されてしまったので、絶対つけること。 当たり前っぽいけどこれでハマったので。

ERROR in ./src/component/root-index.html
Module build failed: Error: Could not find file: 'C:\msys64\home\tyru\git\scrader\src\component\root-index.html'.
    at getValidSourceFile (C:\msys64\home\tyru\git\scrader\node_modules\typescript\lib\typescript.js:79277:23)
    at Object.getEmitOutput (C:\msys64\home\tyru\git\scrader\node_modules\typescript\lib\typescript.js:79644:30)
    at getEmit (C:\msys64\home\tyru\git\scrader\node_modules\ts-loader\dist\index.js:99:43)
    at Object.loader (C:\msys64\home\tyru\git\scrader\node_modules\ts-loader\dist\index.js:27:11)
 @ ./src/component/root-index.ts 11:14-42
 @ multi ./src/init.ts ./src/component/root-index.ts ./src/routes.ts
module.exports = {

  // ...

  module: {
    loaders: [
      {
        test: /\.ts$/,     // これを必ずつける
        exclude: /(node_modules)/,
        loader: 'ts-loader'
      },
      {
        test: /\.html$/,
        loader: 'html-loader',
        query: {
          minimize: true
        }
      }
    ]
  }
}

「error TS2304: Cannot find name ‘require'」と言われる時は require() の型宣言をしてやること

declare function require(x: string): any;

このような型宣言をしないと、以下のようなエラーが出る。

ERROR in ./src/component/root-index.ts
(12,15): error TS2304: Cannot find name 'require'.

以下のような通常の require() をしてる時は必要ないみたいだけど、

import angular = require('angular');
import 'angular-ui-router';
angular.module('scrader', ['ui.router']);

このように式の中?で require() をすると型宣言が必要になる。 正直 TypeScript 力が低すぎてよく分かってない。

// 以下の1行がないとコンパイルエラー!!
declare function require(x: string): any;

class RootIndex {
  msg: string;
  constructor() {
    this.msg = 'Hello!';
  }
}

angular.module('scrader')
  .component('rootIndex', {
    template: require('./root-index.html'),
    // template: `<div ng-bind='$ctrl.msg'></div>`,
    controller: [RootIndex]
  });

WebPack と html-loader 便利すぎる。 Gulp より「コード」っぽい設定を書かずに済むし、loader めっちゃ便利だし、WebPack 大好きになってきた。 require() でコードに事前に*1色々埋め込める仕組みというのが気に入った。

参考記事

Special Thanks

*1:「静的に」と言っていいんだろうか?