SampleModule
                                
                                 SampleModule copied to clipboard
                                
                                    SampleModule copied to clipboard
                            
                            
                            
                        Merge Module Specs
The question is broadly:
- What and How to merge into single PSM1?
I can see the following challenges:
- Some Folder structures may be different
- Some people may add things to their psm1 stub
- Using statements needs to be gathered/unique'd and inserted at top of psm1
- Requires should be added in module manifest (except Admin)
- Classes should be imported in a certain order (where to define it?)
- Classes should be imported before functions
- Extra data should be merge-able (say if you provide a scriptblock? Thinking out loud)
- should the non-merged files be copied? and everything merged deleted, empty dir deleted as well?
Am I missing anything?
Default behaviour could append content of existing psm1 at the end of generated psm1. Tricky though, what might be in there.
Class ordering, might be easiest to default to alphabetical unless inheriting. Then discover (parse). Assuming classes are exposed to AST reasonably enough.
Non-merged files should be copied. Could be format files, configuration files, (about) help files, other arbitrary stuff that lives in the module folder. I'd say ignore empty directories.
On 8 April 2017 at 23:09, Gael [email protected] wrote:
The question is broadly:
- What and How to merge into single PSM1?
I can see the following challenges:
- Some Folder structures may be different
- Some people may add things to their psm1 stub
- Using statements needs to be gathered/unique'd and inserted at top of psm1
- Requires should be added in module manifest (except Admin)
- Classes should be imported in a certain order (where to define it?)
- Classes should be imported before functions
- Extra data should be merge-able (say if you provide a scriptblock? Thinking out loud)
- should the non-merged files be copied? and everything merged deleted, empty dir deleted as well?
Am I missing anything?
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/gaelcolas/SampleModule/issues/7, or mute the thread https://github.com/notifications/unsubscribe-auth/AMO6DtSocEZFJY86pRW_76Gyp0CjT-mdks5ruAWBgaJpZM4M32K4 .
I think Class ordering might be more tricky than this. If Class B inheriting A has a property of type C, the order should be C, A, B. I'd say defaulting to Alphabetical order is fine, but the user should be able to order those, maybe it'll be a parameter.
As I was thinking for the specs, I thought there are different merging behaviours based on different types, that'd be good to split in their own function somehow.
So you would have slightly different processing whether it's a Class, a Function, a scriptblock.
Also, for the merge function to be flexible enough, it could take the MergeItems as ordered parameters, with a default set, and map to Folders or files... here's a rough example:
[ordered]@{
    'Enums'   = 'Enum'
    'Classes' = 'class'
    'Private' = 'Function'
    'Public'  = 'Function'
    'InitializeModule.ps1' = 'Function'
    'InitializeModule' = [scriptblock]
}
Given this input, if the Key has no extension, and the value is anything but [scriptblock], assume it's a folder to process. The Value then tells you what 'behaviour' to use.
Early thoughts, just wanted to put them down as I'm unlikely to work on that today.
As soon as I wrote this, I realised that the value may need to be another dimension...
@{
    Enums = @{Type='Enum}
    Classes = @{Type='Classes';OrderList=@('C','A','B')}
    Private = @{Type='Function';}
    ...
}
It's a shame classes must / might be ordered. Otherwise it's essentially.
'enum*', [PSCustomObject]@{Name='class*'; Order={ }}, 'priv*', 'pub*' | Merge
All of those might include "using" which will have to be refined, no special per-file-type handling. The interior of merge can take care of that.
If Order is a script block it can be fed into Sort-Object.
I always have trouble making this shorter...
function Merge {
    param(
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String]$Name,
        [Parameter(ValueFromPipelineByPropertyName)]
        [ScriptBlock]$Order,
        [String]$Separator = "`n`n"
    )
    begin {
        $usingList = New-Object System.Collections.Generic.List[String]
        $merge = New-Object System.Text.StringBuilder
    }
    process {
        Get-ChildItem $Name -Filter *.ps1 -Recurse | Sort-Object $Order | ForEach-Object {
            $content = $_ | Get-Content | ForEach-Object {
                if ($_ -match '^using') {
                    $usingList.Add($_)
                } else {
                    $_.TrimEnd()
                }
            } | Out-String
            $null = $merge.AppendFormat('{0}{1}', $content.Trim(), $Separator)
        }
    }
    end {
        $null = $merge.Insert(0, ($usingList | Sort-Object | Get-Unique | Out-String))
    
        $merge.ToString()
    }
}
Then the InitializeModule thing which I've never seen outside of my own code :)
Much simpler, sounds good.
I do, albeit rarely, something similar, which is Creating a function named _ModuleName_Config (i.e. SampleModuleConfig), and add at the bottom of the PSM1 a call to that function, unless the Import-module had an hashtable in $Args[0] in which case I pass that Hashtable as params (and if it's a valid filepath, load config and use as params).
So I see a use case, but will look at that in later improvements ;)
Ordering classes:
One possible pattern that might be worth suggesting is that the files which define classes are simply prefixed with a number to denote import order (if such a thing is necessary). That way GCI will just get it right in the first place, no advanced processes required.
The numeric value will be discarded during merge because merge couldn't care less about names.
I've considered this idea, but it gets messy when you start having quite a few class. If you want your last created class to be loaded first, you have to re-number every file...
And then it's a bit less 'clean' in the Classes folder, and if you want test to, say make sure each class has a test file, you need to regex this prefix out...
Also, I currently use this 'trick' in your Merge function (compile branch, will merge soon):
 [PSCustomObject]@{Name='class*';order={@('class1','class2','class12','class11').indexOf($_.BaseName)}}
I struggle to do find shorter, but that does the trick for now...
It's overridden by project in the .build.ps1
Maybe we should have something similar for loading with a single source of truth...?
Lack of idea for now... :(
Merge and independent load are mutually exclusive in my mind at the moment. I'm inclined to say the content of the PSM1 in that circumstance is the end-users responsibility.
I've injected the class load order problem into one of my modules, I'm doing a very poor job of coming up with an alternative to ordering by file name.
The Sort-Object method is not easy to add to the script (no reasonable injection point). Maintaining another file is messy, adding something to indicate a dependency chain to the class file is messy.
It's not even reasonable to use AST to analyse the class, it won't parse if there's an undefined type.
Ho hum.
Class ordering (since I'm never quite happy) and as I have a growing pile of class-based DSC resources I thought I'd share the direction I decided on.
By default, items under a top-level folder (for example, 
If a top-level folder contains order.txt; order.txt is read in (as an array). Items are sorted by $order.IndexOf($_.BaseName) then Name. If the IndexOf method returns -1, the returned value is made.
With this, if order is present, any items listed are returned in that explicit order. Any unlisted files are returned at the end.
Example 1: Default
order.txt does not exist or is empty.
class1.ps1
class11.ps1
class12.ps1
class2.ps1
Example 2: Ordered
With order.txt containing:
class12
class11
Resultant order:
class12.ps1
class11.ps1
class1.ps1
class2.ps1
The code for this is not particularly pretty, however since I already have a function to get the items extending supporting code feels like less of an issue.
The code I use to implement this ordering approach can be found here:
https://github.com/indented-automation/Indented.Build/blob/master/Indented.Build/public/Get-BuildItem.ps1