Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add F# source file to F# project when invoking a F# source file template? #162

Open
trondr opened this issue Oct 3, 2021 · 3 comments
Open

Comments

@trondr
Copy link

trondr commented Oct 3, 2021

Pre and post action support when invoking a template?

Is it feasible for the template system to do some post actions after invoking a template? Or is this out of scope of the design?

I am doing cmdlet development in F# and when invoking a F# source file template I would like the F# source file to be added to the F# project.

I have created a prototype implementation to be added to the function Invoke-PSMDTemplate, sub function Write-TemplateItem. It is probably not the ideal place to put such implementation? General support for pre-action script and post-action script would probably be better? At least I have something that works on my dev machine for the time being.

Write-TemplateItem (changed from approx line 342):

[...]
[System.IO.File]::WriteAllText($destPath, $text, $Encoding)

#Start: Support for adding *fs file to F# project
#If destination path is a *.fs file it should to be included in a F# project in the current directory
#or some parent directory futher up. Look for the neares .*fsproj file and add the file to the project.
if($destPath.EndsWith(".fs"))
{
	#Destination is a F# source file. Find the parent project(s) and add.
	$destinationFile = [System.IO.FileInfo]::new($destPath)
	Find-PsmdFsProject -FolderPath $($destinationFile.Directory.FullName) | ForEach-Object {
		$fsProjectFilePath = $_
		Add-PSMDFsFileToFsProject -FsFilePath $destPath -FsProjectPath $fsProjectFilePath
	}
}
#End: Support for adding *fs file to F# project
[...]

The implementation of the functions Find-PsmdFsProject and Add-PSMDFsFileToFsProject follows:

Find-PsmdFsProject

function Find-PsmdFsProject
{
    <#
        .SYNOPSIS
        Find F# nearest project file(s) in current directory or in parent directories.
        
        .DESCRIPTION
        Find F# nearest project file(s) in current directory or in parent directories.

        .PARAMETER FolderPath
        Full path to the folder to search
        
        .EXAMPLE
        Write-Host Find neares F# project file.
        Find-PsmdFsProject -FolderPath "C:\temp\Fsharp.Console.TestApp\Tests"

        .NOTES               
		Version:        1.0
		Author:         github/trondr
		Company:        github/trondr
		Repository:     https://github.com/trondr/PSModuleDevelopment.git
    
    #>
    param(
        [ValidateScript({ (Test-Path $_ -PathType 'Container') })]
        [string]
        $FolderPath
    )
    $projectFiles = Get-ChildItem -LiteralPath $FolderPath -Filter "*.fsproj" -File
    if($projectFiles.Length -eq 0)
    {
        #Did not find *.fsproj file(s) in current directory, continue recursively up the tree.
        $Directory = [System.IO.DirectoryInfo]::new($FolderPath)
        if($null -ne $Directory.Parent)
        {
            Find-PsmdFsProject -FolderPath $($Directory.Parent.FullName)
        }
    }
    else {
        #Found *.fsproj file(s) in current directory, stop the search and return the findings.
        $projectFiles | ForEach-Object{ Write-Output -InputObject $_.FullName}
    }
}

Add-PSMDFsFileToFsProject

