Dave Paquette

Caffeine Driven Development

Stop writing vendor prefixes

April 20, 2015

I hate writing vendor prefixes in my CSS. I am constantly having to look up what vendor prefixes are needed for different CSS properties. The list is constantly changing as browsers settle on official standards. A great example is border-radius. In the past, you needed to use the following vendor prefixes:

-moz-border-radius: 2px 2px 2px 2px;
-webkit-border-radius: 2px 2px 2px 2px;
border-radius: 2px 2px 2px 2px;
-moz-border-radius: 2px 2px 2px 2px;
-webkit-border-radius: 2px 2px 2px 2px;
border-radius: 2px 2px 2px 2px;

 

But a while ago, browser vendors settled on a standard for border-radius and now the vendor prefixes are no longer needed.

border-radius: 2px 2px 2px 2px;
border-radius: 2px 2px 2px 2px;

 

Keeping up with the changes involves either following the W3C CSS specs very closely and/or constantly checking http://caniuse.com/.

Autoprefixer

It turns out that there is a great tool called Autoprefixer that can add vendor prefixes for you automatically. Autoprefixer can parse your CSS and automatically add vendor prefixes based on information from caniuse. You can even specify exactly what browsers you would like to target so autoprefixer doesn’t need to add vendor prefixes that your application does not need.

Not only does autoprefixer add missing vendor prefixes, it is also smart enough to remove vendor prefixes that are no longer needed. For example, –moz-border-radius and –webkit-border-radius would be removed for you.

Gulp Autoprefixer

The easiest way to use autoprefixer is by using a task runner like Gulp or Grunt. Building on my previous examples of using Gulp and Bower with Visual Studio, we can simply install the gulp-autoprefixer npm package:

npm install gulp-autoprefixer –save-dev

Here is a very simple gulp task that will process a CSS file and output a minified and non-minified version of that CSS file with the vendor prefixes added.

 

var gulp = require('gulp');
var concat = require('gulp-concat');
var minifyCSS = require('gulp-minify-css');
 
var config = {
    appcss: 'Content/Site.css',
    cssout: 'Content/dist/css'
}
 
 
gulp.task('css', function () {
    return gulp.src([config.appcss])
     .pipe(autoprefixer())
     .pipe(gulp.dest(config.cssout))
     .pipe(minifyCSS())
     .pipe(concat('app.min.css'))
     .pipe(gulp.dest(config.cssout));
});
var gulp = require('gulp');
var concat = require('gulp-concat');
var minifyCSS = require('gulp-minify-css');

var config = {
    appcss: 'Content/Site.css',
    cssout: 'Content/dist/css'
}


gulp.task('css', function () {
    return gulp.src([config.appcss])
     .pipe(autoprefixer())
     .pipe(gulp.dest(config.cssout))
     .pipe(minifyCSS())
     .pipe(concat('app.min.css'))
     .pipe(gulp.dest(config.cssout));
});

When I run this task, the CSS file Content/Site.css is processed by autoprefixer and then output to Content/dist/css/app.css. A minified version is also output to the same folder.

image

In my _Layouts.cshtml, I link to the generated Content/dist/css/app.min.css file.

<link href="~/Content/dist/css/app.min.css" rel="stylesheet" />
<link href="~/Content/dist/css/app.min.css" rel="stylesheet" />

 

Whenever the CSS gulp task runs, the output files will be regenerated. For example, the following CSS from Site.css:

.fancy-flex-section {
    display: flex;
}
 
.fancy-rounded {
    -moz-border-radius: 2px 2px 2px 2px;
    -webkit-border-radius: 2px 2px 2px 2px;
    border-radius: 2px 2px 2px 2px;
}
.fancy-flex-section {
    display: flex;
}

.fancy-rounded {
    -moz-border-radius: 2px 2px 2px 2px;
    -webkit-border-radius: 2px 2px 2px 2px;
    border-radius: 2px 2px 2px 2px;
}

 

will be processed by autoprefixer and output as:

.fancy-flex-section {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}
 
.fancy-rounded {
    border-radius: 2px 2px 2px 2px;
}
.fancy-flex-section {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}

.fancy-rounded {
    border-radius: 2px 2px 2px 2px;
}

 

Specify Target Browsers

Specifying the browsers you want to target is simply a matter of passing a parameter to autoprefixer. Under the covers, autoprefixer uses Browserlist, which gets browser usage data from caniuse. A common approach is to support the last 2 versions of all major browsers.

gulp.task('css', function () {
    return gulp.src([config.appcss])
     .pipe(concat('app.css'))
     .pipe(autoprefixer({ browsers: ['last 2 version'] }))
     .pipe(gulp.dest(config.cssout))
     .pipe(minifyCSS())
     .pipe(concat('app.min.css'))
     .pipe(gulp.dest(config.cssout));
});
gulp.task('css', function () {
    return gulp.src([config.appcss])
     .pipe(concat('app.css'))
     .pipe(autoprefixer({ browsers: ['last 2 version'] }))
     .pipe(gulp.dest(config.cssout))
     .pipe(minifyCSS())
     .pipe(concat('app.min.css'))
     .pipe(gulp.dest(config.cssout));
});

Another approach is to support any browser that has more than x% market share:

gulp.task('css', function () {
    return gulp.src([config.appcss])
     .pipe(concat('app.css'))
     .pipe(autoprefixer({ browsers: ['> 5%'] }))
     .pipe(gulp.dest(config.cssout))
     .pipe(minifyCSS())
     .pipe(concat('app.min.css'))
     .pipe(gulp.dest(config.cssout));
});
gulp.task('css', function () {
    return gulp.src([config.appcss])
     .pipe(concat('app.css'))
     .pipe(autoprefixer({ browsers: ['> 5%'] }))
     .pipe(gulp.dest(config.cssout))
     .pipe(minifyCSS())
     .pipe(concat('app.min.css'))
     .pipe(gulp.dest(config.cssout));
});

The list of options for targeting browsers is impressive. You can even target browsers based on usage in a specific country. For a complete list, go check out Browserlist.

Conclusion

With autoprefixer, you no longer need to worry about writing vendor prefixes in your CSS. Simply write your CSS in un-prefixed format and let autoprefixer do the rest for you.

Integrating Gulp and Bower with Visual Studio Online Hosted Builds

April 8, 2015

In previous posts, I talked about using Gulp to manage client side builds and using Bower as a package manager for client side packages.

Shane Courtrille asked me if I could do a follow up post showing how to integrate these concepts with a build hosted on Visual Studio Online. It took me some time and a long list of failures to get this working, but here it is.

Build Server Requirements

In order to successfully use Gulp and Bower on your build server, you will need both Node and Git installed. Luckily, the hosted Visual Studio Online build controllers already have these tools installed. If you are hosting your own build server, you will need to install node.js and Git on the server. When installing these tools, make sure you login to the server as the same user that executes the build process.

Adding a PreBuild Script to Visual Studio Build

The next step is to ensure that we call the default gulp task before attempting to compile our application with msbuild. Using the gulp file from the blog post on using Bower, calling the default gulp task will ensure that all bower packages are installed and all our JavaScript and CSS bundles are created and copied to the expected locations.

Gulp requires a number of node packages to be installed. Before we can call gulp, we need to call npm install to ensure that those packages have been downloaded locally on the build controller.

The easiest way to do this is to create a simple PowerShell script that is executed as a Pre Build step for our Visual Studio build. Pre Build Steps are only available if GitTemplate.12.xaml as your build process template.

image

I added a PreBuild.ps1 script to the root folder for my solution and added that file as my Pre-build script path.

Push-Location "BowerInVS2013"
& npm install
& gulp default
Pop-Location
Push-Location "BowerInVS2013"
& npm install
& gulp default
Pop-Location

 

In theory, that’s all we needed to do. Unfortunately, theory and practice are rarely align…especially when working with Visual Studio builds.

When I tried to run this build in Visual Studio Online, I got the following error:

Error: ENOENT, stat ‘C:\Users\buildguest\AppData\Roaming\npm’

For some reason, the buildguests user that runs the build on Visual Studio online has trouble with the call to npm install. A quick workaround for this is to create the missing npm folder if it does not exist.

$appdatafolder = [Environment]::GetFolderPath('ApplicationData')
$npmfolder = $appdatafolder + "\npm"
if(!(Test-Path -Path $npmfolder)){
    New-Item -Path $npmfolder -ItemType Directory 
}
Push-Location "BowerInVS2013"
& npm install
& gulp default
Pop-Location
$appdatafolder = [Environment]::GetFolderPath('ApplicationData')
$npmfolder = $appdatafolder + "\npm"
if(!(Test-Path -Path $npmfolder)){
    New-Item -Path $npmfolder -ItemType Directory 
}
Push-Location "BowerInVS2013"
& npm install
& gulp default
Pop-Location

 

This brings us to my next failure: Gulp is not installed globally on the hosted build controller so the call to gulp default failed.

To solve this I added added a gulp script to my npm package file (package.json). This gives me a convenient way to call gulp via npm.

{
  "name": "BowerInVS2013",
  "version": "1.0.0",
  "private": true,
  "devDependencies": {
    "del": "^1.1.1",
    "gulp": "^3.8.10",
    "gulp-bower": "0.0.7",
    "gulp-concat": "^2.4.2",
    "gulp-copy": "0.0.2",
    "gulp-minify-css": "^0.3.11",
    "gulp-sourcemaps": "^1.3.0",
    "gulp-uglify": "^1.0.2"
  },
  "scripts": {
    "gulp": "./node_modules/.bin/gulp"
  }
}
{
  "name": "BowerInVS2013",
  "version": "1.0.0",
  "private": true,
  "devDependencies": {
    "del": "^1.1.1",
    "gulp": "^3.8.10",
    "gulp-bower": "0.0.7",
    "gulp-concat": "^2.4.2",
    "gulp-copy": "0.0.2",
    "gulp-minify-css": "^0.3.11",
    "gulp-sourcemaps": "^1.3.0",
    "gulp-uglify": "^1.0.2"
  },
  "scripts": {
    "gulp": "./node_modules/.bin/gulp"
  }
}

The updated PreBuild.ps1 script looks like this:

$appdatafolder = [Environment]::GetFolderPath('ApplicationData')
$npmfolder = $appdatafolder + "\npm"
if(!(Test-Path -Path $npmfolder)){
    New-Item -Path $npmfolder -ItemType Directory 
}
Push-Location "BowerInVS2013"
& npm install
& npm gulp
Pop-Location
$appdatafolder = [Environment]::GetFolderPath('ApplicationData')
$npmfolder = $appdatafolder + "\npm"
if(!(Test-Path -Path $npmfolder)){
    New-Item -Path $npmfolder -ItemType Directory 
}
Push-Location "BowerInVS2013"
& npm install
& npm gulp
Pop-Location

 

We still get one warning from npm and I’m not sure how to fix it.

npm WARN deprecated deflate-crc32-stream@0.1.2: module has been merged into crc32-stream

This warning seems to be a known issue with node on Windows and in this case it seems to be safe to ignore it. The compiled output from the build contains everything that we expect.

Conclusion

With a few minor workarounds, we are able to get Bower and Gulp working with the Visual Studio Online hosted build controllers.