En bedre autowatch for Grunt/Gulp

I Visual Studio 2015 har man direkte støtte for task runners som Grunt og Gulp. Disse blir brukt til å gjøre oppgaver som byggetrinn mens man utvikler og gjerne også når man gjør deployment. En av de tingene som har vært veldig populært er det å benytte en mekanisme som gjør de nødvendige byggetrinnene når en fil endrer seg, slik at man ender opp med å ikke måtte kjøre bygging når man endrer ting – det bare skjer.

Typiske ting man benytter dette byggetrinn for er alt fra kompilering av LESS/SASS filer over til CSS, til ting som CoffeeScript, TypeScript eller BableJS – hvor man skriver noe annet enn ECMAScript 5 og får dette kompilert. Ved deployment til produksjon gjør man gjerne også minifisering av filer og andre ting som passer best ved produksjonsetting. Det har også vært populært å benytte dette for kjøring av tester man har skrevet automatisk også.

Tilbake til autowatch. Det finnes en hel del løsninger der ute for å nettopp respondere på endringer og kjøre oppgaver når dette skjer. Min erfaring er at ingen er helt 100%. Noen får ikke med seg endringene alltid, noen får ikke med seg alle type endringer – som f.eks. nye filer eller sletting av filer. I tillegg til dette ser jeg at ofte knyttes ting opp mot oppgavene / byggtrinnene på en måte som gjør at når en fil endres, kjører den alle filene. Dette er vel og bra når man produksjonsetter noe, men når man sitter og utvikler, så ønsker man at tiden fra man lagrer til at det har skjedd noe er minimal – fortrinnsvis ned på millisekunder. Etter langt om lenge endte jeg opp med å lage min egen variant. Den er kanskje ikke 100% elegant i forhold til enkel gjenbruk, men den gjør jobben effektivt.

Grunt

Jeg har ingen planer om å kaste meg inn i noen debatt om hvilken som er best, men jeg har benyttet Grunt mest og det er det jeg bruker i eksempelet her. For alt jeg vet, kan det hende at problemet jeg har møtt på ikke finnes i Gulp.

Personlig har jeg blitt veldig glad i å skrive ECMAScript 6 og litt av det som er foreslått for 7, og i den forbindelse har jeg falt ned på BabelJS som kompilator. Det finnes andre der ute, men jeg likte denne ved første øyekast og har brukt den siden.

WWWROOT

Jeg benytter ASP.NET 5 og får en folder  som representerer det ferdig kompilerte, Denne heter WWWROOT og ligger under prosjektet man lager. Hvis man benyter ulike andre pakke systemer som f.eks. JSPM og/eller BOWER ønsker man i tillegg gjerne at disse kopieres til WWWROOT. Det finnes ulike strategier for dette, noen velger å la pakke katalogene være rett under WWWROOT, mens jeg personlig liker at WWWROOT ser ut som en fornuftig struktur uten rester av hva slags type pakke manager som er benyttet. Derfor velger jeg også ha et steg som gjør jobben med å kopiere endringer fra disse for meg også. Disse endrer seg jo kun ved nye versjoner og man oppgraderer, men jeg følger allikevel samme strategien da den ikke koster noe ekstra.

Komme i gang

Vi trenger et par ting for å komme i gang. Først og fremst trenger vi noe som heter chokidar, som er den auto watch komponenten jeg har funnet som den jeg kunne stole mest på og ikke minst på kryss av plattformer – hvis man jobber som meg med Windows og OSX frem og tilbake. (PS: Du er nødt til å ha NodeJS installert for å kunne jobbe med dette).

Åpne opp terminal/console/PowerShell og naviger deg til prosjektet ditt og skriv inn:

npm install chokidar

Deretter trenger vi I dette tilfellet BabelJS installert også:

npm install babel

Vi har da de avhengighetene vi trenger for å komme i gang.

Gruntfile.js

Hvis du ikke allerede har en Gruntfile, legg denne til ved å høyreklikke på prosjektet og velge Add –> New Item:

image

Scroll ned til du ser Grunt Configuration File:

image

Du vil da få en fil som ser ut som følger:

Code Snippet

  1. /*
  2. This file in the main entry point for defining grunt tasks and using grunt plugins.
  3. Click here to learn more. go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409
  4. */
  5. module.exports = function (grunt) {
  6.     grunt.initConfig({
  7.     });
  8. };

Denne kan vi da begynne å leke oss litt med. La oss definer opp hva som er våre kildekodefiler for JavaScript som vi ønsker å transformere med BabelJS, legg til følgende i første function i toppen av denne:

Code Snippet

  1. var sourceFiles = [
  2.     '**/*.js',
  3.     '!wwwroot/**/*.js',
  4.     '!node_modules/**/*.js',
  5.     '!bower_components/**/*.js'
  6. ];

