typescriptのコードをvscodeでデバッグしつつコンパイルしてサーバに転送する雛形


いつもの手順をメモ。windows環境。gulpのところが環境依存強くて、rm -rfコマンド投げてるので注意。gulpのところはもっといい方法があるはずなので指摘歓迎。

最終的に以下のファイルが設置される

/.vscode/launch.json
/.vscode/settings.json
/.vscode/tasks.json
/.gitignore
/gulpConfig.json
/gulpfile.js
/package.json
/package-lock.json
/pm2.json
/tsconfig.json

npm初期化

gulpはバージョン4系を使うのでnextを指定。お好みでexpressを指定したり。

シェル実行
$ npm init -y
$ npm install --save dateformat fs-extra node-fetch
$ npm install --save-dev @types/dateformat @types/fs-extra @types/node @types/node-fetch typescript gulp@next gulp-ssh ssh-config
$ mkdir ./src
$ mkdir ./.vscode

typescript設定

かなり厳しい。未使用の変数や引数が全部エラーになるので。

参考サイト

/tsconfig.json
{
  "compilerOptions": {
    // コンパイルバージョン。これを古い値にすると、xxというメソッドは無い!というエラーが出るようになる
    "target": "es2017",
    // nodejsが標準対応しているcommonjsを指定
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./out",
    "rootDir": "./src",
    // 文字コード指定。念の為。
    "charset": "utf8",
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    // d.tsファイルも作る事を指定する
    "declaration": true,
    // trueでBOMを削除する。念の為
    "emitBOM": true,
    // inlineSourceMapはchrome68で非対応なので無し。この項目を指定する時は"sourceMap"の指定を削除する必要がある。
    //"inlineSourceMap": true
    // ソースマップにソースコード自体を入れる。
    "inlineSources": true,
    "newLine": "LF",
    "noEmitOnError": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitUseStrict": false,
    // trueで未使用のローカル変数をエラーにする。過剰かも
    "noUnusedLocals": false,
    // trueで未使用の関数の引数をエラーにする。過剰かも
    "noUnusedParameters": true,
    // tureでコンパイルにかかった時間やメモリ使用量を表示する。邪魔かも
    "diagnostics": true,
    "strictNullChecks": true,
  }
}

vscode設定

typescriptでビルドしてローカルで実行する起動と、gulpを使ってサーバにデプロイする起動の二種類。

/.vscode/launch.json
{
  // IntelliSense を使用して利用可能な属性を学べます。
  // 既存の属性の説明をホバーして表示します。
  // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "プログラムの起動",
      "program": "${workspaceFolder}/out/index.js",
      "preLaunchTask": "typescriptをビルド",
      "outFiles": [
        "${workspaceFolder}/out/**/*.js"
      ]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "gulp-deploy",
      "program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js",
      "args": [
        "deploy"
      ]
    }
  ]
}
/.vscode/tasks.json
{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "label": "typescriptをビルド",
      "type": "typescript",
      "tsconfig": "tsconfig.json",
      "problemMatcher": [
        "$tsc"
      ]
    }
  ]
}

ウィンドウのタイトルをカスタマイズするのと、typescriptコンパイラをプロジェクトのそれに指定。

/.vscode/setting.json
{
  "window.title": "プロジェクト名 - ${activeEditorMedium}${separator}${rootPath}",
  "typescript.tsdk": "node_modules\\typescript\\lib"
}

.gitignore

https://github.com/github/gitignore/blob/master/Node.gitignore から取る。一番下に/outを追加する。ただし、typescriptをコンパイルしたjsもコミットする必要がある場合は/out/は不要なので注意。

/.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next

/out/

gulp

gulpConfig.jsonファイルで転送先のホスト名と、ホストの中でのファイル設置場所を指定する。

/gulpConfig.json
{
  "sshHost": "example.com",
  "targetDirectory": "/home/ec2-user/app/"
}

gulpのタスクはdeployの一つだけ。この中で、typescriptをコンパイル、指定されたホスト名の設定をsshconfigファイルから読み込み、ssh接続、転送先のディレクトリを削除、コンパイル済みのファイルを指定のディレクトリに転送 という処理をしている。転送後にpm2 restart xxxのようなコマンドが必要な場合があるが、それは非対応。

危険を犯してまで転送先のディレクトリを削除している理由は、ローカルから削除したファイルがサーバで残っているのが嫌だから。

/gulpfile.js
const gulp = require('gulp');
const gulpSSH = require('gulp-ssh');
const SSHConfig = require('ssh-config');
const path = require("path");
const fs = require("fs");
const childProcess = require("child_process");
const 転送先host = require("./gulpConfig.json").sshHost;
const 転送先ディレクトリ = require("./gulpConfig.json").targetDirectory;
if (転送先host == "" || 転送先ディレクトリ == "") {
  throw new Error(`gulpConfig.jsonの値がカラです。`);
}
const ssh = getSSHInstance();
gulp.task("deploy", async (done) => {
  await typescriptをコンパイル();
  await sshから一つのコマンドを実行(`rm -rf "${転送先ディレクトリ}" ; exit;\n`);
  await ファイルを転送();
  await sshから一つのコマンドを実行(`cd "${転送先ディレクトリ}" ; npm install --production; exit;\n`);
  done();
});
function ファイルを転送() {
  return new Promise((resolve, reject) => {
    gulp.src(['./**/*.*', '!**/node_modules/**', '!**/.vscode/**'])
      .pipe(ssh.dest(転送先ディレクトリ))
      .on("finish", () => { resolve(); });
  });
}
function sshから一つのコマンドを実行(command) {
  return new Promise(resolve => {
    const client = ssh.getClient();
    sshClientからshellのchannelを取得(client).then(channel => {
      // dataを受信しないとcloseが発火しない。何故・・・
      channel.on("data", (data) => {
        console.log(data.toString("utf-8"));
      });
      channel.on("close", () => {
        channel.end();
        client.end();
        resolve();
      });
      channel.end(command);
    });
  });
}
function sshClientからshellのchannelを取得(client) {
  return new Promise((resolve, reject) => {
    client.gulpReady(() => {
      client.shell((err, channel) => {
        if (err) {
          console.error(`sshのシェルを取得する事に失敗しました。`);
          reject(err);
        } else {
          resolve(channel);
        }
      });
    });
  });
}
function typescriptをコンパイル() {
  return new Promise((resolve, reject) => {
    console.log(`typescriptをコンパイル。`);
    const cp = childProcess.spawn(`node`, [`./node_modules/typescript/bin/tsc`]);
    cp.stderr.on("data", (data) => {
      console.error(data.toString("utf-8"));
    });
    cp.stdout.on("data", (data) => {
      console.log(data.toString("utf-8"));
    })
    cp.on("exit", (code) => {
      if (code !== 0) {
        console.error(`typescriptのコンパイルに失敗しました。`);
        reject();
      } else {
        resolve();
      }
    });
  });
}
function getSSHInstance() {
  if (process.platform !== "win32") {
    throw new Error('win32ではありません。');
  }
  const sshConfigPath = path.join(process.env["USERPROFILE"], ".ssh", "config");
  if (fs.existsSync(sshConfigPath) == false) {
    throw new Error('ssh_configがありません。');
  }
  const parseSshConfig = SSHConfig.parse(fs.readFileSync(sshConfigPath).toString("utf-8")).find({ Host: 転送先host });
  const port = parseSshConfig.config.filter(a => a.param === "Port").map(a => a.value).reduce((_, b) => b, null);
  const IdentityFile = parseSshConfig.config.filter(a => a.param === "IdentityFile").map(a => a.value).reduce((_, b) => b, null);
  const username = parseSshConfig.config.filter(a => a.param === "User").map(a => a.value).reduce((_, b) => b, null);
  return new gulpSSH({
    sshConfig: {
      host: 転送先host,
      port: port,
      username: username,
      privateKey: fs.readFileSync(IdentityFile).toString("utf-8")
    }
  });
}

pm2

おまけ。

pm2.json
{
  "name": "project-name",
  "script": "./out/index.js"
}