How to create multiple themed release builds by using web deploy, gulp and PostCSS in Visual Studio 2015 update 3

 
 

Note

In this blogpost a “release build” is a folder containing all the files (html, css, JavaScript, images, dll’s etc.) needed in production to run a specific themed web application.

 
 

 
 

 
 

The publish / deployment process in Visual Studio is intended to create one “release build”, but I wanted this process to create 2 exactly the same “release builds”, just with different themes (different images and css files).

 
 

To accomplish this, I altered / added / removed the following files:

  • Added deploy.pub.xml (the publish profile)
  • Altered the csproj file of the web project
    • Added folder to the root “Themes”
      • Theme1
        • Images (contains the images for this theme, build type = “None”)
        • variables.cssn (contains all the css next variables for this theme, build type = “None”)
      • Theme2
        • Images (contains the images for this theme, build type = “None”)
        • variables.cssn (contains all the css next variables for this theme, build type = “None”)
  • Added package.json (npm configuration file, build type = “None”)
  • gulpfile.js (gulp configuration file, build type = “None”)
  • gulpfile.helper.js (custom JavaScript code, build type = “None”)
  • Renamed all *.css to *.cssn (css next) and set the build type = “None” (except the css files in the libraries folder)
  • Deleted all *.css files except the files in the libraries folder.
  • .gitignore

 
 

 
 

 
 

Deploy.pubxml

 
 

Create a publishing file “deploy.pubxml” by right clicking on your web application project > publish… > Custom

  • Enter “deploy” for the name, his will create a deploy.pubxml.
  • Choose publish method “File System”
  • Enter a target location, for example C:\Release\MyWebApp

 
 

 
 

 
 

<?xml
version=1.0
encoding=utf-8?>

<!–

This file is used by the publish/package process of your Web project. You can customize the behavior of this process

by editing this MSBuild file. In order to learn more about this please visit http://go.microsoft.com/fwlink/?LinkID=208121.

–>

<Project
ToolsVersion=4.0
xmlns=http://schemas.microsoft.com/developer/msbuild/2003>

<PropertyGroup>

<WebPublishMethod>FileSystem</WebPublishMethod>

<SiteUrlToLaunchAfterPublish />

<publishUrl>C:\Release\Theme1</publishUrl>

<DeleteExistingFiles>False</DeleteExistingFiles>

<LastUsedBuildConfiguration />

<LastUsedPlatform />

<LaunchSiteAfterPublish>False</LaunchSiteAfterPublish>

<ExcludeApp_Data>False</ExcludeApp_Data>

</PropertyGroup>

<!–

The target “GatherAllFilesToPublish” is the last target run by msbuild, before msdeploy starts copying files to the destination folder.

This is the last time we can alter the output, because there is no “after publish” target.

We have to use the property “AfterTargets”, because “DependsOnTargets” doesn’t work.

 

The target runs a gulp task which creates a release output folder per theme (by using PostCSS and copying images to correct location).

– themes: Is a “comma” seperated list of themed releases that should be created

– The first item should be the “default” theme

– projectFolder: The folder containing the *.csproj file.

This folder is needed, because among other things it contains:

– the *.cssn (css next) files that should be compiled to *.css

– the theme images that should be copied

– packageTempDir: The temp folder used by Visual Studio to create a release. This folder will be used as base for the other themed releases.

– publishFolder: Is set to the “publishUrl”,

– The publishing process of Visual Studio is intended to work for one “release build”.

– We are creating multiple “release builds”, one for each theme.

– Each “release build” will be placed inside the parent folder of the “publishFolder”

– Only the first theme (default theme) will be placed directly into the “publishFolder”

 

NOTE

– npm run “gulp”, will only work when the package.json contains scripts { “gulp”: “gulp” }.


–>

<Target
Name=AfterPublish
AfterTargets=GatherAllFilesToPublish>

<Exec
Command=npm run gulp — after-publish –options –themes Theme1,Theme2 –projectFolder $(MSBuildProjectDirectory) –packageFolder $(_PackageTempDir) –publishFolder $(publishUrl) />

</Target>

</Project> 

 
 

 
 

Alter csproj

Add a “before build” step (just before the “project” end tag), to apply theming whenever the project is build (even in development)

<Target
Name=BeforeBuild>

<Exec
Command=npm run gulp — apply-theming />

</Target>

</Project>

 
 

This will only run the theming and it will run the theming inside the current project folder.

 
 

 
 

 
 

Add or update package.json

After saving this file, Visual Studio will automatically run gulp install, to install all npm packages.

 
 

{

    “version”: “1.0.0”,

    “name”: “MyWebApp”,

    “private”: true,

“devDependencies”: {

“del”: “>=2.2.1”,

“gulp”: “>=3.9.1”,

“gulp-plumber”: “>=1.1.0”,

“gulp-postcss”: “>=6.1.1”,

“gulp-rename”: “>=1.2.2”,

“gulp-util”: “>=3.0.7”,

“postcss-import”: “>=8.1.2”,

“postcss-cssnext”: “>=2.7.0”

 
 

},

“scripts”: {

“gulp”: “gulp”

}

}

 
 

 
 

 
 

 
 

Add a gulpfile.js to the root of your web application

 
 

/**

* The version of gulp and all it’s dependencies are managed in the package.json file.

*/

‘use strict’;

 

// Dependencies

const fs = require(‘fs’);

const gulp = require(‘gulp’);

const debug = require(‘gulp-debug’); // Can be used to log all files in a gulp stream (gulp.src or gulp.dest).

const jshint = require(‘gulp-jshint’);

const livereload = require(‘gulp-livereload’);

const plumber = require(‘gulp-plumber’);

const util = require(‘gulp-util’);

const path = require(‘path’);

const helpers = require(‘./gulpfile-helpers.js’);

const log = helpers.log;

const defaultTheme = ‘Theme1’;

 

/**

* The default task.

*/

gulp.task(‘default’, function () {

 

});

 

/**

* This task will be called from the “Deploy.pubxml”.

*/

gulp.task(“after-publish”, function () {


const themes = (util.env.themes || ‘Theme1,Theme2’).trim().split(‘,’);


const projectFolder = (util.env.projectFolder || path.resolve(‘.’)).trim();


const packageFolder = path.resolve((util.env.packageFolder || “..\\bin\\Release\\Package\\PackageTmp”).trim());

 


// When this task is passed an environment variable “publishFolder”, we expect the “publishFolder” to be the path to the first “themed release build”.


// See the Deploy.pubxml for more explanation.


var publishFolder = “C:\\Release”;


if(util.env.publishFolder) {

publishFolder = path.dirname(util.env.publishFolder.trim());

}

 

log(`after-publish: themes – ${themes}`);

log(`after-publish: projectFolder – ${projectFolder}`);

log(`after-publish: packageFolder – ${packageFolder}`);

log(`after-publish: publishFolder – ${publishFolder}`);

 


// Publish each theme.

themes.map((theme) => {

helpers.publishTheme(theme.trim(), defaultTheme, projectFolder, packageFolder, publishFolder);

});

});

 

/**

* The “BeforeBuild” target in the ” csproj” will call this gulp task.

*

* If you want to generate a theme other than the default theme, use the command line:

* npm run gulp — apply-theming –options –theme Theme1

*/

gulp.task(‘apply-theming’, function () {


const theme = (util.env.theme || defaultTheme);

helpers.applyTheme(theme, path.resolve(‘.’), path.resolve(‘.’));

});

 
 

 
 

 
 

 
 

Add a gulpfile.helper.js to the root of your web application

 
 

const del = require(‘del’);

const gulp = require(‘gulp’);

const path = require(‘path’);

const postcss = require(‘gulp-postcss’);

const postcssImport = require(‘postcss-import’);

const postcssNext = require(‘postcss-cssnext’);

const rename = require(‘gulp-rename’);

const util = require(‘gulp-util’);

 

/**

* Copy images and variables.cssn (cssnext) from the “theme” folder to the correct location.

* projectFolder = the folder containing the “*.csproj” file.

* destinationFolder = the folder where the theme files will be placed.

*/

function applyTheme(theme, projectFolder, publishFolder) {

 


// Copy image files to publish folder.

copyImages(theme, projectFolder, publishFolder)

.on(‘finish’, runCopyCssNextThemeFiles);

 


// Copy css next theme files inside publish folder to the “App/Styles” folder.


function runCopyCssNextThemeFiles() {

copyCssNextThemeFiles(theme, publishFolder)

.on(‘finish’, runGenerateCss);

}

 


// Generate css from css next files.


function runGenerateCss() {

generateCss(publishFolder);

}

}

 

/*

* Copy all css next files from the project folder to the specific theme publish folder.

*/

function copyCssNextFiles(projectFolder, publishThemeFolder) {


const src = path.join(projectFolder, ‘/**/*.cssn’);


const dest = publishThemeFolder;

 

log(`copyCssNextFiles: src – ${src}`);

log(`copyCssNextFiles: dest – ${dest}`);

 


return gulp.src(src)

.pipe(gulp.dest(publishThemeFolder));

}

 

/**

* Copy theme css next files (including variables.cssn) from the “theme” folder to the “App/Styles” folder.

*/

function copyCssNextThemeFiles(theme, publishFolder) {


const src = path.join(publishFolder, ‘Themes/’ + theme + ‘/**/*.cssn’);


const dest = path.join(publishFolder, “App/Styles”);

 

log(`copyCssNextThemeFiles: src – ${src}`);

log(`copyCssNextThemeFiles: dest – ${dest}`);

 


return gulp.src(src)

.pipe(gulp.dest(dest));

}

 

function copyImages(theme, projectFolder, destinationFolder) {


const src = path.join(projectFolder, ‘Themes/’ + theme + ‘/Images/**/*.*’);


const dest = path.join(destinationFolder, ‘App/Images’);

 

log(`copyImages: src – ${src}`);

log(`copyImages: dest – ${dest}`);

 


return gulp.src(src)

.pipe(gulp.dest(dest));

}

 

/*

* Copy all files from the Visual Studio package temp folder to the specific theme publish folder.

*/

function copyPackageFiles(packageFiles, publishThemeFolder) {


const src = packageFiles;


const dest = publishThemeFolder;

 

log(`copyPackageFiles: src – ${src}`);

log(`copyPackageFiles: dest – ${dest}`);

 


return gulp.src(packageFiles)

.pipe(gulp.dest(publishThemeFolder));

}

 

/**

* Compile cssn (cssnext) to css.

*/

function generateCss(publishFolder) {


const src = path.join(publishFolder, ‘App/**/*.cssn’);


const dest = path.join(publishFolder, ‘App’);

 

log(`generateCss: src – ${src}`);

log(`generateCss: dest – ${dest}`);

 


const plugins = [

postcssImport,

postcssNext

];

 


return gulp.src(src)

.pipe(postcss(plugins))

.pipe(rename({

extname: “.css”

}))

.on(‘error’, log)

.pipe(gulp.dest(dest));

}

 

/**

* Log message or an array of messages.

*/

function log(message) {


if (Array.isArray(message)) {


for (var i = 0, length = message.length; i < length; i++) {

util.log(message[i]);

}

}


else {

util.log(message);

}

}

 

function publishTheme(theme, defaultTheme, projectFolder, packageFolder, publishFolder) {


const publishThemeFolder = path.join(publishFolder, theme);


const publishThemeFiles = path.join(publishThemeFolder, “**”);


const packageFiles = path.join(packageFolder, ‘**/*’);

 

log(`publishTheme: publishThemeFolder – ${publishThemeFolder}`);

log(`publishTheme: publishThemeFiles – ${publishThemeFiles}`);

log(`publishTheme: packageFiles – ${packageFiles}`);

 


// Remove publish folder.

del.sync([publishThemeFiles], { force: true });

 


// Copy package files to publish folder.

copyPackageFiles(packageFiles, publishThemeFolder)

.on(‘finish’, runCopyCssNextFiles);

 


// Copy css next files to publish folder.


function runCopyCssNextFiles() {

copyCssNextFiles(projectFolder, publishThemeFolder)

.on(‘finish’, runCopyImages);

}

 


function runCopyImages() {


// Copy image files to publish folder.

copyImages(theme, projectFolder, publishThemeFolder)

.on(‘finish’, runCopyCssNextThemeFiles);

}

 


// Copy css next theme files inside publish folder to the “App/Styles” folder.


function runCopyCssNextThemeFiles() {

copyCssNextThemeFiles(theme, publishThemeFolder)

.on(‘finish’, runGenerateCss);

}

 


// Generate css from css next files.


function runGenerateCss() {

generateCss(publishThemeFolder)

.on(‘finish’, runDeleteCssNextFiles);

}

 


// Delete all css next files in the publish folder.


function runDeleteCssNextFiles() {


const ccsNextFiles = path.join(publishThemeFolder, ‘**/*.cssn’);

log(`runDeleteCssNextFiles: ccsNextFiles – ${ccsNextFiles}`);

 

del.sync([ccsNextFiles], { force: true });

}

}

 

/**

* Log message to a log file, creating it, when it does not exist.

* The file will always be overwritten.

*/

function writeToLogFile(message) {

fs.writeFileSync(‘gulp.log.txt’, message);

}

 

exports.applyTheme = applyTheme;

exports.log = log;

exports.publishTheme = publishTheme;

 
 

 
 

Alter the .gitignore file

 
 

/App/Images/*.*

/App/Styles/variables.*

/App/**/*.css

!/App/Libraries/**/*.css

 
 

We excluded the copied “themed” images.

We excluded the copied “themed” variables.cssn.

We excluded all css, except the css in the libraries folder.

 
 

 
 

 
 

Now when you publish the project, the folder C:\Release\MyWebApp, should contain 2 “release builds” : Theme1 and Theme2.

Both containing the specific themed images and css.

  

Exclude file paths from jshint with gulp

If you want to jshint all JavaScript files in a folder, but want to exclude a subfolder, you can use “negation” in gulp.

 

 

var gulp = require("gulp");

var jshint = require("gulp-jshint");
var plumber = require("gulp-plumber");

 

var onError = function (err) {
        console.log(err);
    };

/**
     * Hint all of our custom developed Javascript files.
     */
    gulp.task("jshint", function () {

        return gulp.src([
            "Client/**/*.js",
           "!Client/Libraries/**/*.js"
        ])
        .pipe(plumber({
            errorHandler: onError
        }))
        .pipe(jshint())
        .pipe(jshint.reporter("default"));
    });

How to live reload a css file, without reloading the whole page, by using gulp-livereload.

 

If you want to only reload css and not the entire page, when files change on disk, you can use the following gulp file.

 

/** * When you want to use this gulpfile.js, make sure you execute the gulp.ps1 PowerShell script first, so everything is installed correctly. */ (function (require) { "use strict"; // Gulp. var gulp = require("gulp"); // Gulp plugins. var plumber = require("gulp-plumber"); var livereload = require("gulp-livereload");

// Gulp plumber error handler var onError = function (err) { console.log(err); }; /** * The following file will be reloaded, when one of the "watched" *.html or 8.js files has changed. */ gulp.task('reload', function () { gulp.src([ "App/unit.test.runner.html" ]) .on('error', onError) .pipe(gulp.dest('dist')) .pipe(livereload()); }); /** * All *.css files will be reloaded, when one of the "watched" *.css files has changed. */ gulp.task('reloadCss', function () { gulp.src([ 'App/**/*.css' ]) .on('error', onError) .pipe(gulp.dest('dist')) .pipe(livereload()); }); /** Watch *.css files, but only reload css, not the entire page. Watch *.html and *.js files, when a change is detected, reload the whole page. */ gulp.task("watch", function () { livereload.listen(); gulp.watch([ "App/**/*.css" ], ["reloadCss"]); gulp.watch([ "App/**/*.html", "App/**/*.js" ], ["reload"]); }); /** When the user enters "gulp" on the command line, the default task will automatically be called. This default task below, will run all other task automatically. So when the user enters "gulp" on the command line all task are run. */ gulp.task("default", ["watch", "reload"]); }(require));

Getting started with gulp and livereload in Visual Studio 15

If you want to automatically refresh your web page, when a file on disk changes you can use gulp and the gulp plugin: gulp-livereload.

 

Notes

  • Microsoft Visual Studio 2015 has node included out of the box, for Microsoft Visual Studio 2013, you will have to manually install node.
  • Microsoft Visual Studio 2015 has the task runner explorer included out of the box, for Microsoft Visual Studio 2013, you will have to manually install this as a Visual Studio extension.

 

Command prompt

  • Open cmd (with administrator rights)
  • Change directory to folder containing the web project, e.g. cd "C:\..\..\MySolution\MyProjectWeb"

Install gulp globally

  • npm install gulp –g

Install gulp to project

  • npm install gulp –save-dev

Install gulp plugins

  • npm install gulp-jshint –save-dev
  • npm install gulp-notify –save-dev
  • npm install gulp-plumber –save-dev
  • npm install gulp-watch –save-dev
  • npm install gulp-livereload –save-dev

Add gulpfile.js to project

  • Add a gulpfile.js to the root of the visual studio project
  • Add code to the gulpfile:

gulpfile.js contents

var gulp = require('gulp');
var jshint = require('gulp-jshint');
var plumber = require('gulp-plumber');
var livereload = require('gulp-livereload');
// Gulp plumber error handler
var onError = function (err) {
console.log(err);
};
gulp.task('reload', function () {
// Change the filepath, when you want to live reload a different page in your project.
livereload.reload("./index.html");
});
// This task should be run, when you want to reload the webpage, when files change on disk.
// This task will only watch JavaScript file changes in the folder "/Client" and it's subfolders.
gulp.task('watch', function () {
livereload.listen();
gulp.watch('./Client/**/*.js', ['reload']);
});
// Hint all of our custom developed Javascript to make sure things are clean.
// This task will only hint JavaScript files in the folder "/Client" and it's subfolders.
gulp.task('jshint', function () {
return gulp.src([
'./Client/**/*.js'
])
.pipe(plumber({
errorHandler: onError
}))
.pipe(jshint())
.pipe(jshint.reporter('default'));
});
// When the user enters "gulp" on the command line, the default task will automatically be called.
// This default task below, will run all other task automatically.
// So when the user enters "gulp" on the command line all task are run.
gulp.task('default', ['jshint']);

 

Run the gulp task “watch” with the Task Runner Explorer

This task will watch, the file system for any JavaScript file changes and call the “reload” gulp task, when this occurs.

The “reload” gulp task will tell the browser to refresh itself.

 

image

 

 

Install LiveReload chrome extension

https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei

 

Start debugging your website from Visual Studio in Chrome and enable the LiveReload chrome extension

image

 

 

Make as change

Make a change to a JavaScript file and see the changes live reflected in your browser.

How to fix: No gulp tasks found by the Task Runner Explorer in Visual Studio 2013

The Task Runner Explorer extension for Microsoft Visual Studio 2013 found the gulpfile.js in my project, but the task in that file where not listed, just beneath the gulpfile.js, (No tasks found) was displayed, this was caused by missing dependencies.

image

 

To fix this:

Just open a command prompt.

Change directory to the folder containing the file “gulpfile.js”

Then enter: npm install

All dependency for gulp should be installed, now when you op the project in Visual Studio 2013 the tasks should be shown.

 

image