Det første elementet i arrayen sier at vi skal inkludere alle JavaScript filer. Deretter sier vi at vi ønsker ikke å ha med det som allerede er kompilert i wwwroot og heller ikke fra node eller bower.

Deretter trenger vi en definisjon på filene vi ønsker å kopiere, som ikke går gjennom Babel – bare blir kopiert:

Code Snippet

  1. var filesToCopy = [
  2.     'jspm_packages/**/*.*',
  3.     'bower_components/**/*.*'
  4. ];

Så under grunt.initConfig linjene kan vi legge til det som henter inn avhengighetene som vi la til med NPM.

Code Snippet

  1. var chokidar = require('chokidar');
  2. var babel = require('babel');
  3. var babelOptions = {
  4.     sourceRoot: './',
  5.     sourceMaps: 'inline',
  6.     optional: [
  7.         "es7.decorators"
  8.     ]
  9. };

Da har vi tilgjengelig både chokidar og babel. Vi har også satt opp noen options som vi skal benytte ved kompileringen. Legg merke til at vi legger sourceMaps inline – dette er en opsjon som ikke man ønsker ved release bygg. I tillegg ønsker jeg å benytte meg av ECMAScript 7 Decorators, som tilsvarer omtrent attributes i C#.

Så er det selve Grunt tasken som gjør magien:

Code Snippet

  1. grunt.registerTask("watch", "", function () {
  2.     var done = this.async();
  3.     grunt.log.writeln("Starting watch...");
  4.     var watcher = chokidar.watch('.', {
  5.         persistent: true,
  6.         ignored: 'wwwroot/**/*',
  7.         ignoreInitial: true
  8.     });
  9.     grunt.log.writeln("Watching");
  10.  
  11.     function handleSourceFile(path) {
  12.         grunt.log.writeln("Handling : " + path);
  13.         var fileContent = grunt.file.read(path);
  14.         try {
  15.             babelOptions.sourceFileName = path;
  16.             var transformed = babel.transform(fileContent, babelOptions);
  17.             grunt.file.write('wwwroot/' + path, transformed.code);
  18.             grunt.log.writeln("Handled : " + path);
  19.         } catch (ex) {
  20.             grunt.log.writeln("Error : " + ex);
  21.         }
  22.     }
  23.  
  24.     function handleCopyingFile(path) {
  25.         grunt.log.writeln("Copy : " + path);
  26.         grunt.file.copy(path, 'wwwroot/' + path);
  27.         grunt.log.writeln("Copied : " + path);
  28.     }
  29.  
  30.     function newOrChanged(path) {
  31.         if (grunt.file.isMatch(sourceFiles, path)) handleSourceFile(path);
  32.         if (grunt.file.isMatch(filesToCopy, path)) handleCopyingFile(path);
  33.     }
  34.  
  35.     function deleted(path) {
  36.         grunt.log.writeln("Delete : " + path);
  37.         grunt.file.delete('wwwroot/' + path);
  38.         grunt.log.writeln("Deleted : " + path);
  39.     }
  40.  
  41.     watcher
  42.         .on("add", newOrChanged)
  43.         .on("change", newOrChanged)
  44.         .on("unlink", deleted);
  45. });

Denne bolken med kode registrerer en task i Grunt som heter watch med en function som representerer den. I tasken benyttes chokidar for å watche hele folderen til prosjektet, men ignorerer wwwroot. Funksjonen som heter handleSourceFile() er den som håndterer kompileringen. Denne benytter babel sin transform funksjon til å ta inn fil innholdet fra filen som endret seg med opsjonene som vi satte opp for babel. Resultatet blir skrevet til samme path som den kom fra, bare med wwwroot som prefix. Resten skal vel være noenlunde selv forklarende.

Det siste vi må gjøre er å legge til en registrering av tasken:

Code Snippet

  1. grunt.registerTask('monitor', ['watch']);

Hele filen ser da ut som følger:

Code Snippet

  1. /*
  2. This file in the main entry point for defining grunt tasks and using grunt plugins.
  3. Click here to learn more. go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409
  4. */
  5. module.exports = function (grunt) {
  6.     var sourceFiles = [
  7.         '**/*.js',
  8.         '!wwwroot/**/*.js',
  9.         '!node_modules/**/*.js',
  10.         '!bower_components/**/*.js'
  11.     ];
  12.  
  13.     var filesToCopy = [
  14.         'jspm_packages/**/*.*',
  15.         'bower_components/**/*.*'
  16.     ];
  17.  
  18.     grunt.initConfig({
  19.     });
  20.  
  21.  
  22.     var chokidar = require('chokidar');
  23.     var babel = require('babel');
  24.     var babelOptions = {
  25.         sourceRoot: './',
  26.         sourceMaps: 'inline',
  27.         optional: [
  28.             "es7.decorators"
  29.         ]
  30.     };
  31.  
  32.     grunt.registerTask("watch", "", function () {
  33.         var done = this.async();
  34.         grunt.log.writeln("Starting watch...");
  35.         var watcher = chokidar.watch('.', {
  36.             persistent: true,
  37.             ignored: 'wwwroot/**/*',
  38.             ignoreInitial: true
  39.         });
  40.         grunt.log.writeln("Watching");
  41.  
  42.         function handleSourceFile(path) {
  43.             grunt.log.writeln("Handling : " + path);
  44.             var fileContent = grunt.file.read(path);
  45.             try {
  46.                 babelOptions.sourceFileName = path;
  47.                 var transformed = babel.transform(fileContent, babelOptions);
  48.                 grunt.file.write('wwwroot/' + path, transformed.code);
  49.                 grunt.log.writeln("Handled : " + path);
  50.             } catch (ex) {
  51.                 grunt.log.writeln("Error : " + ex);
  52.             }
  53.         }
  54.  
  55.         function handleCopyingFile(path) {
  56.             grunt.log.writeln("Copy : " + path);
  57.             grunt.file.copy(path, 'wwwroot/' + path);
  58.             grunt.log.writeln("Copied : " + path);
  59.         }
  60.  
  61.         function newOrChanged(path) {
  62.             if (grunt.file.isMatch(sourceFiles, path)) handleSourceFile(path);
  63.             if (grunt.file.isMatch(filesToCopy, path)) handleCopyingFile(path);
  64.         }
  65.  
  66.         function deleted(path) {
  67.             grunt.log.writeln("Delete : " + path);
  68.             grunt.file.delete('wwwroot/' + path);
  69.             grunt.log.writeln("Deleted : " + path);
  70.         }
  71.  
  72.         watcher
  73.             .on("add", newOrChanged)
  74.             .on("change", newOrChanged)
  75.             .on("unlink", deleted);
  76.     });
  77.  
  78.     grunt.registerTask('monitor', ['watch']);
  79. };

 

Nå trenger vi å starte den. Finn Task Runner Exploreren:

image

I exploreren vil du nå se taskene som er registrert og monitor er den vi ønsker å se nærmere på. Man kan dobbeltklikke på denne for å starte den, men man kan også sette opp slik at den starter automatisk når Visual Studio starter, som kan være veldig praktisk.

image

Lag en JavaScript fil i prosjektet, kall den f.eks. Sample.js – eller hva som helst. Lim inn følgende kode:

Code Snippet

  1. class baseClass
  2. {
  3.  
  4. }
  5.  
  6. class subClass extends baseClass
  7. {
  8.  
  9. }

Når tasken kjører, og du da lagrer denne filen, så vil du se følgende:

image

Og du skal ha fått en ny fil under wwwroot:

image

Denne ser veldig veldig annerledes ut enn den koden du skrev.

Code Snippet

  1. "use strict";
  2.  
  3. var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
  4.  
  5. function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
  6.  
  7. function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
  8.  
  9. var baseClass = function baseClass() {
  10.   _classCallCheck(this, baseClass);
  11. };
  12.  
  13. var subClass = (function (_baseClass) {
  14.   _inherits(subClass, _baseClass);
  15.  
  16.   function subClass() {
  17.     _classCallCheck(this, subClass);
  18.  
  19.     _get(Object.getPrototypeOf(subClass.prototype), "constructor", this).apply(this, arguments);
  20.   }
  21.  
  22.   return subClass;
  23. })(baseClass);
  24. //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIlNhbXBsZS5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7OztJQUNNLFNBQVMsWUFBVCxTQUFTO3dCQUFULFNBQVM7OztJQUtULFFBQVE7WUFBUixRQUFROztXQUFSLFFBQVE7MEJBQVIsUUFBUTs7K0JBQVIsUUFBUTs7O1NBQVIsUUFBUTtHQUFTLFNBQVMiLCJmaWxlIjoidW5rbm93biIsInNvdXJjZVJvb3QiOiIuLyIsInNvdXJjZXNDb250ZW50IjpbIlxyXG5jbGFzcyBiYXNlQ2xhc3Ncclxue1xyXG5cclxufVxyXG5cclxuY2xhc3Mgc3ViQ2xhc3MgZXh0ZW5kcyBiYXNlQ2xhc3Ncclxue1xyXG5cclxufSJdfQ==

 

Men siden, som du kan se på siste linjen, så har vi en map tilbake til original koden som gjør at nettleseren skjønner hva som er din egentlig kode – så vil dette være smertefritt for deg som utvikler.