Подписаться на блог

Диплом, часть 2. Сборка и CI

Как уже было сказано в предыдущем посте, диплом я буду писать полностью на JavaScript. 2018 год, поэтому никакие Gulp’ы (или Makefile’ы) для сборки JS я использовать не буду. Обойдусь одним WebPack.

Изначально я планировал собирать только FrontEnd, но потом и BackEnd решил тоже включить в сборку, ибо это даст больше профита, т. к. нативно NodeJS не всё хорошо поддерживает. Да и это отличная возможность пощупать TypeScript везде, где возможно.

Т. к. собираются и клиент и сервер, то будет соответственно 2 конфига webpack’a. Ничего особенного в них нет, если не брать во внимание, что в каждом конфиге есть и babel, и TypeScript. Приложение я планирую писать всё-таки на JS, а всякую сложную логику на TypeScript. В результате такой “архитектуры” у меня и в конфигах некоторая путаница (на первый взгляд), и линтеров целых 2. Ну, опять же в первую очередь я хочу хоть чуть пощупать TypeScript, так что приемлемо.

Конфиги клиента так же имеют кучу всяких loader’ов для стилей, картинок и всего-всего, но в них углубляться не буду. “Для вязкости” были добавлены всякие плагины.

В production-сборке:

config.plugins = [
  new CaseSensitivePathsPlugin(),
  new HappyPack({
    id: 'JavaScript',
    threads: Math.min(os.cpus().length, 4),
    loaders: [{
      loader: 'babel-loader',
    }],
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks(module) {
      return module.context && module.context.indexOf('node_modules') !== -1;
    },
  }),
  new JavaScriptObfuscator({
    rotateUnicodeArray: true,
  }),
  new HtmlWebpackPlugin({
    title: 'Event-Listener',
    template: '../src/client/templates/production.html',
  }),
  new CompressionPlugin({
    algorithm: 'gzip',
  }),
];

В development-сборке:

config.plugins = [
  new HappyPack({
    id: 'JavaScript',
    threads: Math.min(os.cpus().length, 4),
    loaders: [{
      loader: 'babel-loader',
    }],
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks(module) {
      return module.context && module.context.indexOf('node_modules') !== -1;
    },
  }),
  new HtmlWebpackPlugin({
    title: 'Event-Listener',
    template: '../src/client/templates/development.html',
  }),
  new CaseSensitivePathsPlugin(),
];

Серверный конфиг оказался поинтереснее для меня, ибо их я никогда не писал, да и в результате поиска решений некоторых ошибок всплыли интересные особенности. Серверную часть, конечно, так же можно писать и на JS, и на TS. По итогу конфиг webpack’а для сборки сервера выглядит так:

const config = {
  entry: {
    server: path.resolve(__dirname, '../src/server/index.js'),
  },
  target: 'node',
  externals: fs.readdirSync(path.resolve(__dirname, '../node_modules'))
    .reduce((acc, mod) => {
      if (mod === '.bin') {
        return acc;
      }

      acc[mod] = 'commonjs ' + mod;
      return acc;
    }, {}),
  node: {
    console: false,
    global: true,
    process: true,
    Buffer: false,
    __filename: false,
    __dirname: false,
  },
  output: {
    path: path.resolve(__dirname, '../build/server'),
    filename: '[name].js',
    publicPath: '/',
  },
  resolve: {
    extensions: [
      '.ts',
      '.js',
      '.json',
    ],
  },
  module: {
    loaders: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loader: 'awesome-typescript-loader',
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.json$/,
        loader: 'json',
      },
    ],
  },
};

CI/CD одна из тех вещей, которую я делал не потому что надо (в отличие от webpack-конфига), а потому что интересно было всё это настроить. В предыдущем посте я рассказал, что деплой на Heroku — просто. Не совсем так.

Я хотел, чтобы билд производился на GitLab CI, а уже собранное приложение заливалось на Heroku.
Первая проблема: dpl заливал “чистую” версию приложения, без билда, без ещё чего-то. Я долго не мог понять, почему так, нормальной документации у dpl нет и не было (либо я не нашел). Но, где-то в обсуждениях GitLab CI я нашел решение: параметр —skip-cleanup. Теперь “скрипт”, производящий деплой выглядит так:

- dpl --provider=heroku --app=el-dev --api-key=$HEROKU_API_KEY --skip-cleanup

Для stage’а “Deploy” в качестве зависимости указан stage “Build”:

dependencies:
  - "Build"

Stage “Build” производит сборку и сохраняет результат на 3 часа (не знаю, почему именно на 3, но просто сделал так):

"Build":
  image: node:8.9.2
  stage: Checks
  dependencies:
    - "Install"
  script:
    - npm run build
  artifacts:
    expire_in: 3 hrs
    paths:
      - assets
      - build
      - public

С деплоем собранного приложения разобрались, но так же на Heroku улетало множество конфигов, исходников и подобных бесполезных для production’а файлов. Нужно было удалять всё лишнее. В *nix довольно несложно решить эту проблему, однако большая часть способов построена на использовании глобальных параметров системы или изменении. Использовать эти способы CI не позволяет, поэтому решением стал небольшой sh-скрипт, удаляющий всё лишнее:

#!/bin/bash

find ./ \
     -type f \
     -maxdepth 1 \
     -mindepth 1 \
     -name "*" \
     ! -name "Procfile" \
     ! -name "app.json" \
     ! -name "package.json" \
     ! -name "package-lock.json" \
     -exec rm -r {} \;

find ./ \
     -type d \
     -maxdepth 1 \
     -mindepth 1 \
     -name "*" \
     ! -name "public" \
     ! -name "assets" \
     ! -name "build" \
     ! -name ".git" \
     -exec rm -r {} \;

После удаления можно заливать только необходимые файлы на Heroku.
Кстати, Heroku передаёт вообще все переменные проекта через process.env.VARIABLE, например, параметры MongoDB.

Схема CI в Gitlab

Схема CI в Gitlab

Теперь можно начать писать чисто код, изредка добавляя loader’ы в конфиг клиентского webpack и не о чем другом не думать. Особенно хорошо, что в TypeScript я могу погружаться совсем понемногу и использовать его только там, где нужно, хотя, вероятно, для подобного использования стоило бы просто втянуть Flow, но уже поздно (да и мне как-то лень)…

Ну, и, конечно, можно прикрутить Docker, чтобы не думать об окружении, но это уже в будущем, если вообще понадобится.