Skip to main content

5 posts tagged with "visual studio"

View All Tags

Debugging ASP.Net Core in VS or Code

I've been using Visual Studio for a long time. Very good it is too. However, it is heavyweight; it does far more than I need. What I really want when I'm working is a fast snappy editor, with intellisense and debugging. What I've basically described is VS Code. It rocks and has long become my go-to editor for TypeScript.

Since I'm a big C# fan as well I was delighted that editing C# was also possible in Code. What I want now is to be able to debug ASP.Net Core in Visual Studio OR VS Code. Can it be done? Let's see....

I fire up Visual Studio and File -> New Project (yes it's a verb now). Select .NET Core and then ASP.Net Core Web Application. OK. We'll go for a Web Application. Let's not bother with authentication. OK. Wait a couple of seconds and Visual Studio serves up a new project. Hit F5 and we're debugging in Visual Studio.

So far, so straightforward. What will VS Code make of this?

I cd my way to the root of my new ASP.Net Core Web Application and type the magical phrase "code .". Up it fires. I feel lucky, let's hit "F5". Huh, a dropdown shows up saying "Select Environment" and offering me the options of Chrome and Node. Neither do I want. It's about this time I remember this is a clean install of VS Code and doesn't yet have the C# extension installed. In fact, if I open a C# file it up it tells me and recommends that I install. Well that's nice. I take it up on the kind offer; install and reload.

When it comes back up I see the following entries in the "output" tab:

Updating C# dependencies...
Platform: win32, x86_64 (win7-x64)
Downloading package 'OmniSharp (.NET 4.6 / x64)' (20447 KB) .................... Done!
Downloading package '.NET Core Debugger (Windows / x64)' (39685 KB) .................... Done!
Installing package 'OmniSharp (.NET 4.6 / x64)'
Installing package '.NET Core Debugger (Windows / x64)'
Finished

Note that mention of "debugger" there? Sounds super-promising. There's also some prompts: "There are unresolved dependencies from 'WebApplication1/WebApplication1.csproj'. Please execute the restore command to continue"

So it wants me to dotnet restore. It's even offering to do that for me! Have at you; I let it.

Welcome to .NET Core!
---------------------
Learn more about .NET Core @ https://aka.ms/dotnet-docs. Use dotnet --help to see available commands or go to https://aka.ms/dotnet-cli-docs.
Telemetry
--------------
The .NET Core tools collect usage data in order to improve your experience. The data is anonymous and does not include command-line arguments. The data is collected by Microsoft and shared with the community.
You can opt out of telemetry by setting a DOTNET_CLI_TELEMETRY_OPTOUT environment variable to 1 using your favorite shell.
You can read more about .NET Core tools telemetry @ https://aka.ms/dotnet-cli-telemetry.
Configuring...
-------------------
A command is running to initially populate your local package cache, to improve restore speed and enable offline access. This command will take up to a minute to complete and will only happen once.
Decompressing Decompressing 100% 4026 ms
Expanding 100% 34814 ms
Restoring packages for c:\Source\Debugging\WebApplication1\WebApplication1\WebApplication1.csproj...
Restoring packages for c:\Source\Debugging\WebApplication1\WebApplication1\WebApplication1.csproj...
Restore completed in 734.05 ms for c:\Source\Debugging\WebApplication1\WebApplication1\WebApplication1.csproj.
Generating MSBuild file c:\Source\Debugging\WebApplication1\WebApplication1\obj\WebApplication1.csproj.nuget.g.props.
Writing lock file to disk. Path: c:\Source\Debugging\WebApplication1\WebApplication1\obj\project.assets.json
Restore completed in 1.26 sec for c:\Source\Debugging\WebApplication1\WebApplication1\WebApplication1.csproj.
NuGet Config files used:
C:\Users\johnr\AppData\Roaming\NuGet\NuGet.Config
C:\Program Files (x86)\NuGet\Config\Microsoft.VisualStudio.Offline.config
Feeds used:
https://api.nuget.org/v3/index.json
C:\Program Files (x86)\Microsoft SDKs\NuGetPackages\
Done: 0.

The other prompt says "Required assets to build and debug are missing from 'WebApplication1'. Add them?". This also sounds very promising and I give it the nod. This creates a .vscode directory and 2 enclosed files; launch.json and tasks.json.

So lets try that F5 thing again... http://localhost:5000/ is now serving the same app. That looks pretty good. So lets add a breakpoint to the HomeController and see if we can hit it:

Well I can certainly add a breakpoint but all those red squigglies are unnerving me. Let's clean the slate. If you want to simply do that in VS Code hold down CTRL+SHIFT+P and then type "reload". Pick "Reload window". A couple of seconds later we're back in and Code is looking much happier. Can we hit our breakpoint?

Yes we can! So you're free to develop in either Code or VS; the choice is yours. I think that's pretty awesome - and well done to all the peeople behind Code who've made this a pretty seamless experience!

Visual Studio, tsconfig.json and external TypeScript compilation

TypeScript first gained support for tsconfig.json back with the 1.5 release. However, to my lasting regret and surprise Visual Studio will not be gaining meaningful support for it until TypeScript 1.8 ships. However, if you want it now, it's already available to use in beta.

I've already leapt aboard. Whilst there's a number of bugs in the beta it's still totally usable. So use it.

External TypeScript Compilation and the VS build#

Whilst tsconfig.json is useful and super cool it has limitations. It allows you to deactivate compilation upon file saving using compileOnSave. What it doesn't allow is deactivation of the TypeScript compilation that happens as part of a Visual Studio build. That may not matter for the vanilla workflow of just dropping TypeScript files in a Visual Studio web project and having VS invoke the TypeScript compilation. However it comes to matter when your workflow deviates from the norm, as mine does. Using external compilation of TypeScript within Visual Studio is a little tricky. My own use case is somewhat atypical but perhaps not uncommon.

I'm working on a project which has been built using TypeScript since TS 0.9. Not surprisingly, this started off using the default Visual Studio / TypeScript workflow. Active development on the project ceased around 9 months ago. Now it's starting up again. It's a reasonable sized web app and the existing functionality is, in the main, fine. But the users want to add some new screens.

Like any developer, I want to build with the latest and greatest. In my case, this means I want to write modular ES6 using TypeScript. With this approach my code can be leaner and I have less script ordering drama in my life. (Yay import statements!) This can be done by bringing together webpack, TypeScript (ts-loader) and Babel (babel-loader). There's an example of this approach here. Given the size of the existing codebase I'd rather leave the legacy TypeScript as is and isolate my new approach to the screens I'm going to build. Obviously I'd like to have a common build process for all the codebase at some point but I've got a deadline to meet and so a half-old / half-new approach is called for (at least for the time being).

Goodbye TypeScript Compilation in VS#

Writing modular ES6 TypeScript which is fully transpiled to old-school JS is not possible using the Visual Studio tooling at present. For what it's worth I think that SystemJS compilation may make this more possible in the future but I don't really know enough about it to be sure. That's why I'm bringing webpack / Babel into the mix right now. I don't want Visual Studio to do anything for the ES6 code; I don't want it to compile. I want to deactivate my TypeScript compilation for the ES6 code. I can't do this from the tsconfig.json so I'm in a bit of a hole. What to do?

Well, as of (I think) TypeScript 1.7 it's possible to deactivate TypeScript compilation in Visual Studio. To quote:

there is an easier way to disable TypeScriptCompile:

Just add <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> to the .csproj, e.g. in the first <PropertyGroup>.

Awesomeness!

But wait, this means that the legacy TypeScript isn't being compiled any longer. Bear in mind, I'm totally happy with the existing / legacy TypeScript compilation. Nooooooooooooooo!!!!!!!!!!!!!!!

Hello TypeScript Compilation outside VS#

Have no fear, I gotcha. What we're going to do is ensure that Visual Studio triggers 2 external TypeScript builds as part of its own build process:

  • The modular ES6 TypeScript (new)
  • The legacy TypeScript (old)

How do we do this? Through the magic of build targets. We need to add this to our .csproj: (I add it near the end; I'm not sure if location matters though)

<PropertyGroup>
<CompileDependsOn>
$(CompileDependsOn);
WebClientBuild;
</CompileDependsOn>
<CleanDependsOn>
$(CleanDependsOn);
WebClientClean
</CleanDependsOn>
<CopyAllFilesToSingleFolderForPackageDependsOn>
CollectGulpOutput;
CollectLegacyTypeScriptOutput;
$(CopyAllFilesToSingleFolderForPackageDependsOn);
</CopyAllFilesToSingleFolderForPackageDependsOn>
<CopyAllFilesToSingleFolderForMsdeployDependsOn>
CollectGulpOutput;
CollectLegacyTypeScriptOutput;
$(CopyAllFilesToSingleFolderForPackageDependsOn);
</CopyAllFilesToSingleFolderForMsdeployDependsOn>
</PropertyGroup>
<Target Name="WebClientBuild">
<Exec Command="npm install" />
<Exec Command="npm run build-legacy-typescript" />
<Exec Command="npm run build -- --mode $(ConfigurationName)" />
</Target>
<Target Name="WebClientClean">
<Exec Command="npm run clean" />
</Target>
<Target Name="CollectGulpOutput">
<ItemGroup>
<_CustomFiles Include="dist\**\*" />
<FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
<DestinationRelativePath>dist\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
</ItemGroup>
<Message Text="CollectGulpOutput list: %(_CustomFiles.Identity)" />
</Target>
<Target Name="CollectLegacyTypeScriptOutput">
<ItemGroup>
<_CustomFiles Include="Scripts\**\*.js" />
<FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
<DestinationRelativePath>Scripts\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
</ItemGroup>
<Message Text="CollectLegacyTypeScriptOutput list: %(_CustomFiles.Identity)" />
</Target>

There's a few things going on here; let's take them one by one.

The WebClientBuild Target#

This target triggers our external builds. One by one it runs the following commands:

npm install
Installs the npm packages.
npm run build-legacy-typescript
Runs the "build-legacy-typescript"script in our package.json
npm run build -- --mode $(ConfigurationName)
Runs the "build"script in our package.json and passes through a mode parameter of either "Debug" or "Release" from MSBuild - indicating whether we're creating a debug or a release build.

As you've no doubt gathered, I'm following the convention of using the scripts element of my package.json as repository for the various build tasks I might have for a web project. It looks like this:

{
// ...
"scripts": {
"test": "karma start --reporters mocha,junit --single-run --browsers PhantomJS",
"build-legacy-typescript": "tsc -v&&tsc --project Scripts",
"clean": "gulp delete-dist-contents",
"watch": "gulp watch",
"build": "gulp build"
},
// ...
}

As you can see, "build-legacy-typescript" invokes tsc (which is registered as a devDependency) to print out the version of the compiler. Then it invokes tsc again using the project flag directly on the Scripts directory. This is where the legacy TypeScript and its associated tsconfig.json resides. Et voil谩, the old / existing TypeScript is compiled just as it was previously by VS itself.

Next, the "build" invokes a gulp task called, descriptively, "build". This task caters for our brand new codebase of modular ES6 TypeScript. When run, this task will invoke webpack, copy static files, build less etc. Quick digression: you can see there's a "watch" script that does the same thing on a file-watching basis; I use that during development.

The WebClientClean Target#

The task that runs to clean up artefacts created by WebClientBuild.

The CollectLegacyTypeScriptOutput and CollectGulpOutput Targets#

Since we're compiling our TypeScript outside of VS we need to tell MSBuild / MSDeploy about the externally compiled assets in order that they are included in the publish pipeline. Here I'm standing on the shoulders of Steve Cadwallader's excellent post. Thanks Steve!

CollectLegacyTypeScriptOutput and CollectGulpOutput respectively include all the built files contained in the "Scripts" and "dist" folders when a publish takes place. You don't need this for when you're building on your own machine but if you're looking to publish (either from your machine or from TFS) then you will need exactly this. Believe me that last sentence was typed with a memory of great pain and frustration.

So in the end, as far as TypeScript is concerned, I'm using Visual Studio solely as an editor. It's the hooks in the .csproj that ensure that compilation happens. It seems a little quirky that we still need to have the original TypeScript targets in the .csproj file as well; but it works. That's all that matters.

Gulp, npm, long paths and Visual Studio.... Fight!

How I managed to gulp-angular-templatecache working inside Visual Studio#

Every now and then something bites you unexpectedly. After a certain amount of pain, the answer comes to you and you know you want to save others from falling into the same deathtrap.

There I was minding my own business and having a play with a Gulp plugin called gulp-angular-templatecache. If you're not aware of it, it "Concatenates and registers AngularJS templates in the $templateCache". I was planning to use it so that all the views in an Angular app of mine were loaded up-front rather than on demand. (It's a first step in making an "offline-first" version of that particular app.)

I digress already. No sooner had I tapped in:

npm install gulp-angular-templatecache --saveDev

Then I noticed my Visual Studio project was no longer compiling. It was dying a death on build with this error:

ASPNETCOMPILER : error ASPRUNTIME: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.

I was dimly aware that there were issues with the nested node_modules leading to Windows-killing paths. This sounded just like that.... And it was! gulp-angular-templatecache had a dependency on gulp-footer which had a dependency on lodash.assign which had a dependency on lodash._basecreatecallback which had.... You see where I'm going? It seems that the lovely lodash has created the path from hell.

For reasons that aren't particularly clear this kills Visual Studio's build process. This is slightly surprising given that our rogue path is sat in the node_modules directory which isn't part of the project in Visual Studio. That being the case you'd imagine that you could do what you liked there. But no, it seems VS is a delicate flower and we must be careful not to offend. Strange.

It's Workaround Time!#

After a great deal of digging I found the answer nestled in the middle of an answer on Stack Overflow. To quote:

If you will add "lodash.bind" module to your project's package.json as dependency it will be installed in one level with gulp and not as gulp's dependency

That's right, I just needed to tap enter this at the root of my project:

npm install lodash.bind --saveDev

And all was sweetness and light once more - no more complaints from VS.

The Future#

It looks like lodash are on course to address this issue. So one day this this workaround won't be necessary anymore which is good.

However, the general long path issue concerning node / npm hasn't vanished for Windows users. Given VS 2015 is planning to make Gulp and Grunt 1st class citizens of Visual Studio I'm going to guess that sort of issue is likely to arise again and again for other packages. I'm hoping that means that someone will actually fix the underlying path issues that upset Windows with npm.

It sounds like npm are planning to take some steps which is great. But I can't be alone in having a slightly nagging feeling that Windows isn't quite a first class citizen for node / io / npm yet. I really hope it will become one.

Using Gulp in Visual Studio instead of Web Optimization

Update 17/02/2015: I've taken the approach discussed in this post a little further - you can see here#

I've used a number of tools to package up JavaScript and CSS in my web apps. Andrew Davey's tremendous Cassette has been really useful. Also good (although less powerful/magical) has been Microsoft's very own Microsoft.AspNet.Web.Optimization that ships with MVC.

I was watching the ASP.NET Community Standup from October 7th, 2014 and learned that the ASP.Net team is not planning to migrate Microsoft.AspNet.Web.Optimization to the next version of ASP.Net. Instead they're looking to make use of JavaScript task runners like Grunt and maybe Gulp. Perhaps you're even dimly aware that they've been taking steps to make these runners more of a first class citizen in Visual Studio, hence the recent release of the new and groovy Task Runner Explorer.

Gulp has been on my radar for a while now as has Grunt. By "on my radar" what I really mean is "Hmmmm, I really need to learn this..... perhaps I could wait until the Betamax vs VHS battles are done? Oh never mind, here we go...".

My understanding is that Grunt and Gulp essentially do the same thing (run tasks in JavaScript) but have different approaches. Grunt is more about configuration, Gulp is more about code. At present Gulp also has a performance advantage as it does less IO than Grunt - though I understand that's due to change in the future. But generally my preference is code over configuration. On that basis I decided that I was going to give Gulp first crack.

Bub bye Web Optimization#

I already had a project that used Web Optimization to bundle JavaScript and CSS files. When debugging on my own machine Web Optimization served up the full JavaScript and CSS files. Thanks to the magic of source maps I was able to debug the TypeScript that created the JavaScript files too. Which was nice. When I deployed to production, Web Optimization minified and concatenated the JavaScript and CSS files. This meant I had a single HTTP request for JavaScript and a single HTTP request for CSS. This was also... nooice!

I took a copy of my existing project and created a new repo for it on GitHub. It was very simple in terms of bundling. It had a BundleConfig that created 2 bundles; 1 for JavaScript and 1 for CSS:

using System.Web;
using System.Web.Optimization;
namespace Proverb.Web
{
public class BundleConfig
{
// For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
public static void RegisterBundles(BundleCollection bundles)
{
var angularApp = new ScriptBundle("~/angularApp").Include(
// Vendor Scripts
"~/scripts/jquery-{version}.js",
"~/scripts/angular.js",
"~/scripts/angular-animate.js",
"~/scripts/angular-route.js",
"~/scripts/angular-sanitize.js",
"~/scripts/angular-ui/ui-bootstrap-tpls.js",
"~/scripts/toastr.js",
"~/scripts/moment.js",
"~/scripts/spin.js",
"~/scripts/underscore.js",
// Bootstrapping
"~/app/app.js",
"~/app/config.route.js",
// common Modules
"~/app/common/common.js",
"~/app/common/logger.js",
"~/app/common/spinner.js",
// common.bootstrap Modules
"~/app/common/bootstrap/bootstrap.dialog.js"
);
// directives
angularApp.IncludeDirectory("~/app/directives", "*.js", true);
// services
angularApp.IncludeDirectory("~/app/services", "*.js", true);
// controllers
angularApp.IncludeDirectory("~/app/admin", "*.js", true);
angularApp.IncludeDirectory("~/app/about", "*.js", true);
angularApp.IncludeDirectory("~/app/dashboard", "*.js", true);
angularApp.IncludeDirectory("~/app/layout", "*.js", true);
angularApp.IncludeDirectory("~/app/sayings", "*.js", true);
angularApp.IncludeDirectory("~/app/sages", "*.js", true);
bundles.Add(angularApp);
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/content/ie10mobile.css",
"~/content/bootstrap.css",
"~/content/font-awesome.css",
"~/content/toastr.css",
"~/content/styles.css"
));
}
}
}

