SampleModule icon indicating copy to clipboard operation
SampleModule copied to clipboard

Merge Module Specs

Open gaelcolas opened this issue 8 years ago • 11 comments

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?

gaelcolas avatar Apr 08 '17 22:04 gaelcolas

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 .

indented-automation avatar Apr 08 '17 23:04 indented-automation

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.

gaelcolas avatar Apr 09 '17 08:04 gaelcolas

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';}
    ...
}

gaelcolas avatar Apr 09 '17 08:04 gaelcolas

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 :)

indented-automation avatar Apr 09 '17 09:04 indented-automation

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 ;)

gaelcolas avatar Apr 09 '17 19:04 gaelcolas

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.

indented-automation avatar Apr 12 '17 14:04 indented-automation

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...

gaelcolas avatar Apr 12 '17 19:04 gaelcolas

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... :(

gaelcolas avatar Apr 12 '17 19:04 gaelcolas

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.

indented-automation avatar Apr 13 '17 08:04 indented-automation

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.

indented-automation avatar Apr 13 '17 10:04 indented-automation

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, \public) are returned in the order Get-ChildItem emits.

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

indented-automation avatar Jun 27 '17 12:06 indented-automation