I came up with an ideal strategy:
- Place the pristine CMS files at an external "code libraries" location, and make them read-only for good measure.
- Make a copy of the CMS-supplied project file.
- Convert all project items to link to the code library's copy, including assembly references. The required
link
element for MSBuild complicates this, so I wrote a little console app to parse the XML and generate the necessary elements. I also store the path to the library in a property.
At this point, you have a compilable project. The files are all just links to read-only, baseline, CMS-provided files, but the IDE shows a nice directory structure, as if the files were actually in your repository. As you customize, you can replace the links with your own actual files.
However, your website files are now split between two completely different locations. The web server needs them to actually be in the same directory. This is where some scripting comes in handy.
Using your build scripting tool, plug into the post-build event to copy linked files into the project directory. Naturally, you can omit the compiled files. Here's how I did it with MSBuild (thanks to this blog post):
<Target Name="AfterBuild"> <ItemGroup> <LinkedKenticoFiles Include="@(None);@(EmbeddedResource);@(Content)" Condition="$([System.String]::new('%(FullPath)').StartsWith('$(MY_LIBRARY_BASE_PATH)'))" /> </ItemGroup> <Copy SourceFiles="%(LinkedKenticoFiles.Identity)" DestinationFiles="%(LinkedKenticoFiles.Link)" SkipUnchangedFiles='true' /> </Target>
Optionally, plug into the cleaning event to remove those files. I used similar MSBuild syntax to delete the files, and because it leaves behind empty directories, I remove empty directories, too. (Thanks to this answer.)
<Target Name="BeforeClean"> <ItemGroup> <LinkedKenticoFiles Include="@(None);@(EmbeddedResource);@(Content)" Condition="$([System.String]::new('%(FullPath)').StartsWith('$(MY_LIBRARY_BASE_PATH)'))" /> </ItemGroup> <Delete Files="%(LinkedKenticoFiles.Link)" /> <ItemGroup> <Directories Include="$([System.IO.Directory]::GetDirectories($(MSBuildProjectDirectory), '*', System.IO.SearchOption.AllDirectories))" /> <Directories> <Files>$([System.IO.Directory]::GetFiles("%(Directories.Identity)", "*", System.IO.SearchOption.AllDirectories).get_Length())</Files> </Directories> </ItemGroup> <RemoveDir Directories="@(Directories)" Condition="%(Files)=='0'" /> </Target>
Optionally, make your source control ignore the site / project folder, to avoid checking in the temporary, copied site files.