Диплом, часть 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
Теперь можно начать писать чисто код, изредка добавляя loader’ы в конфиг клиентского webpack и не о чем другом не думать. Особенно хорошо, что в TypeScript я могу погружаться совсем понемногу и использовать его только там, где нужно, хотя, вероятно, для подобного использования стоило бы просто втянуть Flow, но уже поздно (да и мне как-то лень)…
Ну, и, конечно, можно прикрутить Docker, чтобы не думать об окружении, но это уже в будущем, если вообще понадобится.