Sitecore Helix Powershell Filewatch

Most likely if you’re developing with Sitecore you have your webroot and your source separated and publish to your site with webdeploy or some other kind of publishing technology.  This is a fine way to do it, but it’s far easier if it just happens automatically.  that’s what these scripts aim to do with Powershell!

Filewatch

Here is a zip file for the completed solution that is covered below: HelixFileWatcher. Or check out the source View on Github

First we need to define a few parameters:

#where your solution is
$SourceDirectory = "D:\Source\SitecoreSource\SolutionRoot"
#where your webroot is
$DeployTargetWebPath = "C:\inetpub\wwwroot\sc90.local"

Next we define how files are moved from your solution to the webroot, this is done through a hashtable matching a file extension to a script block. Note that the views are being deployed to a “Demo” MVC Area as an example.

$global:FileWatchActions = @{}
function Get-ProjectRoot{
	param(
		[string]$Path
	)
	if ($path -eq [string]::Empty){
		return [string]::Empty
	}
	if (-Not (Test-Path $Path)){
		return Get-ProjectRoot -Path (split-Path $Path)
	}
	$PathItem = Get-Item -Path $Path
	if (-Not ($PathItem -is [System.IO.DirectoryInfo])){
		return Get-ProjectRoot -Path (Split-Path $Path)
	}
	if ((resolve-path "$Path\*.csproj").Count -gt 0){
		return $Path
	}elseif($PathItem.Parent -ne $null){
		return Get-ProjectRoot -Path $PathItem.Parent.FullName
	}
	return [string]::Empty
}
function Copy-ItemToWebroot{
	param(
		$Path,
		$OldPath,
		$Delete,
		$Index,
		$IntermediatePath
	)
	if ($Index -lt 0){
		return
	}
	
	$TargetPath = $DeployTargetWebPath + $IntermediatePath + $Path.Substring($Index)
	if ($Delete -and (Test-Path $TargetPath)){
		write-host "Removing file $TargetPath" -ForegroundColor Red
		Remove-Item $TargetPath -Force -Recurse
	}elseif (-Not (Test-Path $Path) -and (Test-Path $TargetPath)){
		write-host "Removing file $TargetPath" -ForegroundColor Red
		Remove-Item $TargetPath -Force -Recurse
	}elseif(Test-Path $Path){
		if ($OldPath -ne [string]::Empty){
			$OldTargetPath = $DeployTargetWebPath + $IntermediatePath + $OldPath.Substring($Index)
			if ((Test-Path $OldTargetPath) -and ((Split-Path $Path) -eq (Split-Path $OldPath) )){
				$newName = Split-Path $Path -Leaf -Resolve
				write-host "Renaming Item" -ForegroundColor Yellow
				write-host "    $OldTargetPath" -ForegroundColor Yellow
				write-host "    =>$TargetPath" -ForegroundColor Yellow
				Rename-Item $OldTargetPath $newName -Force
				return
			}
		}
		if (-Not (Test-Path $TargetPath) -or (Compare-Object (ls $Path) (ls $TargetPath) -Property Name, Length, LastWriteTime)){
			write-host "Copying Item" -ForegroundColor Green
			write-host "    $Path" -ForegroundColor Green
			write-host "    =>$TargetPath" -ForegroundColor Green
			New-Item -Path "$(Split-Path $TargetPath)" -ItemType Directory -Force
			Copy-Item -Path $Path -Destination $TargetPath -Recurse -Force
		}
	}
}

#Add watcher action configurations
#Based on extension define how to process the files that are changed
$global:FileWatchActions.Add(".cshtml", {
	param(
		$Path,
		$OldPath,
		$Delete
	)
	$index = $Path.IndexOf("\Views", 5)
	Copy-ItemToWebroot -Path $Path -OldPath $OldPath -Delete $Delete -Index $index -IntermediatePath "\Areas\Demo"
} )

$global:FileWatchActions.Add(".config", {
	param(
		$Path,
		$OldPath,
		$Delete
	)
	$index = $Path.IndexOf("\App_Config\Include", 5)
	Copy-ItemToWebroot -Path $Path -OldPath $OldPath -Delete $Delete -Index $index
	if ($index -eq -1){
		$fileName = Split-Path $Path -Leaf
		$FileDirectory = Get-ProjectRoot -Path $Path
		if ($fileName.StartsWith("web", "CurrentCultureIgnoreCase")){
			Copy-ItemToWebroot -Path $Path -OldPath $OldPath -Delete $Delete -Index $FileDirectory.Length -IntermediatePath "\Areas\Demo"		
		}
	}
} )

$global:FileWatchActions.Add(".dll", {
	param(
		$Path,
		$OldPath,
		$Delete
	)
	$index = $Path.IndexOf("\bin", 5)
	Copy-ItemToWebroot -Path $Path -OldPath $OldPath -Delete $Delete -Index $index	
} )

