gradleからnpm run scriptを実行する方法(gulpもこれでOK)


Spring Bootにおいて、src/main/resources/templatesにthymeleafを配置、src/main/resources/staticにjsやCSSなどのフロントで読み込むファイルを配置するのがデフォルトの設定となっています。

「herokuにデプロイしたWebアプリのフロントのjsとCSSが軒並み404になっている…」という現象で気づいたのですが、

厄介なことに、そのままの状態ではJavaのビルド時(jarにパッケージングするとき)に、フロント側でコンパイルが必要なものをコンパイルすることができません。

今まではこの現象の回避策として、私は「コンパイルした後のjs、cssもソースコード管理する」という手段を取ってきました。

が、そもそもコンパイルで自動生成するものをgitで管理するのはあまり筋の良い方法とは言えません。(.classファイルをソースコード管理する人はいないはずです。)

そもそも、以前は「フロントエンドのコードをコンパイル」するという発想がなかった(CSSもJavaScriptも書いたものをそのまま使う)ので、src/main/resources/staticに自分の書いたものを配置するだけで良かったわけです。

しかし、今はCSSはSassやSCSS、POSTCSSを書いてツールでコンパイルする時代です。

JavaScriptに関してはBabelなどのトランスパイラなしで語れない状況になっています。

ですから、「gradleでビルドするときに一緒にフロントのコードもビルドできないかな」という発想になります。

というわけで検索してみたところ、gradleからnodeのタスクを実行するpluginを発見しました。

gradle-node-plugin

※ mavenをお使いの場合は、frontend-maven-plugin あたりを使用してください。(これの使い方に関しては本記事では触れません。)

それでは、早速使ってみましょう。

build.gradleの修正

build.gradleに以下を追記します。


// gradle-node-pluginを使用
plugins {
    id "com.moowork.node" version "0.13"
}

//以下のケースでは、src/main/resourcesにpackage.jsonが存在する想定
node {
    workDir = file("${project.projectDir}/src/main/resources") //package.jsonがあるディレクトリの位置を指定
    nodeModulesDir = file("${project.projectDir}/src/main/resources") //node_modulesがあるディレクトリの位置を指定
}

task jsBuild(type: NpmTask) {
    args = ['run', 'build']; //package.jsonで定義するタスクを記述する
}

//この行は、「apply plugin:'java'」の後に記述する
classes.dependsOn jsBuild //classファイルを作成する直前に、jsBuildタスクを実行する

後から気づいたことですが、ルートディレクトリ直下にpackage.jsonを配置しても良いかもしれません。

package.jsonで、実行タスクのインタフェース定義

build.gradleで「npm run build」を実行するタスクを定義したので、scriptsにbuildタスクを定義してあげます。

{
  "name": "SomeFullstackProject",
  "version": "0.0.1",
  "scripts": {
    "build": "npm install & gulp build"
  },
  "devDependencies": {
    "babel-preset-es2015": "6.9.0",
    "del": "2.2.1",
    "gulp": "3.9.1",
    "gulp-angular-templatecache": "2.0.0",
    "gulp-babel": "6.1.2",
    "gulp-concat": "2.6.0",
    "gulp-ng-annotate": "2.0.0",
    "gulp-plumber": "1.1.0",
    "gulp-sass": "2.3.2",
    "gulp-uglify": "1.5.4",
    "run-sequence": "1.2.2"
  }
}

npm run build を実行すると npm installと gulp buildが実行されるように設定します。

gulpfile.jsを書く

具体的なタスクの処理内容を記述します。

var gulp = require('gulp');
var scss = require('gulp-sass');
var babel = require('gulp-babel');
var uglify = require('gulp-uglify');
var concat = require('gulp-concat');
var ngTemplateCache = require('gulp-angular-templatecache');
var plumber = require('gulp-plumber');
var del = require('del');
var runSequence = require('run-sequence');
var ngAnnotate = require('gulp-ng-annotate');

/**
 * gradleビルド時に実行されるビルドタスク
 */
gulp.task('scss', function () {
  return gulp.src('frontSrc/scss/**/*.scss')
    .pipe(plumber())
    .pipe(scss({
      outputStyle: 'compressed'
    }))
    .pipe(gulp.dest('static/css'))
});


gulp.task('js', function () {
  return gulp.src('frontSrc/js/**/*.js')
    .pipe(plumber())
    .pipe(concat('bundle.min.js'))
    .pipe(ngAnnotate())
    .pipe(babel({
      presets: ['es2015']
    }))
    .pipe(uglify())
    .pipe(gulp.dest('static/js/dist'))
});

gulp.task('lib', function () {
  return gulp.src('static/js/lib/**/*')
    .pipe(gulp.dest(targetDir + "/static/js/lib"));
});

gulp.task('template', function () {
  return gulp.src('frontSrc/templates/**/*.html')
    .pipe(ngTemplateCache({
      module: 'app'
    }))
    .pipe(gulp.dest('static/js/dist'))
});


/**
 * 各種ローカル開発用コンパイルタスク
 * いちいちbuildしていると時間がかかるので、参照しているコンパイル先の
 * ディレクトリに成果物をダイレクトに突っ込む
 */
var targetDir = '../../../bin';


gulp.task('scss:develop', function () {
  return gulp.src('frontSrc/scss/**/*.scss')
    .pipe(plumber())
    .pipe(scss({
      outputStyle: 'compressed'
    }))
    .pipe(gulp.dest(targetDir + '/static/css'));
});

gulp.task('img:develop', function () {
  return gulp.src('static/img/**/*')
    .pipe(gulp.dest(targetDir + '/static/img'));
});

gulp.task('html:develop', function () {
  return gulp.src('templates/**/*.html')
    .pipe(gulp.dest(targetDir + '/templates'));
});

gulp.task('js:develop', function () {
  return gulp.src('frontSrc/js/**/*.js')
    .pipe(plumber())
    .pipe(concat('bundle.min.js'))
    .pipe(ngAnnotate())
    .pipe(babel({
      presets: ['es2015']
    }))
    .pipe(uglify())
    .pipe(gulp.dest(targetDir + '/static/js/dist'));
});

gulp.task('lib:develop', function () {
  return gulp.src('static/js/lib/**/*')
    .pipe(gulp.dest(targetDir + "/static/js/lib"));
});

gulp.task('template:develop', function () {
  return gulp.src('frontSrc/templates/**/*.html')
    .pipe(ngTemplateCache({
      module: 'app'
    }))
    .pipe(gulp.dest(targetDir + '/static/js/dist'));

});

/**
 * ローカル開発用のwatchタスク。
 */
gulp.task('watch', function () {
  gulp.watch('frontSrc/scss/**/*.scss', ['scss:develop']);
  gulp.watch('frontSrc/templates/**/*.html', ['template:develop']);
  gulp.watch('frontSrc/js/**/*.js', ['js:develop']);
  gulp.watch('static/img/**/*', ['img:develop']);
  gulp.watch('templates/**/*.html', ['html:develop']);

});

gulp.task('build', function (callback) {
  return runSequence(
    ["scss", "template", "js", "lib"]
    , callback
  )
});


java