function Add-PsmdFsFileToFsProject {
    <#
        .SYNOPSIS
        Adds a f# source file to a F# project.
        
        .DESCRIPTION
        Adds a f# source file to a F# project. Source file is inserted before Program.fs 
        if project is a console project. Otherwise source file is added to the end.

        .PARAMETER FsProjectPath
        Full path to the F# project file.

        .PARAMETER FsFilePath
        Full path to the F# source file.

        .EXAMPLE
        Write-Host Add F# source file to F# project
        $fsprojectFilePath = "C:\temp\Fsharp.Console.TestApp\Fsharp.Console.TestApp.fsproj"
        $fsFilePath = "C:\temp\Fsharp.Console.TestApp\Tests\ExampleTests2.fs"
        Add-PSMDFsFileToFsProject -FsProjectPath $fsprojectFilePath -FsFilePath $fsFilePath

        .NOTES               
		Version:        1.0
		Author:         github/trondr
		Company:        github/trondr
		Repository:     https://github.com/trondr/PSModuleDevelopment.git
    
    #>
    
    [CmdletBinding()]
    param (
        [ValidateScript({
            [string]$path = $_
            (Test-Path $path -PathType 'Leaf') -and ($path.EndsWith(".fsproj"))
        })]
        [string]
        $FsProjectPath,
        [ValidateScript({
            [string]$path = $_
            (Test-Path $path -PathType 'Leaf') -and ($path.EndsWith(".fs"))
        })]
        [string]
        $FsFilePath
    )
    
    begin {
        $fsProjectFileDirectoryPath = [System.IO.FileInfo]::new($FsProjectPath).Directory.FullName
        $fsFilePathRelativePath = $FsFilePath.Replace($fsProjectFileDirectoryPath,"").TrimStart([System.IO.Path]::DirectorySeparatorChar).Replace([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
        function Format-XML
        {
            param(
                [xml]$Xml, 
                $Indent=2
            )
            $StringWriter = New-Object System.IO.StringWriter
            $XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter
            $xmlWriter.Formatting = "indented"
            $xmlWriter.Indentation = $Indent
            $xml.WriteContentTo($XmlWriter)| Out-Null
            $XmlWriter.Flush()
            $StringWriter.Flush()
            Write-Output $StringWriter.ToString()
        }
    }
    
    process {
        [xml]$fsprojXmlDoc = Get-Content $FsProjectPath
        $itemGroups = $fsprojXmlDoc.SelectNodes("/Project/ItemGroup/Compile")
        if($itemGroups.Count -gt 0)
        {
            $allreadyExists = (($itemGroups.Include | Where-Object { ($_ -eq $fsFilePathRelativePath) }) | Measure-Object).Count -gt 0
            if($allreadyExists -eq $false)
            {
                #Fs file is not allready added, so add it.
                Write-Host "Adding file '$FsFilePath' to project '$FsProjectPath'." -ForegroundColor Green
                [System.Xml.XmlNode]$compileElement = $fsprojXmlDoc.CreateElement("Compile")
                ($compileElement.SetAttribute("Include",$fsFilePathRelativePath)) | Out-Null
                [System.Xml.XmlNode]$parentNode = $itemGroups[0].ParentNode
                [System.Xml.XmlNode]$lastChild = $parentNode.LastChild
                if ($parentNode.LastChild.Include -ieq "Program.fs")
                {
                    #Add the new Compile item before Program.fs).
                    ($parentNode.InsertBefore($compileElement, $lastChild)) | Out-Null
                }
                else {
                    #Add the new Compile item to the end of the list.
                    ($parentNode.AppendChild($compileElement)) | Out-Null
                }
                #Write changes back to fsproj file.
                (Format-XML -Xml $($fsprojXmlDoc.InnerXml) -Indent 4) | Set-Content -Path $fsprojectFilePath -Encoding utf8BOM | Out-Null
            }
            else {
                Write-Host "File '$FsFilePath' has allready been added to project '$FsProjectPath'." -ForegroundColor Yellow
            }
        }
        else {
            Write-Host "No ItemGroup with Compile items found in project file '$FsProjectPath'. There must be at least one Compile item in one ItemGroup." -ForegroundColor Yellow
        }
    }
    
    end {
        
    }
}
@FriedrichWeinmann
Copy link
Member

FriedrichWeinmann commented Oct 3, 2021

Heya,
thank for the suggestion!
Technically it's out of the scope of the system ... to be f# specific.
What is in-scope however is an extended support for integrated logic:
Currently, scriptblocks are all executed at the beginning of the entire template (not the specific file they may be assigned to).
The new version is going to support:

  • Start Script (before starting template)
  • Pre-File script
  • Post-File script
  • End Script (once template is done)
    Actually, the object model has already been updated, however I have yet to integrate it into the commands.
    It's definitely the number 1 feature on my dev list for the next version.

Could be a nifty idea to also support some kind of building-block system - template fragments? - that can be reused in multiple templates.
Or templates directly referencing templates ...

Anyway, future ramblings, the scriptblock updates are definitely coming soon(tm)

@trondr
Copy link
Author

trondr commented Oct 3, 2021

Thanks! Looking forward to the new version! From what you are saying I understand that in the new version I will be able to move any F# specific code to a start/post/pre script block in the specific F# template. Instead of hardcoding support for F# in the main engine like I did in the prototype. Using start/pre/post script seems much cleaner!

By the way will there be any context provided to the script blocks? Such as $outpath? And other parameter values?

I guess access to variables via Get-PSFConfig will allways be availble, but maybe script blocks could be resolved before execution so that any variables on the format þsomevariableþ in the script are expanded before script execution? This can possibly simplify coding of script blocks somewhat. But the same information could also be stored in an easy accessible context variable that the the script can extract on execution.

@FriedrichWeinmann
Copy link
Member

The scriptblocks will receive a hashtable with relevant context data as argument:
All will receive:

  • Template Name
  • Parameters
  • Root Path into which the template is written
  • Global Choice data (hoping to include multiple choice options, such as "Which License do you want to include?"

Additionally, the two scriptblocks that happen around individual files will also receive:

  • Information related to the file itself (path mostly)
  • Local Choice data

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants