LoginSignup
4

More than 5 years have passed since last update.

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

Posted at

いつもの手順をメモ。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"
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4