I set myself a task. I wanted to be able to work in *exactly* the way I was working now. But using Gulp instead of Web Optimization. I wanted to lose the BundleConfig above and remove Web Optimization from my application, secure in the knowledge that I had lost nothing. Could it be done? Read on!

Installing Gulp (and Associates)#

I fired up Visual Studio and looked for an excuse to use the Task Runner Explorer. The first thing I needed was Gulp. My machine already had Node and NPM installed so I went to the command line to install Gulp globally:

npm install gulp -g

Now to start to plug Gulp into my web project. It was time to make the introductions: Visual Studio meet NPM. At the root of the web project I created a package.json file by executing the following command and accepting all the defaults:

npm init

I wanted to add Gulp as a development dependency of my project: ("Development" because I only need to run tasks at development time. My app has no dependency on Gulp at runtime - at that point it's just about serving up static files.)

npm install gulp --save-dev

This installs gulp local to the project as a development dependency. As a result we now have a "node_modules" folder sat in our root which contains our node packages. Currently, as our package.json reveals, this is only gulp:

"devDependencies": {
"gulp": "^3.8.8"
}

It's time to go to town. Let's install all the packages we're going to need to bundle and minify JavaScript and CSS:

npm install gulp-concat gulp-uglify gulp-rev del path gulp-ignore gulp-asset-manifest gulp-minify-css --save-dev

This installs the packages as dev dependencies (as you've probably guessed) and leaves us with a list of dev dependencies like this:

"devDependencies": {
"del": "^0.1.3",
"gulp": "^3.8.8",
"gulp-asset-manifest": "0.0.5",
"gulp-concat": "^2.4.1",
"gulp-ignore": "^1.2.1",
"gulp-minify-css": "^0.3.10",
"gulp-rev": "^1.1.0",
"gulp-uglify": "^1.0.1",
"path": "^0.4.9"
}

Making gulpfile.js#

So now I was ready. I had everything I needed to replace my BundleConfig.cs. I created a new file called gulpfile.js in the root of my web project that looked like this:

/// <vs AfterBuild='default' />
var gulp = require("gulp");
// Include Our Plugins
var concat = require("gulp-concat");
var ignore = require("gulp-ignore");
var manifest = require("gulp-asset-manifest");
var minifyCss = require("gulp-minify-css");
var uglify = require("gulp-uglify");
var rev = require("gulp-rev");
var del = require("del");
var path = require("path");
var tsjsmapjsSuffix = ".{ts,js.map,js}";
var excludetsjsmap = "**/*.{ts,js.map}";
var bundleNames = { scripts: "scripts", styles: "styles" };
var filesAndFolders = {
base: ".",
buildBaseFolder: "./build/",
debug: "debug",
release: "release",
css: "css",
// The fonts we want Gulp to process
fonts: ["./fonts/*.*"],
// The scripts we want Gulp to process - adapted from BundleConfig
scripts: [
// Vendor Scripts
"./scripts/angular.js",
"./scripts/angular-animate.js",
"./scripts/angular-route.js",
"./scripts/angular-sanitize.js",
"./scripts/angular-ui/ui-bootstrap-tpls.js",
"./scripts/toastr.js",
"./scripts/moment.js",
"./scripts/spin.js",
"./scripts/underscore.js",
// Bootstrapping
"./app/app" + tsjsmapjsSuffix,
"./app/config.route" + tsjsmapjsSuffix,
// common Modules
"./app/common/common" + tsjsmapjsSuffix,
"./app/common/logger" + tsjsmapjsSuffix,
"./app/common/spinner" + tsjsmapjsSuffix,
// common.bootstrap Modules
"./app/common/bootstrap/bootstrap.dialog" + tsjsmapjsSuffix,
// directives
"./app/directives/**/*" + tsjsmapjsSuffix,
// services
"./app/services/**/*" + tsjsmapjsSuffix,
// controllers
"./app/about/**/*" + tsjsmapjsSuffix,
"./app/admin/**/*" + tsjsmapjsSuffix,
"./app/dashboard/**/*" + tsjsmapjsSuffix,
"./app/layout/**/*" + tsjsmapjsSuffix,
"./app/sages/**/*" + tsjsmapjsSuffix,
"./app/sayings/**/*" + tsjsmapjsSuffix
],
// The styles we want Gulp to process - adapted from BundleConfig
styles: [
"./content/ie10mobile.css",
"./content/bootstrap.css",
"./content/font-awesome.css",
"./content/toastr.css",
"./content/styles.css"
]
};
filesAndFolders.debugFolder = filesAndFolders.buildBaseFolder + "/" + filesAndFolders.debug + "/";
filesAndFolders.releaseFolder = filesAndFolders.buildBaseFolder + "/" + filesAndFolders.release + "/";
/**
* Create a manifest depending upon the supplied arguments
*
* @param {string} manifestName
* @param {string} bundleName
* @param {boolean} includeRelativePath
* @param {string} pathPrepend
*/
function getManifest(manifestName, bundleName, includeRelativePath, pathPrepend) {
// Determine filename ("./build/manifest-debug.json" or "./build/manifest-release.json"
var manifestFile = filesAndFolders.buildBaseFolder + "manifest-" + manifestName + ".json";
return manifest({
bundleName: bundleName,
includeRelativePath: includeRelativePath,
manifestFile: manifestFile,
log: true,
pathPrepend: pathPrepend,
pathSeparator: "/"
});
}
// Delete the build folder
gulp.task("clean", function (cb) {
del([filesAndFolders.buildBaseFolder], cb);
});
// Copy across all files in filesAndFolders.scripts to build/debug
gulp.task("scripts-debug", ["clean"], function () {
return gulp
.src(filesAndFolders.scripts, { base: filesAndFolders.base })
.pipe(gulp.dest(filesAndFolders.debugFolder));
});
// Create a manifest.json for the debug build - this should have lots of script files in
gulp.task("manifest-scripts-debug", ["scripts-debug"], function () {
return gulp
.src(filesAndFolders.scripts, { base: filesAndFolders.base })
.pipe(ignore.exclude("**/*.{ts,js.map}")) // Exclude ts and js.map files from the manifest (as they won't become script tags)
.pipe(getManifest(filesAndFolders.debug, bundleNames.scripts, true));
});
// Copy across all files in filesAndFolders.styles to build/debug
gulp.task("styles-debug", ["clean"], function () {
return gulp
.src(filesAndFolders.styles, { base: filesAndFolders.base })
.pipe(gulp.dest(filesAndFolders.debugFolder));
});
// Create a manifest.json for the debug build - this should have lots of style files in
gulp.task("manifest-styles-debug", ["styles-debug", "manifest-scripts-debug"], function () {
return gulp
.src(filesAndFolders.styles, { base: filesAndFolders.base })
//.pipe(ignore.exclude("**/*.{ts,js.map}")) // Exclude ts and js.map files from the manifest (as they won't become script tags)
.pipe(getManifest(filesAndFolders.debug, bundleNames.styles, true));
});
// Concatenate & Minify JS for release into a single file
gulp.task("scripts-release", ["clean"], function () {
return gulp
.src(filesAndFolders.scripts)
.pipe(ignore.exclude("**/*.{ts,js.map}")) // Exclude ts and js.map files - not needed in release mode
.pipe(concat("app.js")) // Make a single file - if you want to see the contents then include the line below
//.pipe(gulp.dest(releaseFolder))
.pipe(uglify()) // Make the file titchy tiny small
.pipe(rev()) // Suffix a version number to it
.pipe(gulp.dest(filesAndFolders.releaseFolder)); // Write single versioned file to build/release folder
});
// Create a manifest.json for the release build - this should just have a single file for scripts
gulp.task("manifest-scripts-release", ["scripts-release"], function () {
return gulp
.src(filesAndFolders.buildBaseFolder + filesAndFolders.release + "/*.js")
.pipe(getManifest(filesAndFolders.release, bundleNames.scripts, false));
});
// Copy across all files in filesAndFolders.styles to build/debug
gulp.task("styles-release", ["clean"], function () {
return gulp
.src(filesAndFolders.styles)
.pipe(concat("app.css")) // Make a single file - if you want to see the contents then include the line below
//.pipe(gulp.dest(releaseFolder))
.pipe(minifyCss()) // Make the file titchy tiny small
.pipe(rev()) // Suffix a version number to it
.pipe(gulp.dest(filesAndFolders.releaseFolder + "/" + filesAndFolders.css)); // Write single versioned file to build/release folder
});
// Create a manifest.json for the debug build - this should have a single style files in
gulp.task("manifest-styles-release", ["styles-release", "manifest-scripts-release"], function () {
return gulp
.src(filesAndFolders.releaseFolder + "**/*.css")
.pipe(getManifest(filesAndFolders.release, bundleNames.styles, false, filesAndFolders.css + "/"));
});
// Copy across all fonts in filesAndFolders.fonts to both release and debug locations
gulp.task("fonts", ["clean"], function () {
return gulp
.src(filesAndFolders.fonts, { base: filesAndFolders.base })
.pipe(gulp.dest(filesAndFolders.debugFolder))
.pipe(gulp.dest(filesAndFolders.releaseFolder));
});
// Default Task
gulp.task("default", [
"scripts-debug", "manifest-scripts-debug", "styles-debug", "manifest-styles-debug",
"scripts-release", "manifest-scripts-release", "styles-release", "manifest-styles-release",
"fonts"
]);

What gulpfile.js does#

This file does a number of things each time it is run. First of all it deletes any build folder in the root of the web project so we're ready to build anew. Then it packages up files both for debug and for release mode. For debug it does the following:

  1. It copies the ts, js.map and js files declared in filesAndFolders.scripts to the build/debug folder preserving their original folder structure. (So, for example, app/app.ts, app/app.js.map and app/app.js will all end up at build/debug/app/app.ts, build/debug/app/app.js.map and build/debug/app/app.js respectively.) This is done to allow the continued debugging of the original TypeScript files when running in debug mode.
  2. It copies the css files declared in filesAndFolders.styles to the build/debug folder preserving their original folder structure. (So content/bootstrap.css will end up at build/debug/content/bootstrap.css.)
  3. It creates a build/manifest-debug.json file which contains details of all the script and style files that have been packaged up: ```json { "scripts":[ "scripts/angular.js", "scripts/angular-animate.js", "scripts/angular-route.js", "scripts/angular-sanitize.js", "scripts/angular-ui/ui-bootstrap-tpls.js", "scripts/toastr.js", "scripts/moment.js", "scripts/spin.js", "scripts/underscore.js", "app/app.js", "app/config.route.js", "app/common/common.js", "app/common/logger.js", "app/common/spinner.js", "app/common/bootstrap/bootstrap.dialog.js", "app/directives/imgPerson.js", "app/directives/serverError.js", "app/directives/sidebar.js", "app/directives/spinner.js", "app/directives/waiter.js", "app/directives/widgetClose.js", "app/directives/widgetHeader.js", "app/directives/widgetMinimize.js", "app/services/datacontext.js", "app/services/repositories.js", "app/services/repository.sage.js", "app/services/repository.saying.js", "app/about/about.js", "app/admin/admin.js", "app/dashboard/dashboard.js", "app/layout/shell.js", "app/layout/sidebar.js", "app/layout/topnav.js", "app/sages/sageDetail.js", "app/sages/sageEdit.js", "app/sages/sages.js", "app/sayings/sayingEdit.js", "app/sayings/sayings.js" ], "styles":[ "content/ie10mobile.css", "content/bootstrap.css", "content/font-awesome.css", "content/toastr.css", "content/styles.css" ] }

For release our gulpfile works with the same resources but has a different aim. Namely to minimise the the number of HTTP requests, obfuscate the code and version the files produced to prevent caching issues. To achieve those lofty aims it does the following:

  1. It concatenates together all the js files declared in filesAndFolders.scripts, minifies them and writes them to a single build/release/app-{xxxxx}.js file (where -{xxxxx} represents a version created by gulp-rev).
  2. It concatenates together all the css files declared in filesAndFolders.styles, minifies them and writes them to a single build/release/css/app-{xxxxx}.css file. The file is placed in a css subfolder because of relative paths specified in the CSS file.
  3. It creates a build/manifest-release.json file which contains details of all the script and style files that have been packaged up: ```json { "scripts":["app-95d1e06d.js"], "styles":["css/app-1a6256ea.css"] }
As you can see, the number of files included are reduced down to 2; 1 for JavaScript and 1 for CSS.
<!-- -->
Finally, for both the debug and release packages the contents of the `fonts` folder is copied across wholesale, preserving the original folder structure. This is because the CSS files contain relative references that point to the font files. If I had image files which were referenced by my CSS I'd similarly need to include these in the build process.
## Task Runner Explorer gets in on the action
The eagle eyed amongst you will also have noticed a peculiar first line to our `gulpfile.js`:
```js
/// <vs AfterBuild='default' />

This mysterious comment is actually how the Task Runner Explorer hooks our gulpfile.js into the Visual Studio build process. Our "magic comment" ensures that on the AfterBuild event, Task Runner Explorer runs the default task in our gulpfile.js. The reason we're using the AfterBuild event rather than the BeforeBuild event is because our project contains TypeScript and we need the transpiled JavaScript to be created before we can usefully run our package tasks. If we were using JavaScript alone then that wouldn't be an issue and either build event would do.

How do I use this in my HTML?#

Well this is magnificent - we have a gulpfile that builds our debug and release packages. The question now is, how do we use it?

Web Optimization made our lives really easy. Up in my head I had a @Styles.Render("~/Content/css") which pushed out my CSS and down at the foot of the body tag I had a @Scripts.Render("~/angularApp") which pushed out my script tags. Styles and Scripts are server-side utilities. It would be very easy to write equivalent utility classes that, depending on whether we were in debug or not, read the appropriate build/manifest-xxxxxx.json file and served up either debug or release style / script tags.

That would be pretty simple - and for what it's worth **simple is good

**. But today I felt like a challenge. What say server side rendering had been outlawed? A draconian ruling had been passed and all you had to play with was HTML / JavaScript and a server API that served up JSON? What would you do then? (All fantasy I know... But go with me on this - it's a journey.) Or more sensibly, what if you just want to remove some of the work your app is doing server-side to bundle and minify. Just serve up static assets instead. Spend less money in Azure why not?

Before I make all the changes let's review where we were. I had a single MVC view which, in terms of bundles, CSS and JavaScript pretty much looked like this:

<!DOCTYPE html>
<html>
<head>
<!-- ... -->
@Styles.Render("~/Content/css")
</head>
<body>
<!-- ... -->
@Scripts.Render("~/angularApp")
<script>
(function () {
$.getJSON('@Url.Content("~/Home/StartApp")')
.done(function (startUpData) {
var appConfig = $.extend({}, startUpData, {
appRoot: '@Url.Content("~/")',
remoteServiceRoot: '@Url.Content("~/api/")'
});
angularApp.start({
thirdPartyLibs: {
moment: window.moment,
toastr: window.toastr,
underscore: window._
},
appConfig: appConfig
});
});
})();
</script>
</body>
</html>

This is already more a complicated example than most peoples use cases. Essentially what's happening here is both bundles are written out as part of the HTML and then, once the scripts have loaded the Angular app is bootstrapped with some configuration loaded from the server by a good old jQuery AJAX call.

After reading an article about script loading by the magnificently funny Jake Archibald I felt ready. I cast my MVC view to the four winds and created myself a straight HTML file:

<!DOCTYPE html>
<html>
<head>
<!-- ... -->
</head>
<body>
<!-- ... -->
<script src="Scripts/jquery-2.1.1.min.js"></script>
<script>
(function () {
var appConfig = {};
var scriptsToLoad;
/**
* Handler which fires as each script loads
*/
function onScriptLoad(event) {
scriptsToLoad -= 1;
// Now all the scripts are present start the app
if (scriptsToLoad === 0) {
angularApp.start({
thirdPartyLibs: {
moment: window.moment,
toastr: window.toastr,
underscore: window._
},
appConfig: appConfig
});
}
}
// Load startup data from the server
$.getJSON("api/Startup")
.done(function (startUpData) {
appConfig = startUpData;
// Determine the assets folder depending upon whether in debug mode or not
var buildFolder = appConfig.appRoot + "build/";
var debugOrRelease = (appConfig.inDebug ? "debug" : "release");
var manifestFile = buildFolder + "manifest-" + debugOrRelease + ".json";
var outputFolder = buildFolder + debugOrRelease + "/";
// Load JavaScript and CSS listed in manifest file
$.getJSON(manifestFile)
.done(function (manifest){
manifest.styles.forEach(function (href) {
var link = document.createElement("link");
link.rel = "stylesheet";
link.media = "all";
link.href = outputFolder + href;
document.head.appendChild(link);
});
scriptsToLoad = manifest.scripts.length;
manifest.scripts.forEach(function (src) {
var script = document.createElement("script");
script.onload = onScriptLoad;
script.src = outputFolder + src;
script.async = false;
document.head.appendChild(script);
});
})
});
})();
</script>
</body>
</html>

If you very carefully compare the HTML above the MVC view that it replaces you can see the commonalities. They are doing pretty much the same thing - the only real difference is the bootstrapping API. Previously it was an MVC endpoint at /Home/StartApp. Now it's a Web API endpoint at api/Startup. Here's how it works:

  1. A jQuery AJAX call kicks off a call to load the bootstrapping / app config data. Importantly this data includes whether the app is running in debug or not.
  2. Depending on the isDebug flag the app either loads the build/manifest-debug.json or build/manifest-release.json manifest.
  3. For each CSS file in the styles bundle a link element is created and added to the page.
  4. For each JavaScript file in the scripts bundle a script element is created and added to the page.

It's worth pointing out that this also has a performance edge over Web Optimization as the assets are loaded asynchronously! (Yes I know it says script.async = false but that's not what you think it is... Go read Jake's article!)

To finish off I had to make a few tweaks to my web.config:

<!-- Allow ASP.Net to serve up JSON files -->
<system.webServer>
<staticContent>
<mimeMap fileExtension=".json" mimeType="application/json"/>
</staticContent>
</system.webServer>
<!-- The build folder (and it's child folder "debug") will not be cached.
When people are debugging they don't want to cache -->
<location path="build">
<system.webServer>
<staticContent>
<clientCache cacheControlMode="DisableCache"/>
</staticContent>
</system.webServer>
</location>
<!-- The release folder will be cached for a loooooong time
When you're in Production caching is your friend -->
<location path="build/release">
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge"/>
</staticContent>
</system.webServer>
</location>

I want to publish, how do I include my assets?#

It's time for some csproj trickery. I must say I think I'll be glad to see the back of project files when ASP.Net vNext ships. This is what you need:

<Target Name="AfterBuild">
<ItemGroup>
<!-- what ever is in the build folder should be included in the project -->
<Content Include="build\**\*.*" />
</ItemGroup>
</Target>

What's happening here is that *after* a build Visual Studio considers the complete contents of the build folder to part of the project. It's after the build because the folder will be deleted and reconstructed as part of the build.

Team Foundation Server, Continuous Integration and separate projects for JavaScript unit tests

Do you like to separate out your unit tests from the project you are testing? I imagine so. My own practice when creating a new project in Visual Studio is to create a separate unit test project alongside whose responsibility is to house unit tests for that new project.

When I check in code for that project I expect the continuous integration build to kick off and, as part of that, the unit tests to be run. When it comes to running .NET tests then Team Foundation Server (and it's cloud counterpart Visual Studio Online) has your back. When it comes to running JavaScript tests then... not so much.

This post will set out:

  1. How to get JavaScript tests to run on TFS / VSO in a continuous integration scenario.
  2. How to achieve this *without* having to include your tests as part of web project.

To do this I will lean heavily (that's fancy language for "rip off entirely") on an excellent blog post by Mathew Aniyan which covers point #1. My contribution is point #2.

Points #1 and #2 in short order#

First of all, install Chutzpah on TFS / VSO. You can do this by following Steps 1 - 6 from Mathew Aniyan's post. Instead of following steps 7 and 8 create a new unit test project in your solution.

Edit 29/05/2014: Matthew Manela (creator of Chutzpah) has confirmed that this is the correct approach - thanks chap!

@johnny_reilly Nope that is pretty much what you need to do.

鈥 Matthew Manela (@mmanela) May 15, 2014

To our unit test project add your JavaScript unit tests. These should be marked in Visual Studio with a Build Action of "Content" and a Copy to Output Directory of "Do not copy". You should be able to run these tests locally using the Visual Studio Chutzpah extension - or indeed in some other JavaScript test runner. Then, and this is the important part, edit the csproj file of your unit test project and add this Import Project statement:

<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />

Ordering is important in this case. It matters that this new statement sits after the other Import Project statements. So you should end up with a csproj file that looks in part like this: (comments added by me for clarity)

<!-- Pre-existing Import Project statements start -->
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- Pre-existing Import Project statements end -->
<!-- New addition start -->
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
<!-- New addition end -->

Check in your amended csproj and the unit tests to TFS / VSO. You should see the JavaScript unit tests being run as part of the build.