$global:FileWatchActions.Add("folder", {
	param(
		$Path,
		$OldPath,
		$Delete
	)
	if (-Not( $delete -or $OldPath -ne [string]::Empty)){
		return
	}
	$index = $Path.IndexOf("\Views", 5)
	if ($index -ne -1){
		Copy-ItemToWebroot -Path $Path -OldPath $OldPath -Delete $Delete -Index $index -IntermediatePath "\Areas\Demo"
		return		
	}
	$index = $Path.IndexOf("\App_Config\Include", 5)
	if ($index -ne -1){
		Copy-ItemToWebroot -Path $Path -OldPath $OldPath -Delete $Delete -Index $index -IntermediatePath "\App_Config\Include"
		return		
	}
})

Then we set up the file watchers to watch the important parts of our code

$global:LastEvent = ((Get-Date).AddSeconds(2).ToString('HH:mm:ss.fff'))
function global:Send-ChangesToWebroot{
	param(
	[string]$Path = [string]::Empty,
	[string]$OldPath = [string]::Empty,
	[bool]$Delete = $false
	)
	$extension = [IO.Path]::GetExtension($Path)
	$IsDirectory = $false
	if (Test-Path $Path){
		$IsDirectory= (Get-Item -Path $Path) -is [System.IO.DirectoryInfo]
	}elseif ($Delete -and $extension -eq [string]::Empty){
		$IsDirectory = $true;
	}
	try{
		if (-Not $IsDirectory -and $global:FileWatchActions.ContainsKey($extension)){
			$global:LastEvent = ((Get-Date).AddSeconds(2).ToString('HH:mm:ss.fff'))
			$global:FileWatchActions.Get_Item($extension).Invoke($Path, $OldPath, $Delete)
		}elseif ($IsDirectory){
			$global:LastEvent = ((Get-Date).AddSeconds(2).ToString('HH:mm:ss.fff'))
			$global:FileWatchActions.Get_Item("folder").Invoke($Path, $OldPath, $Delete)
		}
	}catch [System.Exception]{
		Write-Host "An error has occurred while attempting to run the processor for $extension" -ForegroundColor Red
		Write-Host "Path: $Path" -ForegroundColor Red
		Write-Host "OldPath: $OldPath" -ForegroundColor Red
		Write-Host $_.Exception.ToString() -ForegroundColor Red
	}
}
function Add-Watcher{
	param(
		$Directory
	)
	$Watcher = New-Object IO.FileSystemWatcher $Directory, "*" -Property @{IncludeSubdirectories = $true;NotifyFilter = [IO.NotifyFilters]'FileName, DirectoryName, LastWrite, Size'}
	
	Register-ObjectEvent $Watcher Changed -SourceIdentifier "$Directory FileChanged" -Action {Send-ChangesToWebroot -Path $Event.SourceEventArgs.FullPath}
	
	Register-ObjectEvent $Watcher Renamed -SourceIdentifier "$Directory FileRenamed" -Action {Send-ChangesToWebroot -Path $Event.SourceEventArgs.FullPath -OldPath $Event.SourceEventArgs.OldFullPath}
	
	Register-ObjectEvent $Watcher Deleted -SourceIdentifier "$Directory FileDeleted" -Action {Send-ChangesToWebroot -Path $Event.SourceEventArgs.FullPath -Delete $true}
	
	Register-ObjectEvent $Watcher Created -SourceIdentifier "$Directory FileCreated" -Action {Send-ChangesToWebroot -Path $Event.SourceEventArgs.FullPath}
	
	$Watcher.EnableRaisingEvents = $true
}
Resolve-Path "$SourceDirectory/*/App_Config/Include" | ForEach-Object{
	Write-Host "Adding watch location: $_" -ForegroundColor Yellow
	Add-Watcher $_ | Out-Null
}

Resolve-Path "$SourceDirectory/*/Views" | ForEach-Object{
	Write-Host "Adding watch location: $_" -ForegroundColor Yellow	
	Add-Watcher $_ | Out-Null
}

Resolve-Path "$SourceDirectory/*/bin" | ForEach-Object{
	Write-Host "Adding watch location: $_" -ForegroundColor Yellow
	Add-Watcher $_ | Out-Null
}

Resolve-Path "$SourceDirectory/*/Assets" | ForEach-Object {
	Write-Host "Adding watch location: $_" -ForegroundColor Yellow
	Add-Watcher $_ | Out-Null
}

Write-Host [string]::Empty
Write-Host "Now watching for changes made in the repo." -ForegroundColor Yellow
Write-Host "Any changes made will be delivered to the Webroot automatically" -ForegroundColor Yellow
Write-Host "***************************************************************" -ForegroundColor Yellow
while($true){
	#sleep more quickly when changes are happening
	if ($global:LastEvent -gt ((Get-Date).ToString('HH:mm:ss.fff'))){
		Start-Sleep -m 5
	}else{
		Start-Sleep 1
	}
}