As I consideredthe possible approaches to this scenario, I had a few limitations/requirements that apply due to various restraints. There is no money available to license a third-party tool; the minification process needs to run automatically and as seamless as possible; I want developers to accomplish everything inside Visual Studio; and any tools/configuration needs to be inside the source control repository.
As a result, I had three alternatives to consider:
- Custom Tool that operates on the file
- Pre/Post events in Visual Studio
- Custom task in the build process
Custom Tool inside Visual Studio
Pre/Post events inside Visual Studio
There are two sets of pre/post events in SharePoint projects - pre/post build and pre/post deployment. Obviously, the pre/post deployment events will not work, since they run after the wsp file is generated. The pre/post build events are promising though. They run before the packaging process, they are seamless to developers, and the settings are stored within the project file - allowing the settings to be stored in source control.
Custom task in build process
All of the benefits of the pre/post build events also apply to the build process. There is one drawback, however. The build process typically operates on a build server after code is committed. I want the minification to happen on the developer's machines. But, if the pre/post build event is coded appropriately, they can also run on the build server.
So, how are we minifying our file? I am using the pre-build event in Visual Studio. To also leverage the build server, I created a target directly in the .csproj files and that target is part of the pre-build event processing. The remainder of this post will cover the implementation details.
Modifying the project file
Before proceeding any further, I should note that many of the following changes are not performed using Visual Studio dialogs/windows. These changes are updates to the project file (*.csproj) which is actually an xml file that uses the MSBUILD schema. The process requires the project to be unloaded in Visual Studio, edited and then re-loaded.
To help illustrate the changes to the project file, I started with a new, empty SharePoint project, and then added a Visual Web Part (Sandboxed). I then unloaded the project and opened the .csproj file in the XML editor, collapsing all of the PropertyGroup and ItemGroup elements. Since the order of entries in the file does not matter, I will add all customizations at the bottom:
Items to minimize
The major obstacle I had to overcome was the naming of the files. The "source" file name ends in ".debug.js" and all of the metadata variables %(variable) in MSBUILD that reference an extension will only select the ".js" part. Do you know how hard it is to remove a string from the middle of a filename in MSBUILD? Yea, it is that difficult. And it drove me nuts! And, just when I thought I was getting close, it occurred to me that using the web-industry standard naming in which the minified file ends with ".min.js" is perfectly acceptable as well. So, I backed out of that approach and decided that the developer should specify the files to minimize in a different manner.
The "Build Action" property of a file can be assigned to one of several pre-defined values. These values can be extended via a setting in the project file, as described on MSDN in the MSBUILD/Visual Studio integration page:
Additional Build Actions
Visual Studio allows you to change the item type name of a file in a project with the Build Action property of the File Properties window. Compile, EmbeddedResource, Content, and None item type names are always listed in this menu, along with any other item type names already in your project. To ensure any custom item type names are always available in this menu, you can add the names to an item type named AvailableItemName.
Let's look again at our project file:
Now that we have identified the files to process, we can code a build target to select those items. A build target is not normally specified in a project file, rather the standard targets are imported from the files shipped with Visual Studio. However, we are free to add whatever we need. Let's look at a target that simply lists the files that match our criteria.
Minimized file name
Now that we can identify the files to minimize, we are back at the problem I started with. What is the filename of the resulting minified file? Again, using Waldek's tool as inspiration, I started digging thru the MSBUILD/Visual Studio documentation. The Common MSBUILD Project Items page details the attributes of different item types (Build Action in VS Properties). For Content Items, two of the common attributes have relevance to our approach; DependentUpon and LastGenOutput. The following image shows how DependentUpon nests the output file below the input file:
Under the covers, the output files (DialogHandler.min.js & VerbClientHandler.js in the picture above) are actually added to the project file as new elements, and the attributes are applied to them. The LastGenOutput attribute is applied to the input files, neatly tying the related items together (lines 89-95 and 101-107):
Now that we can identify our input files and their output filenames, we need to actually perform the minification. Waldek's code is not a build task, but searching CodePlex yields a build task that uses the same script compressor: http://yuicompressor.codeplex.com/. The documentation on that site is pretty complete; however, the documentation is for version 1 of the code, while the default release is version 2. In addition, version 2 does not include a compiled version of the build task. So, either download the source, or version 1.7 for .Net 3.5 (which is what I did). Remember - SharePoint 2010 runs on .Net 3.5.
Let's take a look at the source and output files:
Calling the task from Visual Studio pre-build
The last step is to ensure that our new target is actually invoked when the project is compiled. The CodePlex site shows how to create a post-build task that launches MSBUILD to process a separate file. I used a very similar alternative - overriding the built-in Before Build target (lines 130-132):
(By default, a csproj file will not include the BeforeBuild target. The MSBUILD process will invoke that target, if it is present: Extend the Visual Studio Build Process.)
We need to ensure that the build task assembly, and all of its dependencies, are copied to all machines that will build the solution. I recommend creating a folder in the project named lib (short for library) to contain all assemblies required by the project. Third-party assemblies (controls/components) would usually go in this library as well.
If a machine running a build (either within Visual Studio or with the msbuild.exe program) and the build references a task assembly that cannot be found, the build will fail. Even if the compile is successful, the build will fail.