diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..c44c24ff --- /dev/null +++ b/.editorconfig @@ -0,0 +1,244 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Code Actions #### + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_prefer_system_hash_code = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_prefer_system_threading_lock = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = none +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/Build/Build-Module.ps1 b/Build/Build-Module.ps1 index 233e0d2e..d82a7b16 100644 --- a/Build/Build-Module.ps1 +++ b/Build/Build-Module.ps1 @@ -33,6 +33,7 @@ Build-Module -ModuleName 'PSPublishModule' { New-ConfigurationModule -Type RequiredModule -Name 'powershellget' -Guid 'Auto' -Version 'Latest' New-ConfigurationModule -Type RequiredModule -Name 'PSScriptAnalyzer' -Guid 'Auto' -Version 'Latest' New-ConfigurationModule -Type RequiredModule -Name 'Pester' -Version Auto -Guid Auto + New-ConfigurationModule -Type RequiredModule -Name 'Microsoft.PowerShell.PSResourceGet' -Guid 'Auto' -Version 'Latest' # Add external module dependencies, using loop for simplicity New-ConfigurationModule -Type ExternalModule -Name @( diff --git a/Docs/Install-ModuleDocumentation.md b/Docs/Install-ModuleDocumentation.md new file mode 100644 index 00000000..efb25a88 --- /dev/null +++ b/Docs/Install-ModuleDocumentation.md @@ -0,0 +1,281 @@ +--- +external help file: PSPublishModule-help.xml +Module Name: PSPublishModule +online version: +schema: 2.0.0 +--- + +# Install-ModuleDocumentation + +## SYNOPSIS +Installs bundled module documentation/examples (Internals) to a chosen path. + +## SYNTAX + +### ByName (Default) +``` +Install-ModuleDocumentation [[-Name] ] [-RequiredVersion ] -Path [-Layout ] + [-OnExists ] [-CreateVersionSubfolder] [-Force] [-ListOnly] [-Open] [-NoIntro] + [-ProgressAction ] [-WhatIf] [-Confirm] [] +``` + +### ByModule +``` +Install-ModuleDocumentation [-Module ] [-RequiredVersion ] -Path + [-Layout ] [-OnExists ] [-CreateVersionSubfolder] [-Force] [-ListOnly] [-Open] [-NoIntro] + [-ProgressAction ] [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION +Copies the contents of a module's Internals folder (or the path defined in +PrivateData.PSData.PSPublishModuleDelivery) to a destination outside of +$env:PSModulePath, optionally including README/CHANGELOG from module root. + +## EXAMPLES + +### EXAMPLE 1 +``` +Install-ModuleDocumentation -Name AdminManager -Path 'C:\Docs' +``` + +### EXAMPLE 2 +``` +Get-Module -ListAvailable AdminManager | Install-ModuleDocumentation -Path 'D:\AM' +``` + +## PARAMETERS + +### -Name +Module name to install documentation for. +Accepts pipeline by value. + +```yaml +Type: String +Parameter Sets: ByName +Aliases: ModuleName + +Required: False +Position: 1 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Module +A PSModuleInfo object (e.g., from Get-Module -ListAvailable) to operate on directly. + +```yaml +Type: PSModuleInfo +Parameter Sets: ByModule +Aliases: InputObject, ModuleInfo + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -RequiredVersion +Specific version of the module to target. +If omitted, selects the highest available. + +```yaml +Type: Version +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path +Destination directory where the Internals content will be copied. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Layout +How to lay out the destination path: +- Direct: copy into \ +- Module: copy into \\\\\\ +- ModuleAndVersion (default): copy into \\\\\\\\\\\ + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: ModuleAndVersion +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -OnExists +What to do if the destination folder already exists: +- Merge (default): merge files/folders; overwrite files only when -Force is used +- Overwrite: remove the existing destination, then copy fresh +- Skip: do nothing and return the existing destination path +- Stop: throw an error + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: Merge +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CreateVersionSubfolder +When set (default), content is placed under '\\\\\\\\\\\'. +If disabled, content is copied directly into '\'. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force +Overwrite existing files. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ListOnly +Show what would be copied and where, without copying any files. +Returns the +computed destination path(s). +Use -Verbose for details. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Open +After a successful copy, open the README in the destination (if present). + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -NoIntro +{{ Fill NoIntro Description }} + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +## RELATED LINKS diff --git a/Docs/New-ConfigurationBuild.md b/Docs/New-ConfigurationBuild.md index 481ccdd5..3c49c8a5 100644 --- a/Docs/New-ConfigurationBuild.md +++ b/Docs/New-ConfigurationBuild.md @@ -14,17 +14,18 @@ Allows to configure build process for the module ``` New-ConfigurationBuild [-Enable] [-DeleteTargetModuleBeforeBuild] [-MergeModuleOnBuild] - [-MergeFunctionsFromApprovedModules] [-SignModule] [-DotSourceClasses] [-DotSourceLibraries] - [-SeparateFileLibraries] [-RefreshPSD1Only] [-UseWildcardForFunctions] [-LocalVersioning] - [-SkipBuiltinReplacements] [-DoNotAttemptToFixRelativePaths] [[-CertificateThumbprint] ] - [[-CertificatePFXPath] ] [[-CertificatePFXBase64] ] [[-CertificatePFXPassword] ] - [[-NETProjectPath] ] [[-NETConfiguration] ] [[-NETFramework] ] - [[-NETProjectName] ] [-NETExcludeMainLibrary] [[-NETExcludeLibraryFilter] ] - [[-NETIgnoreLibraryOnLoad] ] [[-NETBinaryModule] ] [-NETHandleAssemblyWithSameName] - [-NETLineByLineAddType] [-NETBinaryModuleCmdletScanDisabled] [-NETMergeLibraryDebugging] - [-NETResolveBinaryConflicts] [[-NETResolveBinaryConflictsName] ] [-NETBinaryModuleDocumentation] - [-NETDoNotCopyLibrariesRecursively] [[-NETSearchClass] ] [-NETHandleRuntimes] - [-ProgressAction ] [] + [-MergeFunctionsFromApprovedModules] [-SignModule] [-SignIncludeInternals] [-SignIncludeBinaries] + [-SignIncludeExe] [[-SignCustomInclude] ] [[-SignExcludePaths] ] [-DotSourceClasses] + [-DotSourceLibraries] [-SeparateFileLibraries] [-RefreshPSD1Only] [-UseWildcardForFunctions] + [-LocalVersioning] [-SkipBuiltinReplacements] [-DoNotAttemptToFixRelativePaths] + [[-CertificateThumbprint] ] [[-CertificatePFXPath] ] [[-CertificatePFXBase64] ] + [[-CertificatePFXPassword] ] [[-NETProjectPath] ] [[-NETConfiguration] ] + [[-NETFramework] ] [[-NETProjectName] ] [-NETExcludeMainLibrary] + [[-NETExcludeLibraryFilter] ] [[-NETIgnoreLibraryOnLoad] ] [[-NETBinaryModule] ] + [-NETHandleAssemblyWithSameName] [-NETLineByLineAddType] [-NETBinaryModuleCmdletScanDisabled] + [-NETMergeLibraryDebugging] [-NETResolveBinaryConflicts] [[-NETResolveBinaryConflictsName] ] + [-NETBinaryModuleDocumentation] [-NETDoNotCopyLibrariesRecursively] [[-NETSearchClass] ] + [-NETHandleRuntimes] [-ProgressAction ] [] ``` ## DESCRIPTION @@ -86,7 +87,8 @@ Accept wildcard characters: False ``` ### -MergeModuleOnBuild -Parameter description +Merge module on build. +Combines Private/Public/Classes/Enums into a single PSM1 and prepares PSD1 accordingly. ```yaml Type: SwitchParameter @@ -101,7 +103,7 @@ Accept wildcard characters: False ``` ### -MergeFunctionsFromApprovedModules -Parameter description +When merging, also include functions from ApprovedModules referenced by the module. ```yaml Type: SwitchParameter @@ -116,7 +118,11 @@ Accept wildcard characters: False ``` ### -SignModule -Parameter description +Enables code-signing for the built module output. +When enabled alone, only merged +scripts are signed (psm1/psd1/ps1) and Internals are excluded. +Use the SignInclude* +switches to opt-in to additional content. ```yaml Type: SwitchParameter @@ -130,8 +136,95 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -SignIncludeInternals +When signing is enabled, also sign scripts that reside under the Internals folder. +Default: disabled (Internals are skipped). + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SignIncludeBinaries +When signing is enabled, include binary files (e.g., .dll, .cat) in signing. +Default: disabled. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SignIncludeExe +When signing is enabled, include .exe files. +Default: disabled. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SignCustomInclude +Overrides the include patterns passed to the signer. +If provided, this replaces +the defaults entirely. +Example: '*.psm1','*.psd1','*.ps1','*.dll'. +Use with +caution; it disables the default safe set. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SignExcludePaths +Additional path substrings to exclude from signing (relative matches). +Example: +'Examples','SomeFolder'. +Internals are excluded by default unless +-SignIncludeInternals is specified. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -DotSourceClasses -Parameter description +Keep classes in a separate dot-sourced file instead of merging them into the main PSM1. ```yaml Type: SwitchParameter @@ -146,7 +239,7 @@ Accept wildcard characters: False ``` ### -DotSourceLibraries -Parameter description +Keep library-loading code in a separate dot-sourced file. ```yaml Type: SwitchParameter @@ -161,7 +254,7 @@ Accept wildcard characters: False ``` ### -SeparateFileLibraries -Parameter description +Write library-loading code into a distinct file and reference it via ScriptsToProcess/DotSource. ```yaml Type: SwitchParameter @@ -176,7 +269,7 @@ Accept wildcard characters: False ``` ### -RefreshPSD1Only -Parameter description +Only regenerate the manifest (PSD1) without rebuilding/merging other artifacts. ```yaml Type: SwitchParameter @@ -191,7 +284,8 @@ Accept wildcard characters: False ``` ### -UseWildcardForFunctions -Parameter description +Export all functions/aliases via wildcard in PSD1. +Useful for debugging non-merged builds. ```yaml Type: SwitchParameter @@ -206,7 +300,7 @@ Accept wildcard characters: False ``` ### -LocalVersioning -Parameter description +Use local versioning (bump PSD1 version on each build without querying PSGallery). ```yaml Type: SwitchParameter @@ -266,7 +360,7 @@ Accept wildcard characters: False ``` ### -CertificateThumbprint -Parameter description +Thumbprint of a code-signing certificate from the local cert store to sign module files. ```yaml Type: String @@ -274,14 +368,14 @@ Parameter Sets: (All) Aliases: Required: False -Position: 1 +Position: 3 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -CertificatePFXPath -Parameter description +Path to a PFX containing a code-signing certificate used for signing. ```yaml Type: String @@ -289,14 +383,14 @@ Parameter Sets: (All) Aliases: Required: False -Position: 2 +Position: 4 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -CertificatePFXBase64 -Parameter description +Base64 string of a PFX (e.g., provided via CI secrets) used for signing. ```yaml Type: String @@ -304,14 +398,14 @@ Parameter Sets: (All) Aliases: Required: False -Position: 3 +Position: 5 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -CertificatePFXPassword -Parameter description +Password for the PFX provided via -CertificatePFXPath or -CertificatePFXBase64. ```yaml Type: String @@ -319,7 +413,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 4 +Position: 6 Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -335,14 +429,14 @@ Parameter Sets: (All) Aliases: Required: False -Position: 5 +Position: 7 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -NETConfiguration -Parameter description +Build configuration for .NET projects ('Release' or 'Debug'). ```yaml Type: String @@ -350,14 +444,14 @@ Parameter Sets: (All) Aliases: Required: False -Position: 6 +Position: 8 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -NETFramework -Parameter description +Target frameworks for .NET build (e.g., 'netstandard2.0','net6.0'). ```yaml Type: String[] @@ -365,7 +459,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 7 +Position: 9 Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -381,7 +475,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 8 +Position: 10 Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -413,7 +507,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 9 +Position: 11 Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -430,7 +524,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 10 +Position: 12 Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -449,7 +543,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 11 +Position: 13 Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -543,7 +637,7 @@ Parameter Sets: (All) Aliases: ResolveBinaryConflictsName Required: False -Position: 12 +Position: 14 Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -590,7 +684,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 13 +Position: 15 Default value: None Accept pipeline input: False Accept wildcard characters: False diff --git a/Docs/New-ConfigurationDelivery.md b/Docs/New-ConfigurationDelivery.md new file mode 100644 index 00000000..5e347a75 --- /dev/null +++ b/Docs/New-ConfigurationDelivery.md @@ -0,0 +1,287 @@ +--- +external help file: PSPublishModule-help.xml +Module Name: PSPublishModule +online version: +schema: 2.0.0 +--- + +# New-ConfigurationDelivery + +## SYNOPSIS +Configures delivery metadata for bundling and installing internal docs/examples. + +## SYNTAX + +``` +New-ConfigurationDelivery [-Enable] [[-InternalsPath] ] [-IncludeRootReadme] [-IncludeRootChangelog] + [-IncludeRootLicense] [[-ReadmeDestination] ] [[-ChangelogDestination] ] + [[-LicenseDestination] ] [[-ImportantLinks] ] [[-IntroText] ] + [[-UpgradeText] ] [[-IntroFile] ] [[-UpgradeFile] ] + [-ProgressAction ] [] +``` + +## DESCRIPTION +Adds Delivery options to the PSPublishModule configuration so the build embeds +discovery metadata in the manifest (PrivateData.PSData.PSPublishModuleDelivery) +and so the Internals folder is easy to find post-install by helper cmdlets +such as Install-ModuleDocumentation. + +Typical usage is to call this in your Build\Manage-Module.ps1 alongside +New-ConfigurationInformation -IncludeAll 'Internals\' so that the Internals +directory is physically included in the shipped module and discoverable later. + +## EXAMPLES + +### EXAMPLE 1 +``` +New-ConfigurationDelivery -Enable -InternalsPath 'Internals' -IncludeRootReadme -IncludeRootChangelog +Emits Options.Delivery and causes PrivateData.PSData.PSPublishModuleDelivery to be written in the manifest. +``` + +### EXAMPLE 2 +``` +New-ConfigurationInformation -IncludeAll 'Internals\' +PS> New-ConfigurationDelivery -Enable +Minimal configuration that bundles Internals and exposes it to the installer. +``` + +## PARAMETERS + +### -Enable +Enables delivery metadata emission. +If not specified, nothing is emitted. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InternalsPath +Relative path inside the module that contains internal deliverables +(e.g. +'Internals'). +Defaults to 'Internals'. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 1 +Default value: Internals +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -IncludeRootReadme +Include module root README.* during installation (if present). + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -IncludeRootChangelog +Include module root CHANGELOG.* during installation (if present). + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -IncludeRootLicense +Include module root LICENSE.* during installation (if present). + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ReadmeDestination +Where to bundle README.* within the built module. +One of: Internals, Root, Both, None. +Default: Internals. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: Internals +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ChangelogDestination +Where to bundle CHANGELOG.* within the built module. +One of: Internals, Root, Both, None. +Default: Internals. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: Internals +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -LicenseDestination +Where to bundle LICENSE.* within the built module. +One of: Internals, Root, Both, None. +Default: Internals. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 4 +Default value: Internals +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ImportantLinks +One or more key/value pairs that represent important links to display to the user, +for example @{ Title = 'Docs'; Url = 'https://...' }. + +```yaml +Type: IDictionary[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 5 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -IntroText +Text lines shown to users after Install-ModuleDocumentation completes. +Accepts a string array. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 6 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -UpgradeText +Text lines with upgrade instructions shown when requested via Show-ModuleDocumentation -Upgrade. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 7 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -IntroFile +Relative path (within the module root) to a Markdown/text file to use as the Intro content. +If provided, it is preferred over IntroText for display and is also copied by +Install-ModuleDocumentation. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 8 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -UpgradeFile +Relative path (within the module root) to a Markdown/text file to use for Upgrade instructions. +If provided, it is preferred over UpgradeText for display and is also copied by +Install-ModuleDocumentation. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 9 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +This emits a Type 'Options' object under Options.Delivery so it works with the +existing New-PrepareStructure logic without further changes. + +## RELATED LINKS diff --git a/Docs/New-ConfigurationManifest.md b/Docs/New-ConfigurationManifest.md index 4779f8e2..b50c2cf5 100644 --- a/Docs/New-ConfigurationManifest.md +++ b/Docs/New-ConfigurationManifest.md @@ -16,7 +16,7 @@ Creates a new configuration manifest for a PowerShell module. New-ConfigurationManifest [-ModuleVersion] [[-CompatiblePSEditions] ] [-GUID] [-Author] [[-CompanyName] ] [[-Copyright] ] [[-Description] ] [[-PowerShellVersion] ] [[-Tags] ] [[-IconUri] ] [[-ProjectUri] ] - [[-DotNetFrameworkVersion] ] [[-LicenseUri] ] [[-Prerelease] ] + [[-DotNetFrameworkVersion] ] [[-LicenseUri] ] [[-Prerelease] ] [-RequireLicenseAcceptance] [[-FunctionsToExport] ] [[-CmdletsToExport] ] [[-AliasesToExport] ] [[-FormatsToProcess] ] [-ProgressAction ] [] ``` @@ -338,3 +338,19 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable This function helps in creating a standardized module manifest for PowerShell modules. ## RELATED LINKS +### -RequireLicenseAcceptance +Indicates the module requires explicit user acceptance of the license (PowerShellGet). +When enabled, ensure the module package contains a root `license.txt`. The builder +normalizes any `LICENSE`/`LICENSE.md` found into `license.txt` at the module root. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` diff --git a/Docs/Readme.md b/Docs/Readme.md index ea2b57a4..6741d82d 100644 --- a/Docs/Readme.md +++ b/Docs/Readme.md @@ -56,6 +56,9 @@ Locale: en-US ### [Initialize-ProjectManager](Initialize-ProjectManager.md) {{ Fill in the Description }} +### [Install-ModuleDocumentation](Install-ModuleDocumentation.md) +{{ Fill in the Description }} + ### [Invoke-DotNetReleaseBuild](Invoke-DotNetReleaseBuild.md) {{ Fill in the Description }} @@ -77,6 +80,9 @@ Locale: en-US ### [New-ConfigurationCompatibility](New-ConfigurationCompatibility.md) {{ Fill in the Description }} +### [New-ConfigurationDelivery](New-ConfigurationDelivery.md) +{{ Fill in the Description }} + ### [New-ConfigurationDocumentation](New-ConfigurationDocumentation.md) {{ Fill in the Description }} @@ -134,3 +140,6 @@ Locale: en-US ### [Set-ProjectVersion](Set-ProjectVersion.md) {{ Fill in the Description }} +### [Show-ModuleDocumentation](Show-ModuleDocumentation.md) +{{ Fill in the Description }} + diff --git a/Docs/Register-Certificate.md b/Docs/Register-Certificate.md index 225f1d2d..270b8e4d 100644 --- a/Docs/Register-Certificate.md +++ b/Docs/Register-Certificate.md @@ -8,26 +8,28 @@ schema: 2.0.0 # Register-Certificate ## SYNOPSIS -{{ Fill in the Synopsis }} +Signs files in a path using a code-signing certificate (Windows and PowerShell Core supported). ## SYNTAX ### PFX ``` Register-Certificate -CertificatePFX -Path [-TimeStampServer ] - [-IncludeChain ] [-Include ] [-HashAlgorithm ] [-ProgressAction ] - [-WhatIf] [-Confirm] [] + [-IncludeChain ] [-Include ] [-ExcludePath ] [-HashAlgorithm ] + [-ProgressAction ] [-WhatIf] [-Confirm] [] ``` ### Store ``` Register-Certificate -LocalStore [-Thumbprint ] -Path [-TimeStampServer ] - [-IncludeChain ] [-Include ] [-HashAlgorithm ] [-ProgressAction ] - [-WhatIf] [-Confirm] [] + [-IncludeChain ] [-Include ] [-ExcludePath ] [-HashAlgorithm ] + [-ProgressAction ] [-WhatIf] [-Confirm] [] ``` ## DESCRIPTION -{{ Fill in the Description }} +Locates a code-signing certificate (by thumbprint from the Windows cert store or from a PFX) +and applies Authenticode signatures to matching files under -Path. +On Windows, uses Set-AuthenticodeSignature; on non-Windows, uses OpenAuthenticode module if available. ## EXAMPLES @@ -41,7 +43,8 @@ PS C:\> {{ Add example code here }} ## PARAMETERS ### -CertificatePFX -{{ Fill CertificatePFX Description }} +A PFX file to use for signing. +Mutually exclusive with -LocalStore/-Thumbprint. ```yaml Type: String @@ -55,29 +58,28 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -Confirm -Prompts you for confirmation before running the cmdlet. +### -LocalStore +Certificate store to search ('LocalMachine' or 'CurrentUser') when using a certificate from the store. ```yaml -Type: SwitchParameter -Parameter Sets: (All) -Aliases: cf +Type: String +Parameter Sets: Store +Aliases: -Required: False +Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` -### -HashAlgorithm -{{ Fill HashAlgorithm Description }} +### -Thumbprint +Certificate thumbprint to select a single certificate from the chosen -LocalStore. ```yaml Type: String -Parameter Sets: (All) -Aliases: -Accepted values: SHA1, SHA256, SHA384, SHA512 +Parameter Sets: Store +Aliases: CertificateThumbprint Required: False Position: Named @@ -86,75 +88,78 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -Include -{{ Fill Include Description }} +### -Path +Root directory containing files to sign. ```yaml -Type: String[] +Type: String Parameter Sets: (All) Aliases: -Required: False +Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` -### -IncludeChain -{{ Fill IncludeChain Description }} +### -TimeStampServer +RFC3161 timestamp server URL. +Default: http://timestamp.digicert.com ```yaml Type: String Parameter Sets: (All) Aliases: -Accepted values: All, NotRoot, Signer Required: False Position: Named -Default value: None +Default value: Http://timestamp.digicert.com Accept pipeline input: False Accept wildcard characters: False ``` -### -LocalStore -{{ Fill LocalStore Description }} +### -IncludeChain +Which portion of the chain to include in the signature: All, NotRoot, or Signer. +Default: All. ```yaml Type: String -Parameter Sets: Store +Parameter Sets: (All) Aliases: -Accepted values: LocalMachine, CurrentUser -Required: True +Required: False Position: Named -Default value: None +Default value: All Accept pipeline input: False Accept wildcard characters: False ``` -### -Path -{{ Fill Path Description }} +### -Include +File patterns to include during signing. +Defaults to scripts only: '*.ps1','*.psd1','*.psm1'. +You may pass additional patterns if needed (e.g., '*.dll'). ```yaml -Type: String +Type: String[] Parameter Sets: (All) Aliases: -Required: True +Required: False Position: Named -Default value: None +Default value: @('*.ps1', '*.psd1', '*.psm1') Accept pipeline input: False Accept wildcard characters: False ``` -### -Thumbprint -{{ Fill Thumbprint Description }} +### -ExcludePath +One or more path substrings to exclude from signing. +Useful for skipping folders like 'Internals' unless opted-in. ```yaml -Type: String -Parameter Sets: Store -Aliases: CertificateThumbprint +Type: String[] +Parameter Sets: (All) +Aliases: Required: False Position: Named @@ -163,8 +168,9 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -TimeStampServer -{{ Fill TimeStampServer Description }} +### -HashAlgorithm +Hash algorithm for the signature. +Default: SHA256. ```yaml Type: String @@ -173,7 +179,7 @@ Aliases: Required: False Position: Named -Default value: None +Default value: SHA256 Accept pipeline input: False Accept wildcard characters: False ``` @@ -194,6 +200,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -ProgressAction {{ Fill ProgressAction Description }} @@ -214,11 +235,8 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## INPUTS -### None - ## OUTPUTS -### System.Object ## NOTES ## RELATED LINKS diff --git a/Docs/Show-ModuleDocumentation.md b/Docs/Show-ModuleDocumentation.md new file mode 100644 index 00000000..3eb15897 --- /dev/null +++ b/Docs/Show-ModuleDocumentation.md @@ -0,0 +1,302 @@ +--- +external help file: PSPublishModule-help.xml +Module Name: PSPublishModule +online version: +schema: 2.0.0 +--- + +# Show-ModuleDocumentation + +## SYNOPSIS +Shows README/CHANGELOG or a chosen document for a module, with a simple console view. + +## SYNTAX + +### ByName (Default) +``` +Show-ModuleDocumentation [[-Name] ] [-RequiredVersion ] [-Readme] [-Changelog] [-License] + [-Intro] [-Upgrade] [-File ] [-PreferInternals] [-List] [-Raw] [-Open] + [-ProgressAction ] [] +``` + +### ByModule +``` +Show-ModuleDocumentation [-Module ] [-RequiredVersion ] [-Readme] [-Changelog] + [-License] [-Intro] [-Upgrade] [-File ] [-PreferInternals] [-List] [-Raw] [-Open] + [-ProgressAction ] [] +``` + +### ByPath +``` +Show-ModuleDocumentation [-RequiredVersion ] [-DocsPath ] [-Readme] [-Changelog] [-License] + [-Intro] [-Upgrade] [-File ] [-PreferInternals] [-List] [-Raw] [-Open] + [-ProgressAction ] [] +``` + +## DESCRIPTION +Finds a module (by name or PSModuleInfo) and renders README/CHANGELOG from the module root +or from its Internals folder (as defined in PrivateData.PSData.PSPublishModuleDelivery). +You can also point directly to a docs folder via -DocsPath (e.g., output of Install-ModuleDocumentation). + +## EXAMPLES + +### EXAMPLE 1 +``` +Show-ModuleDocumentation -Name EFAdminManager -Readme +``` + +### EXAMPLE 2 +``` +Get-Module -ListAvailable EFAdminManager | Show-ModuleDocumentation -Changelog +``` + +### EXAMPLE 3 +``` +Show-ModuleDocumentation -DocsPath 'C:\Docs\EFAdminManager\3.0.0' -Readme -Open +``` + +## PARAMETERS + +### -Name +Module name to show documentation for. +Accepts pipeline by value. + +```yaml +Type: String +Parameter Sets: ByName +Aliases: ModuleName + +Required: False +Position: 1 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Module +A PSModuleInfo object (e.g., from Get-Module -ListAvailable) to operate on directly. + +```yaml +Type: PSModuleInfo +Parameter Sets: ByModule +Aliases: InputObject, ModuleInfo + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -RequiredVersion +Specific version of the module to target. +If omitted, selects the highest available. + +```yaml +Type: Version +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DocsPath +A folder that contains documentation to display (e.g., the destination created by Install-ModuleDocumentation). +When provided, the cmdlet does not look up the module and shows docs from this folder. + +```yaml +Type: String +Parameter Sets: ByPath +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Readme +Show README*. +If both root and Internals copies exist, the root copy is preferred unless -PreferInternals is set. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Changelog +Show CHANGELOG*. +If both root and Internals copies exist, the root copy is preferred unless -PreferInternals is set. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -License +Show LICENSE (prefers root `license.txt`). Falls back to any `LICENSE*` file found in root or Internals. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Intro +Show introduction. Prefers `IntroFile` content from delivery metadata; otherwise prints `IntroText` lines. +If neither is present, falls back to README. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Upgrade +Show upgrade instructions. Prefers `UpgradeFile` content from delivery metadata; otherwise prints `UpgradeText` lines. +If neither is present, falls back to `UPGRADE*`/`UPGRADING*`/`MIGRATION*` files if found. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -File +Relative path to a specific file to display (relative to module root or Internals). +If rooted, used as-is. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PreferInternals +Prefer the Internals copy of README/CHANGELOG when both exist. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -List +List available README/CHANGELOG files found (root and Internals) instead of displaying content. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Raw +Output the raw file content (no styling). + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Open +Open the resolved file in the system default viewer instead of rendering in the console. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +## RELATED LINKS diff --git a/PSPublishModule.psd1 b/PSPublishModule.psd1 index 0a806ebc..0c5cd902 100644 --- a/PSPublishModule.psd1 +++ b/PSPublishModule.psd1 @@ -7,9 +7,9 @@ Copyright = '(c) 2011 - 2025 Przemyslaw Klys @ Evotec. All rights reserved.' Description = 'Simple project allowing preparing, managing, building and publishing modules to PowerShellGallery' DotNetFrameworkVersion = '4.5.2' - FunctionsToExport = @('Convert-ProjectEncoding', 'Convert-ProjectLineEnding', 'Export-CertificateForNuGet', 'Get-MissingFunctions', 'Get-ModuleInformation', 'Get-ModuleTestFailures', 'Get-PowerShellAssemblyMetadata', 'Get-PowerShellCompatibility', 'Get-ProjectConsistency', 'Get-ProjectEncoding', 'Get-ProjectLineEnding', 'Get-ProjectVersion', 'Initialize-PortableModule', 'Initialize-PortableScript', 'Initialize-ProjectManager', 'Invoke-DotNetReleaseBuild', 'Invoke-ModuleBuild', 'Invoke-ModuleTestSuite', 'New-ConfigurationArtefact', 'New-ConfigurationBuild', 'New-ConfigurationCommand', 'New-ConfigurationCompatibility', 'New-ConfigurationDocumentation', 'New-ConfigurationExecute', 'New-ConfigurationFileConsistency', 'New-ConfigurationFormat', 'New-ConfigurationImportModule', 'New-ConfigurationInformation', 'New-ConfigurationManifest', 'New-ConfigurationModule', 'New-ConfigurationModuleSkip', 'New-ConfigurationPlaceHolder', 'New-ConfigurationPublish', 'New-ConfigurationTest', 'Publish-GitHubReleaseAsset', 'Publish-NugetPackage', 'Register-Certificate', 'Remove-Comments', 'Remove-ProjectFiles', 'Send-GitHubRelease', 'Set-ProjectVersion') + FunctionsToExport = @('Convert-ProjectEncoding', 'Convert-ProjectLineEnding', 'Export-CertificateForNuGet', 'Get-MissingFunctions', 'Get-ModuleInformation', 'Get-ModuleTestFailures', 'Get-PowerShellAssemblyMetadata', 'Get-PowerShellCompatibility', 'Get-ProjectConsistency', 'Get-ProjectEncoding', 'Get-ProjectLineEnding', 'Get-ProjectVersion', 'Initialize-PortableModule', 'Initialize-PortableScript', 'Initialize-ProjectManager', 'Install-ModuleDocumentation', 'Invoke-DotNetReleaseBuild', 'Invoke-ModuleBuild', 'Invoke-ModuleTestSuite', 'New-ConfigurationArtefact', 'New-ConfigurationBuild', 'New-ConfigurationCommand', 'New-ConfigurationCompatibility', 'New-ConfigurationDelivery', 'New-ConfigurationDocumentation', 'New-ConfigurationExecute', 'New-ConfigurationFileConsistency', 'New-ConfigurationFormat', 'New-ConfigurationImportModule', 'New-ConfigurationInformation', 'New-ConfigurationManifest', 'New-ConfigurationModule', 'New-ConfigurationModuleSkip', 'New-ConfigurationPlaceHolder', 'New-ConfigurationPublish', 'New-ConfigurationTest', 'Publish-GitHubReleaseAsset', 'Publish-NugetPackage', 'Register-Certificate', 'Remove-Comments', 'Remove-ProjectFiles', 'Send-GitHubRelease', 'Set-ProjectVersion', 'Show-ModuleDocumentation') GUID = 'eb76426a-1992-40a5-82cd-6480f883ef4d' - ModuleVersion = '2.0.25' + ModuleVersion = '2.0.26' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ @@ -31,6 +31,10 @@ Guid = 'a699dea5-2c73-4616-a270-1f7abb777e71' ModuleName = 'Pester' ModuleVersion = '5.7.1' + }, @{ + Guid = 'e4e0bda1-0703-44a5-b70d-8fe704cd0643' + ModuleName = 'Microsoft.PowerShell.PSResourceGet' + ModuleVersion = '1.1.1' }, 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Archive', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Security') RootModule = 'PSPublishModule.psm1' } \ No newline at end of file diff --git a/Private/Copy-PSPDirectoryTree.ps1 b/Private/Copy-PSPDirectoryTree.ps1 new file mode 100644 index 00000000..a5d75862 --- /dev/null +++ b/Private/Copy-PSPDirectoryTree.ps1 @@ -0,0 +1,27 @@ +function Copy-PSPDirectoryTree { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string] $Source, + [Parameter(Mandatory)][string] $Destination, + [switch] $Overwrite + ) + if (-not (Test-Path -LiteralPath $Source)) { return } + if (-not (Test-Path -LiteralPath $Destination)) { + New-Item -ItemType Directory -Path $Destination -Force | Out-Null + } + $items = Get-ChildItem -LiteralPath $Source -Force -ErrorAction SilentlyContinue + foreach ($item in $items) { + $target = Join-Path $Destination $item.Name + if ($item.PSIsContainer) { + Copy-PSPDirectoryTree -Source $item.FullName -Destination $target -Overwrite:$Overwrite.IsPresent + } else { + try { + Copy-Item -LiteralPath $item.FullName -Destination $target -Force:$Overwrite.IsPresent -ErrorAction Stop + } catch { + if (-not (Test-Path -LiteralPath $target)) { throw } + # If not overwriting and file exists, skip silently + } + } + } +} + diff --git a/Private/New-PersonalManifest.ps1 b/Private/New-PersonalManifest.ps1 index e307e416..241072cd 100644 --- a/Private/New-PersonalManifest.ps1 +++ b/Private/New-PersonalManifest.ps1 @@ -99,7 +99,7 @@ function New-PersonalManifest { if ($Data.Path) { $Data.Remove('Path') } - $ValidateEntriesPrivateData = @('Tags', 'LicenseUri', 'ProjectURI', 'IconUri', 'ReleaseNotes', 'Prerelease', 'RequireLicenseAcceptance', 'ExternalModuleDependencies') + $ValidateEntriesPrivateData = @('Tags', 'LicenseUri', 'ProjectURI', 'IconUri', 'ReleaseNotes', 'Prerelease', 'RequireLicenseAcceptance', 'ExternalModuleDependencies', 'PSPublishModuleDelivery') foreach ($Entry in [string[]] $Data.Keys) { if ($Entry -in $ValidateEntriesPrivateData) { $Data.PrivateData.PSData.$Entry = $Data.$Entry @@ -124,6 +124,10 @@ function New-PersonalManifest { if ($Configuration.Steps.PublishModule.Prerelease) { $Data.PrivateData.PSData.Prerelease = $Configuration.Steps.PublishModule.Prerelease } + # Emit delivery metadata when provided + if ($Configuration.Options.Delivery -and $Configuration.Options.Delivery.Enable) { + $Data.PrivateData.PSData.PSPublishModuleDelivery = $Configuration.Options.Delivery + } if ($TemporaryManifest.ExternalModuleDependencies) { # Add External Module Dependencies $Data.PrivateData.PSData.ExternalModuleDependencies = $TemporaryManifest.ExternalModuleDependencies @@ -164,4 +168,4 @@ function New-PersonalManifest { } $Data | Export-PSData -DataFile $ManifestPath -Sort } -} \ No newline at end of file +} diff --git a/Private/Resolve-DocFile.ps1 b/Private/Resolve-DocFile.ps1 new file mode 100644 index 00000000..18798f9f --- /dev/null +++ b/Private/Resolve-DocFile.ps1 @@ -0,0 +1,29 @@ +function Resolve-DocFile { + [CmdletBinding()] + param( + [Parameter(Mandatory)][ValidateSet('README','CHANGELOG','LICENSE','UPGRADE')] [string] $Kind, + [string] $RootBase, + [string] $InternalsBase, + [switch] $PreferInternals + ) + $root = $null; $intern = $null + $patterns = @() + switch ($Kind) { + 'README' { $patterns = @('README*') } + 'CHANGELOG' { $patterns = @('CHANGELOG*') } + 'LICENSE' { $patterns = @('LICENSE*','license.txt') } + 'UPGRADE' { $patterns = @('UPGRADE*','UPGRADING*','MIGRATION*') } + } + if ($RootBase) { + foreach ($p in $patterns) { + if (-not $root) { $root = Get-ChildItem -LiteralPath $RootBase -Filter $p -File -ErrorAction SilentlyContinue | Select-Object -First 1 } + } + } + if ($InternalsBase) { + foreach ($p in $patterns) { + if (-not $intern) { $intern = Get-ChildItem -LiteralPath $InternalsBase -Filter $p -File -ErrorAction SilentlyContinue | Select-Object -First 1 } + } + } + if ($PreferInternals) { if ($intern) { return $intern } else { return $root } } + else { if ($root) { return $root } else { return $intern } } +} diff --git a/Private/Start-ArtefactsBuilding.ps1 b/Private/Start-ArtefactsBuilding.ps1 index ab7713eb..cbbc10e8 100644 --- a/Private/Start-ArtefactsBuilding.ps1 +++ b/Private/Start-ArtefactsBuilding.ps1 @@ -36,8 +36,8 @@ return } - $ModuleName = $Configuration.Information.ModuleName - $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion + $ModuleName = $Configuration.Information.ModuleName + $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion # Lets replace variables user may have used in paths $FullProjectPath = Initialize-ReplacePath -ReplacementPath $FullProjectPath -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration @@ -59,6 +59,7 @@ # default values $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Type) } + # Destination for the main module should be the artefact Path (not the modules path) if ($Artefact.RequiredModules.ModulesPath) { $DirectPathForPrimaryModule = $Artefact.RequiredModules.ModulesPath } elseif ($Artefact.RequiredModules.Path) { @@ -68,6 +69,7 @@ } else { $DirectPathForPrimaryModule = $FolderPathReleases } + # Destination for required modules is taken from RequiredModules.* or falls back to artefact Path if ($Artefact.RequiredModules.Path) { $DirectPathForRequiredModules = $Artefact.RequiredModules.Path } elseif ($Artefact.RequiredModules.ModulesPath) { @@ -152,4 +154,4 @@ } } } -ColorBefore Yellow -ColorTime Yellow -Color Yellow -} \ No newline at end of file +} diff --git a/Private/Start-ModuleMerging.ps1 b/Private/Start-ModuleMerging.ps1 index 76fdd0ca..226363b9 100644 --- a/Private/Start-ModuleMerging.ps1 +++ b/Private/Start-ModuleMerging.ps1 @@ -29,6 +29,10 @@ } $LinkDirectoriesWithSupportFiles = $LinkDirectories | Where-Object { $_ -notin $CompareWorkaround } + # Additional guard: do not stage merge directories or their subpaths (Private/Public/Classes/Enums) + $LinkDirectoriesWithSupportFiles = $LinkDirectoriesWithSupportFiles | Where-Object { + $_ -notlike 'Private*' -and $_ -notlike 'Public*' -and $_ -notlike 'Classes*' -and $_ -notlike 'Enums*' + } foreach ($Directory in $LinkDirectoriesWithSupportFiles) { $Dir = [System.IO.Path]::Combine($FullModuleTemporaryPath, "$Directory") Add-Directory -Directory $Dir @@ -43,6 +47,26 @@ $FilesToLink = $LinkPrivatePublicFiles | Where-Object { $_ -notlike '*.ps1' -and $_ -notlike '*.psd1' } Copy-InternalFiles -LinkFiles $FilesToLink -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath + # Copy non-merge PS1 files (e.g., Internals\Scripts\*.ps1) into the final module + foreach ($supportDir in $LinkDirectoriesWithSupportFiles) { + $dirTrim = $supportDir.TrimEnd([char]'/',[char]'\') + if ($dirTrim -notlike 'Internals*') { continue } + $glob = [System.IO.Path]::Combine($FullProjectPath, $dirTrim, '*.ps1') + $files = if ($PSEdition -eq 'Core') { + Get-ChildItem -Path $glob -ErrorAction SilentlyContinue -Recurse -FollowSymlink + } else { + Get-ChildItem -Path $glob -ErrorAction SilentlyContinue -Recurse + } + foreach ($file in $files) { + $rel = (Resolve-Path -LiteralPath $file.FullName -Relative) + if ($null -eq $IsWindows -or $IsWindows -eq $true) { $rel = $rel.Replace('.\\','') } else { $rel = $rel.Replace('./','') } + $relDir = [System.IO.Path]::GetDirectoryName($rel) + if ($relDir) { Add-Directory -Directory ([System.IO.Path]::Combine($FullModuleTemporaryPath, $relDir)) } + Copy-InternalFiles -LinkFiles @($rel) -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath + } + } + + if ($Configuration.Information.LibrariesStandard) { # User provided option, we don't care } elseif ($Configuration.Information.LibrariesCore -and $Configuration.Information.LibrariesDefault) { @@ -159,6 +183,77 @@ return $false } + # If Delivery metadata is enabled, stage root README/CHANGELOG to requested destinations + if ($Configuration.Options -and $Configuration.Options.Delivery -and $Configuration.Options.Delivery.Enable) { + $internalsRel = if ($Configuration.Options.Delivery.InternalsPath) { [string]$Configuration.Options.Delivery.InternalsPath } else { 'Internals' } + if ($null -eq $IsWindows -or $IsWindows -eq $true) { + $internalsRel = $internalsRel.Replace('/', '\') + } else { + $internalsRel = $internalsRel.Replace('\\', '/') + } + $destInternals = [System.IO.Path]::Combine($FullModuleTemporaryPath, $internalsRel) + Add-Directory -Directory $destInternals + + $rootFiles = Get-ChildItem -Path $FullProjectPath -File -ErrorAction SilentlyContinue + $readmeDest = if ($Configuration.Options.Delivery.ReadmeDestination) { $Configuration.Options.Delivery.ReadmeDestination } else { 'Internals' } + $chlogDest = if ($Configuration.Options.Delivery.ChangelogDestination) { $Configuration.Options.Delivery.ChangelogDestination } else { 'Internals' } + $licDest = if ($Configuration.Options.Delivery.LicenseDestination) { $Configuration.Options.Delivery.LicenseDestination } else { 'Internals' } + + if ($rootFiles) { + foreach ($rf in $rootFiles) { + if ($rf.Name -like 'README*') { + switch ($readmeDest) { + 'Internals' { Copy-Item -LiteralPath $rf.FullName -Destination $destInternals -Force -ErrorAction SilentlyContinue } + 'Root' { Copy-Item -LiteralPath $rf.FullName -Destination (Join-Path $FullModuleTemporaryPath $rf.Name) -Force -ErrorAction SilentlyContinue } + 'Both' { + Copy-Item -LiteralPath $rf.FullName -Destination $destInternals -Force -ErrorAction SilentlyContinue + Copy-Item -LiteralPath $rf.FullName -Destination (Join-Path $FullModuleTemporaryPath $rf.Name) -Force -ErrorAction SilentlyContinue + } + } + } elseif ($rf.Name -like 'CHANGELOG*') { + switch ($chlogDest) { + 'Internals' { Copy-Item -LiteralPath $rf.FullName -Destination $destInternals -Force -ErrorAction SilentlyContinue } + 'Root' { Copy-Item -LiteralPath $rf.FullName -Destination (Join-Path $FullModuleTemporaryPath $rf.Name) -Force -ErrorAction SilentlyContinue } + 'Both' { + Copy-Item -LiteralPath $rf.FullName -Destination $destInternals -Force -ErrorAction SilentlyContinue + Copy-Item -LiteralPath $rf.FullName -Destination (Join-Path $FullModuleTemporaryPath $rf.Name) -Force -ErrorAction SilentlyContinue + } + } + } elseif ($rf.Name -like 'LICENSE*') { + switch ($licDest) { + 'Internals' { + Copy-Item -LiteralPath $rf.FullName -Destination (Join-Path $destInternals 'license.txt') -Force -ErrorAction SilentlyContinue + } + 'Root' { + Copy-Item -LiteralPath $rf.FullName -Destination (Join-Path $FullModuleTemporaryPath 'license.txt') -Force -ErrorAction SilentlyContinue + } + 'Both' { + Copy-Item -LiteralPath $rf.FullName -Destination (Join-Path $destInternals 'license.txt') -Force -ErrorAction SilentlyContinue + Copy-Item -LiteralPath $rf.FullName -Destination (Join-Path $FullModuleTemporaryPath 'license.txt') -Force -ErrorAction SilentlyContinue + } + } + } + } + # Enforce root license.txt when RequireLicenseAcceptance is true + $requireAccept = $false + if ($Configuration.Information.Manifest.RequireLicenseAcceptance) { $requireAccept = $true } + if ($requireAccept) { + # If no license.txt present at root, but license exists under rootFiles or Internals, ensure root license.txt + $existingRootLicense = Join-Path $FullModuleTemporaryPath 'license.txt' + if (-not (Test-Path -LiteralPath $existingRootLicense)) { + $sourceLic = ($rootFiles | Where-Object { $_.Name -like 'LICENSE*' } | Select-Object -First 1) + if (-not $sourceLic -and (Test-Path -LiteralPath $destInternals)) { + $cand = Get-ChildItem -LiteralPath $destInternals -Filter 'LICENSE*' -File -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($cand) { $sourceLic = $cand } + } + if ($sourceLic) { + Copy-Item -LiteralPath $sourceLic.FullName -Destination $existingRootLicense -Force -ErrorAction SilentlyContinue + } + } + } + } + } + if ($Configuration.Steps.BuildModule.CreateFileCatalog) { # Something is wrong here for folders other than root, need investigation $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew() @@ -195,4 +290,4 @@ Copy-InternalFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath Write-Text -End -Time $LinkingFilesTime } -} \ No newline at end of file +} diff --git a/Private/Start-ModuleSigning.ps1 b/Private/Start-ModuleSigning.ps1 index 543d7362..5561dad8 100644 --- a/Private/Start-ModuleSigning.ps1 +++ b/Private/Start-ModuleSigning.ps1 @@ -11,7 +11,7 @@ WarningVariable = 'Warnings' LocalStore = 'CurrentUser' Path = $FullModuleTemporaryPath - Include = @('*.ps1', '*.psd1', '*.psm1', '*.dll', '*.cat') + # Include list will be determined below TimeStampServer = 'http://timestamp.digicert.com' } @@ -37,6 +37,32 @@ $registerCertificateSplat.Thumbprint = $Configuration.Options.Signing.CertificateThumbprint } } + # Build include patterns with safe defaults (scripts only) + if ($Configuration.Options.Signing -and $Configuration.Options.Signing.Include) { + $registerCertificateSplat.Include = @($Configuration.Options.Signing.Include) + } else { + $include = @('*.ps1','*.psm1','*.psd1') + if ($Configuration.Options.Signing -and $Configuration.Options.Signing.IncludeBinaries) { + $include += @('*.dll','*.cat') + } + if ($Configuration.Options.Signing -and $Configuration.Options.Signing.IncludeExe) { + $include += @('*.exe') + } + $registerCertificateSplat.Include = $include + } + + # Exclude Internals unless explicitly enabled + $excludePaths = @() + if (-not ($Configuration.Options.Signing -and $Configuration.Options.Signing.IncludeInternals)) { + $excludePaths += 'Internals' + } + if ($Configuration.Options.Signing -and $Configuration.Options.Signing.ExcludePaths) { + $excludePaths += @($Configuration.Options.Signing.ExcludePaths) + } + if ($excludePaths.Count -gt 0) { + $registerCertificateSplat.ExcludePath = $excludePaths + } + [Array] $SignedFiles = Register-Certificate @registerCertificateSplat if ($Warnings) { foreach ($W in $Warnings) { @@ -63,4 +89,4 @@ } } -PreAppend Plus } -} \ No newline at end of file +} diff --git a/Private/Write-Heading.ps1 b/Private/Write-Heading.ps1 new file mode 100644 index 00000000..c9dcbc3f --- /dev/null +++ b/Private/Write-Heading.ps1 @@ -0,0 +1,8 @@ +function Write-Heading { + [CmdletBinding()] + param([Parameter(Mandatory)][string] $Text) + Write-Host ('=' * $Text.Length) -ForegroundColor DarkGray + Write-Host $Text -ForegroundColor Cyan + Write-Host ('=' * $Text.Length) -ForegroundColor DarkGray +} + diff --git a/Public/Get-MissingFunctions.ps1 b/Public/Get-MissingFunctions.ps1 index a43a93fd..d0e24f6a 100644 --- a/Public/Get-MissingFunctions.ps1 +++ b/Public/Get-MissingFunctions.ps1 @@ -1,4 +1,47 @@ function Get-MissingFunctions { + <# + .SYNOPSIS + Analyzes a script or scriptblock and reports functions it calls that are not present. + + .DESCRIPTION + Scans the provided file path or scriptblock, detects referenced commands, filters them down to + function calls, and returns a summary or the raw helper function definitions that can be inlined. + When -ApprovedModules is specified, helper definitions are only taken from those modules; otherwise + only the list is returned. Use this to build self-contained scripts by discovering dependencies. + + .PARAMETER FilePath + Path to a script file to analyze for missing function dependencies. Alias: Path. + + .PARAMETER Code + ScriptBlock to analyze instead of a file. Alias: ScriptBlock. + + .PARAMETER Functions + Known function names to treat as already available (exclude from missing list). + + .PARAMETER Summary + Return only a flattened summary list of functions used (objects with Name/Source), not inlined definitions. + + .PARAMETER SummaryWithCommands + Return a hashtable with Summary (names), SummaryFiltered (objects), and Functions (inlineable text). + + .PARAMETER ApprovedModules + Module names that are allowed sources for pulling inline helper function definitions. + + .PARAMETER IgnoreFunctions + Function names to ignore when computing the missing set. + + .EXAMPLE + Get-MissingFunctions -FilePath .\Build\Manage-Module.ps1 -Summary + Returns a list of functions used by the script. + + .EXAMPLE + $sb = { Invoke-ModuleBuild -ModuleName 'MyModule' } + Get-MissingFunctions -Code $sb -SummaryWithCommands -ApprovedModules 'PSSharedGoods','PSPublishModule' + Returns a hashtable with a summary and inlineable helper definitions sourced from approved modules. + + .NOTES + Use with Initialize-PortableScript to emit a self-contained version of a script. + #> [CmdletBinding()] param( [alias('Path')][string] $FilePath, @@ -84,4 +127,4 @@ } else { return $FunctionsOutput } -} \ No newline at end of file +} diff --git a/Public/Initialize-PortableModule.ps1 b/Public/Initialize-PortableModule.ps1 index fe27640d..b1e684f2 100644 --- a/Public/Initialize-PortableModule.ps1 +++ b/Public/Initialize-PortableModule.ps1 @@ -1,4 +1,37 @@ function Initialize-PortableModule { + <# + .SYNOPSIS + Downloads and/or imports a module and its dependencies as a portable set. + + .DESCRIPTION + Assists in preparing a portable environment for a module by either downloading it (plus dependencies) + to a specified path, importing those modules from disk, or both. Generates a convenience script that + imports all discovered module manifests when -Download is used. + + .PARAMETER Name + Name of the module to download/import. Alias: ModuleName. + + .PARAMETER Path + Filesystem path where modules are saved or imported from. Defaults to the current script root. + + .PARAMETER Download + Save the module and its dependencies to the specified path. + + .PARAMETER Import + Import the module and its dependencies from the specified path. + + .EXAMPLE + Initialize-PortableModule -Name 'EFAdminManager' -Path 'C:\Portable' -Download + Saves the module and its dependencies into C:\Portable. + + .EXAMPLE + Initialize-PortableModule -Name 'EFAdminManager' -Path 'C:\Portable' -Import + Imports the module and its dependencies from C:\Portable. + + .EXAMPLE + Initialize-PortableModule -Name 'EFAdminManager' -Path 'C:\Portable' -Download -Import + Saves and then imports the module and dependencies, and creates a helper script. + #> [CmdletBinding()] param( [alias('ModuleName')][string] $Name, @@ -78,4 +111,4 @@ Import-Module $_ -Verbose:$false -Force } } -} \ No newline at end of file +} diff --git a/Public/Initialize-PortableScript.ps1 b/Public/Initialize-PortableScript.ps1 index 33a42423..5f5a9be5 100644 --- a/Public/Initialize-PortableScript.ps1 +++ b/Public/Initialize-PortableScript.ps1 @@ -1,4 +1,29 @@ function Initialize-PortableScript { + <# + .SYNOPSIS + Produces a self-contained script by inlining missing helper function definitions. + + .DESCRIPTION + Analyzes the input script for function calls not present in the script itself, pulls helper + definitions from approved modules, and writes a combined output file that begins with those + helper definitions followed by the original script content. Useful for portable delivery. + + .PARAMETER FilePath + Path to the source script to analyze and convert. + + .PARAMETER OutputPath + Destination path for the generated self-contained script. + + .PARAMETER ApprovedModules + Module names that are permitted sources for inlined helper functions. + + .EXAMPLE + Initialize-PortableScript -FilePath .\Scripts\Do-Work.ps1 -OutputPath .\Artefacts\Do-Work.Portable.ps1 -ApprovedModules PSSharedGoods + Generates a portable script with helper functions inlined at the top. + + .NOTES + Output encoding is UTF8BOM on PS 7+, UTF8 on PS 5.1 for compatibility. + #> [cmdletBinding()] param( [string] $FilePath, @@ -20,4 +45,4 @@ ) $FinalScript | Set-Content -LiteralPath $OutputPath -Encoding $Encoding $Output -} \ No newline at end of file +} diff --git a/Public/Install-ModuleDocumentation.ps1 b/Public/Install-ModuleDocumentation.ps1 new file mode 100644 index 00000000..cc33bb53 --- /dev/null +++ b/Public/Install-ModuleDocumentation.ps1 @@ -0,0 +1,287 @@ +function Install-ModuleDocumentation { + <# + .SYNOPSIS + Installs bundled module documentation/examples (Internals) to a chosen path. + + .DESCRIPTION + Copies the contents of a module's Internals folder (or the path defined in + PrivateData.PSData.PSPublishModuleDelivery) to a destination outside of + $env:PSModulePath, optionally including README/CHANGELOG from module root. + + .PARAMETER Name + Module name to install documentation for. Accepts pipeline by value. + + .PARAMETER RequiredVersion + Specific version of the module to target. If omitted, selects the highest available. + + .PARAMETER Module + A PSModuleInfo object (e.g., from Get-Module -ListAvailable) to operate on directly. + + .PARAMETER Path + Destination directory where the Internals content will be copied. + + .PARAMETER Layout + How to lay out the destination path: + - Direct: copy into + - Module: copy into \\ + - ModuleAndVersion (default): copy into \\\\ + + .PARAMETER OnExists + What to do if the destination folder already exists: + - Merge (default): merge files/folders; overwrite files only when -Force is used + - Overwrite: remove the existing destination, then copy fresh + - Skip: do nothing and return the existing destination path + - Stop: throw an error + + .PARAMETER CreateVersionSubfolder + When set (default), content is placed under '\\\\'. + If disabled, content is copied directly into ''. + + .PARAMETER Force + Overwrite existing files. + + .PARAMETER ListOnly + Show what would be copied and where, without copying any files. Returns the + computed destination path(s). Use -Verbose for details. + + .PARAMETER Open + After a successful copy, open the README in the destination (if present). + + .PARAMETER NoIntro + Suppress introductory notes and important links printed after installation. + + .EXAMPLE + Install-ModuleDocumentation -Name AdminManager -Path 'C:\Docs' + + .EXAMPLE + Get-Module -ListAvailable AdminManager | Install-ModuleDocumentation -Path 'D:\\AM' + Installs the highest available version of AdminManager to D:\AM\AdminManager\ + + .EXAMPLE + # Copy into Path\Name only, merge on re-run without overwriting existing files + Install-ModuleDocumentation -Name EFAdminManager -Path 'C:\Docs' -Layout Module + + .EXAMPLE + # Overwrite destination on re-run + Install-ModuleDocumentation -Name EFAdminManager -Path 'C:\Docs' -OnExists Overwrite + + .EXAMPLE + # Dry-run: show what would be copied and where + Install-ModuleDocumentation -Name EFAdminManager -Path 'C:\Docs' -ListOnly -Verbose + + .EXAMPLE + # Copy, suppress intro/links printing, and open README afterwards + Install-ModuleDocumentation -Name EFAdminManager -Path 'C:\Docs' -NoIntro -Open + #> + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='ByName', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('ModuleName')] + [string] $Name, + [Parameter(ParameterSetName='ByModule', ValueFromPipeline = $true)] + [Alias('InputObject','ModuleInfo')] + [System.Management.Automation.PSModuleInfo] $Module, + [version] $RequiredVersion, + [Parameter(Mandatory)] + [string] $Path, + [ValidateSet('Direct','Module','ModuleAndVersion')] + [string] $Layout = 'ModuleAndVersion', + [ValidateSet('Merge','Overwrite','Skip','Stop')] + [string] $OnExists = 'Merge', + [switch] $CreateVersionSubfolder, # legacy toggle: if bound and Layout not specified, maps to Direct/ModuleAndVersion + [switch] $Force, + [switch] $ListOnly, + [switch] $Open, + [switch] $NoIntro + ) + + begin { + # Use a generic list for performance/compat across PS5/PS7 + $resolvedTargets = [System.Collections.Generic.List[string]]::new() + } + process { + if ($PSCmdlet.ParameterSetName -eq 'ByName') { + if (-not $Name) { throw "Specify -Name or provide a module via the pipeline." } + $candidates = Get-Module -ListAvailable -Name $Name + if (-not $candidates) { throw "Module '$Name' not found." } + if ($RequiredVersion) { + $Module = $candidates | Where-Object { $_.Version -eq $RequiredVersion } | Sort-Object Version -Descending | Select-Object -First 1 + if (-not $Module) { throw "Module '$Name' with version $RequiredVersion not found." } + } else { + $Module = $candidates | Sort-Object Version -Descending | Select-Object -First 1 + } + } elseif ($PSCmdlet.ParameterSetName -eq 'ByModule') { + if (-not $Module) { throw "Pipeline didn't pass a PSModuleInfo object. Use -Name or pipe Get-Module output." } + if ($RequiredVersion -and $Module.Version -ne $RequiredVersion) { + # Try to resolve the exact version if requested differs from piped object + $resolved = Get-Module -ListAvailable -Name $Module.Name | Where-Object { $_.Version -eq $RequiredVersion } | Sort-Object Version -Descending | Select-Object -First 1 + if ($resolved) { $Module = $resolved } else { throw "Module '$($Module.Name)' with version $RequiredVersion not found." } + } + } + + $manifestPath = Join-Path $Module.ModuleBase ("{0}.psd1" -f $Module.Name) + if (-not (Test-Path -LiteralPath $manifestPath)) { + throw "Manifest not found for module '$($Module.Name)' at '$manifestPath'." + } + + $manifest = Test-ModuleManifest -Path $manifestPath + $delivery = $manifest.PrivateData.PSData.PSPublishModuleDelivery + + $internalsRel = if ($delivery -and $delivery.InternalsPath) { [string]$delivery.InternalsPath } else { 'Internals' } + $includeReadme = if ($null -ne $delivery.IncludeRootReadme) { [bool]$delivery.IncludeRootReadme } else { $true } + $includeChlog = if ($null -ne $delivery.IncludeRootChangelog) { [bool]$delivery.IncludeRootChangelog } else { $true } + $includeLicense = if ($null -ne $delivery.IncludeRootLicense) { [bool]$delivery.IncludeRootLicense } else { $true } + + $internalsPath = Join-Path $Module.ModuleBase $internalsRel + if (-not (Test-Path -LiteralPath $internalsPath)) { + throw "Internals path '$internalsRel' not found under module base '$($Module.ModuleBase)'." + } + + # Back-compat: if legacy CreateVersionSubfolder was provided and Layout not changed, honor it + if ($PSBoundParameters.ContainsKey('CreateVersionSubfolder') -and -not $PSBoundParameters.ContainsKey('Layout')) { + $Layout = if ($CreateVersionSubfolder) { 'ModuleAndVersion' } else { 'Direct' } + } + + switch ($Layout) { + 'Direct' { $dest = $Path } + 'Module' { $dest = Join-Path $Path $Module.Name } + 'ModuleAndVersion' { $dest = Join-Path (Join-Path $Path $Module.Name) $Module.Version.ToString() } + } + + # If listing only, do not copy — just output the planned destination + if ($ListOnly) { + Write-Verbose "Would copy Internals from '$internalsPath' to '$dest' using Layout=$Layout, OnExists=$OnExists." + $resolvedTargets.Add($dest) + return + } + + if ($PSCmdlet.ShouldProcess("$internalsPath", "Copy to '$dest'")) { + $exists = Test-Path -LiteralPath $dest + if ($exists) { + switch ($OnExists) { + 'Skip' { $resolvedTargets.Add($dest); return } + 'Stop' { throw "Destination '$dest' already exists." } + 'Overwrite' { + Remove-Item -LiteralPath $dest -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path $dest -Force | Out-Null + Copy-PSPDirectoryTree -Source $internalsPath -Destination $dest -Overwrite:$true + } + 'Merge' { + Copy-PSPDirectoryTree -Source $internalsPath -Destination $dest -Overwrite:$Force.IsPresent + } + } + } else { + New-Item -ItemType Directory -Path $dest -Force | Out-Null + Copy-PSPDirectoryTree -Source $internalsPath -Destination $dest -Overwrite:$Force.IsPresent + } + + # Copy selected root files from module base in a PS5/PS7-safe, readable way + $rootFiles = @(Get-ChildItem -Path $Module.ModuleBase -File -ErrorAction SilentlyContinue) + + if ($includeReadme -and $rootFiles.Count -gt 0) { + foreach ($file in $rootFiles) { + if ($file.Name -like 'README*') { + try { Copy-Item -LiteralPath $file.FullName -Destination $dest -Force:$Force.IsPresent -ErrorAction Stop } catch { } + } + } + } + if ($includeChlog -and $rootFiles.Count -gt 0) { + foreach ($file in $rootFiles) { + if ($file.Name -like 'CHANGELOG*') { + try { Copy-Item -LiteralPath $file.FullName -Destination $dest -Force:$Force.IsPresent -ErrorAction Stop } catch { } + } + } + } + if ($includeLicense -and $rootFiles.Count -gt 0) { + foreach ($file in $rootFiles) { + if ($file.Name -like 'LICENSE*') { + try { Copy-Item -LiteralPath $file.FullName -Destination (Join-Path $dest 'license.txt') -Force:$Force.IsPresent -ErrorAction Stop } catch { } + } + } + } + + # Copy Intro/Upgrade files if explicitly provided in delivery metadata + if ($delivery) { + if ($delivery.IntroFile) { + $introSrc = Join-Path $Module.ModuleBase ([string]$delivery.IntroFile) + if (Test-Path -LiteralPath $introSrc) { + try { Copy-Item -LiteralPath $introSrc -Destination $dest -Force:$Force.IsPresent -ErrorAction Stop } catch { } + } + } + if ($delivery.UpgradeFile) { + $upgradeSrc = Join-Path $Module.ModuleBase ([string]$delivery.UpgradeFile) + if (Test-Path -LiteralPath $upgradeSrc) { + try { Copy-Item -LiteralPath $upgradeSrc -Destination $dest -Force:$Force.IsPresent -ErrorAction Stop } catch { } + } + } + } + if ($includeLicense -and $rootFiles.Count -gt 0) { + foreach ($file in $rootFiles) { + if ($file.Name -like 'LICENSE*') { + try { Copy-Item -LiteralPath $file.FullName -Destination $dest -Force:$Force.IsPresent -ErrorAction Stop } catch { } + } + } + } + + $resolvedTargets.Add($dest) + + # Intro and links (unless suppressed) + if (-not $NoIntro) { + $hasIntro = $false + if ($delivery) { + if ($delivery.IntroText) { + $hasIntro = $true + Write-Host '' + Write-Host 'Introduction:' -ForegroundColor Cyan + foreach ($line in [string[]]$delivery.IntroText) { Write-Host " $line" } + } + if ($delivery.IntroFile) { + $introDest = Join-Path $dest ([IO.Path]::GetFileName([string]$delivery.IntroFile)) + if (Test-Path -LiteralPath $introDest) { + $hasIntro = $true + Write-Host '' + Write-Host "Introduction (from $([IO.Path]::GetFileName($introDest))):" -ForegroundColor Cyan + try { Write-Host (Get-Content -LiteralPath $introDest -Raw -ErrorAction Stop) } catch {} + } + } + if ($delivery.ImportantLinks) { + Write-Host '' + Write-Host 'Links:' -ForegroundColor Cyan + foreach ($l in $delivery.ImportantLinks) { + try { + $title = if ($l.Title) { $l.Title } elseif ($l.Name) { $l.Name } else { '' } + $url = $l.Url + if ($title -and $url) { + Write-Host " - $title" + Write-Host " $url" + } elseif ($url) { + Write-Host " - $url" + } elseif ($title) { + Write-Host " - $title" + } + } catch {} + } + } + } + } + + # Optionally open README in destination + if ($Open) { + try { + $readme = Get-ChildItem -LiteralPath $dest -Filter 'README*' -File -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($readme) { + Start-Process -FilePath $readme.FullName | Out-Null + } else { + # If README not found, open the destination folder + Start-Process -FilePath $dest | Out-Null + } + } catch { + Write-Verbose "Open failed: $($_.Exception.Message)" + } + } + } + } + end { + if ($resolvedTargets.Count -gt 0) { $resolvedTargets | Select-Object -Unique } + } +} diff --git a/Public/Invoke-ModuleBuild.ps1 b/Public/Invoke-ModuleBuild.ps1 index cdef1b42..1f7cda28 100644 --- a/Public/Invoke-ModuleBuild.ps1 +++ b/Public/Invoke-ModuleBuild.ps1 @@ -47,19 +47,23 @@ function Invoke-ModuleBuild { Include all files in the Artefacts from given folders. Default are 'Images', 'Resources', 'Templates', 'Bin', 'Lib', 'Data' folders. .PARAMETER IncludeCustomCode - Parameter description + Optional scriptblock executed during staging that can add custom files/folders + to the build. Use helper functions like Add-Directory/Copy-Item to place extra + content into the temporary module structure before merge/packaging. .PARAMETER IncludeToArray - Parameter description + Advanced hashtable form for includes. Supports keys matching IncludeRoot, + IncludePS1, IncludeAll etc. Prefer the dedicated parameters unless you need + to pass a single structured object. .PARAMETER LibrariesCore - Parameter description + Alternate relative path for .NET Core-targeted libraries folder. Default: 'Lib/Core'. .PARAMETER LibrariesDefault - Parameter description + Alternate relative path for classic .NET Framework-targeted libraries folder. Default: 'Lib/Default'. .PARAMETER LibrariesStandard - Parameter description + Alternate relative path for .NET Standard-targeted libraries folder. Default: 'Lib/Standard'. .PARAMETER ExitCode Exit code to be returned to the caller. If not provided, it will not exit the script, but finish gracefully. @@ -234,4 +238,4 @@ function Invoke-ModuleBuild { Exit 1 } } -} \ No newline at end of file +} diff --git a/Public/New-ConfigurationBuild.ps1 b/Public/New-ConfigurationBuild.ps1 index 74d7fe90..788dbf8c 100644 --- a/Public/New-ConfigurationBuild.ps1 +++ b/Public/New-ConfigurationBuild.ps1 @@ -13,31 +13,54 @@ Delete target module before build .PARAMETER MergeModuleOnBuild - Parameter description + Merge module on build. Combines Private/Public/Classes/Enums into a single PSM1 and prepares PSD1 accordingly. .PARAMETER MergeFunctionsFromApprovedModules - Parameter description + When merging, also include functions from ApprovedModules referenced by the module. .PARAMETER SignModule - Parameter description + Enables code-signing for the built module output. When enabled alone, only merged + scripts are signed (psm1/psd1/ps1) and Internals are excluded. Use the SignInclude* + switches to opt-in to additional content. + + .PARAMETER SignIncludeInternals + When signing is enabled, also sign scripts that reside under the Internals folder. + Default: disabled (Internals are skipped). + + .PARAMETER SignIncludeBinaries + When signing is enabled, include binary files (e.g., .dll, .cat) in signing. + Default: disabled. + + .PARAMETER SignIncludeExe + When signing is enabled, include .exe files. Default: disabled. + + .PARAMETER SignCustomInclude + Overrides the include patterns passed to the signer. If provided, this replaces + the defaults entirely. Example: '*.psm1','*.psd1','*.ps1','*.dll'. Use with + caution; it disables the default safe set. + + .PARAMETER SignExcludePaths + Additional path substrings to exclude from signing (relative matches). Example: + 'Examples','SomeFolder'. Internals are excluded by default unless + -SignIncludeInternals is specified. .PARAMETER DotSourceClasses - Parameter description + Keep classes in a separate dot-sourced file instead of merging them into the main PSM1. .PARAMETER DotSourceLibraries - Parameter description + Keep library-loading code in a separate dot-sourced file. .PARAMETER SeparateFileLibraries - Parameter description + Write library-loading code into a distinct file and reference it via ScriptsToProcess/DotSource. .PARAMETER RefreshPSD1Only - Parameter description + Only regenerate the manifest (PSD1) without rebuilding/merging other artifacts. .PARAMETER UseWildcardForFunctions - Parameter description + Export all functions/aliases via wildcard in PSD1. Useful for debugging non-merged builds. .PARAMETER LocalVersioning - Parameter description + Use local versioning (bump PSD1 version on each build without querying PSGallery). .PARAMETER DoNotAttemptToFixRelativePaths Configures module builder to not replace $PSScriptRoot\..\ with $PSScriptRoot\ @@ -47,22 +70,22 @@ Best practice is to use $MyInvocation.MyCommand.Module.ModuleBase or similar instead of relative paths. .PARAMETER CertificateThumbprint - Parameter description + Thumbprint of a code-signing certificate from the local cert store to sign module files. .PARAMETER CertificatePFXPath - Parameter description + Path to a PFX containing a code-signing certificate used for signing. .PARAMETER CertificatePFXBase64 - Parameter description + Base64 string of a PFX (e.g., provided via CI secrets) used for signing. .PARAMETER CertificatePFXPassword - Parameter description + Password for the PFX provided via -CertificatePFXPath or -CertificatePFXBase64. .PARAMETER NETConfiguration - Parameter description + Build configuration for .NET projects ('Release' or 'Debug'). .PARAMETER NETFramework - Parameter description + Target frameworks for .NET build (e.g., 'netstandard2.0','net6.0'). .PARAMETER NETProjectPath Path to the project that you want to build. This is useful if it's not in Sources folder directly within module directory @@ -165,6 +188,11 @@ [switch] $MergeModuleOnBuild, [switch] $MergeFunctionsFromApprovedModules, [switch] $SignModule, + [switch] $SignIncludeInternals, + [switch] $SignIncludeBinaries, + [switch] $SignIncludeExe, + [string[]] $SignCustomInclude, + [string[]] $SignExcludePaths, [switch] $DotSourceClasses, [switch] $DotSourceLibraries, [switch] $SeparateFileLibraries, @@ -240,6 +268,20 @@ } } } + if ($PSBoundParameters.ContainsKey('SignIncludeInternals') -or $PSBoundParameters.ContainsKey('SignIncludeBinaries') -or $PSBoundParameters.ContainsKey('SignIncludeExe') -or $PSBoundParameters.ContainsKey('SignCustomInclude') -or $PSBoundParameters.ContainsKey('SignExcludePaths')) { + [ordered] @{ + Type = 'Options' + Options = [ordered] @{ + Signing = [ordered] @{ + IncludeInternals = $SignIncludeInternals.IsPresent + IncludeBinaries = $SignIncludeBinaries.IsPresent + IncludeExe = $SignIncludeExe.IsPresent + Include = $SignCustomInclude + ExcludePaths = $SignExcludePaths + } + } + } + } if ($PSBoundParameters.ContainsKey('DotSourceClasses')) { # only when there are classes [ordered] @{ diff --git a/Public/New-ConfigurationCommand.ps1 b/Public/New-ConfigurationCommand.ps1 index e478aa51..5d2493f1 100644 --- a/Public/New-ConfigurationCommand.ps1 +++ b/Public/New-ConfigurationCommand.ps1 @@ -1,4 +1,21 @@ function New-ConfigurationCommand { + <# + .SYNOPSIS + Defines a command import configuration for the build pipeline. + + .DESCRIPTION + Creates a configuration object that specifies a module and one or more command names + to reference during the build process (for discovery, linking, or documentation). + + .PARAMETER ModuleName + Name of the module that contains the commands. + + .PARAMETER CommandName + One or more command names to reference from the module. + + .EXAMPLE + New-ConfigurationCommand -ModuleName 'PSSharedGoods' -CommandName 'Write-Text','Remove-EmptyValue' + #> [CmdletBinding()] param( [string] $ModuleName, @@ -13,4 +30,4 @@ } } $Configuration -} \ No newline at end of file +} diff --git a/Public/New-ConfigurationDelivery.ps1 b/Public/New-ConfigurationDelivery.ps1 new file mode 100644 index 00000000..02f9e1fa --- /dev/null +++ b/Public/New-ConfigurationDelivery.ps1 @@ -0,0 +1,117 @@ +function New-ConfigurationDelivery { + <# + .SYNOPSIS + Configures delivery metadata for bundling and installing internal docs/examples. + + .DESCRIPTION + Adds Delivery options to the PSPublishModule configuration so the build embeds + discovery metadata in the manifest (PrivateData.PSData.PSPublishModuleDelivery) + and so the Internals folder is easy to find post-install by helper cmdlets + such as Install-ModuleDocumentation. + + Typical usage is to call this in your Build\Manage-Module.ps1 alongside + New-ConfigurationInformation -IncludeAll 'Internals\' so that the Internals + directory is physically included in the shipped module and discoverable later. + + .PARAMETER Enable + Enables delivery metadata emission. If not specified, nothing is emitted. + + .PARAMETER InternalsPath + Relative path inside the module that contains internal deliverables + (e.g. 'Internals'). Defaults to 'Internals'. + + .PARAMETER IncludeRootReadme + Include module root README.* during installation (if present). + + .PARAMETER IncludeRootChangelog + Include module root CHANGELOG.* during installation (if present). + + .PARAMETER IncludeRootLicense + Include module root LICENSE.* during installation (if present). + + .PARAMETER ReadmeDestination + Where to bundle README.* within the built module. One of: Internals, Root, Both, None. Default: Internals. + + .PARAMETER ChangelogDestination + Where to bundle CHANGELOG.* within the built module. One of: Internals, Root, Both, None. Default: Internals. + + .PARAMETER LicenseDestination + Where to bundle LICENSE.* within the built module. One of: Internals, Root, Both, None. Default: Internals. + + .PARAMETER ImportantLinks + One or more key/value pairs that represent important links to display to the user, + for example @{ Title = 'Docs'; Url = 'https://...' }. + + .PARAMETER IntroText + Text lines shown to users after Install-ModuleDocumentation completes. Accepts a string array. + + .PARAMETER UpgradeText + Text lines with upgrade instructions shown when requested via Show-ModuleDocumentation -Upgrade. + + .PARAMETER IntroFile + Relative path (within the module root) to a Markdown/text file to use as the Intro content. + If provided, it is preferred over IntroText for display and is also copied by + Install-ModuleDocumentation. + + .PARAMETER UpgradeFile + Relative path (within the module root) to a Markdown/text file to use for Upgrade instructions. + If provided, it is preferred over UpgradeText for display and is also copied by + Install-ModuleDocumentation. + + .EXAMPLE + PS> New-ConfigurationDelivery -Enable -InternalsPath 'Internals' -IncludeRootReadme -IncludeRootChangelog + Emits Options.Delivery and causes PrivateData.PSData.PSPublishModuleDelivery to be written in the manifest. + + .EXAMPLE + PS> New-ConfigurationInformation -IncludeAll 'Internals\' + PS> New-ConfigurationDelivery -Enable + Minimal configuration that bundles Internals and exposes it to the installer. + + .NOTES + This emits a Type 'Options' object under Options.Delivery so it works with the + existing New-PrepareStructure logic without further changes. + #> + [CmdletBinding()] + param( + [switch] $Enable, + [string] $InternalsPath = 'Internals', + [switch] $IncludeRootReadme, + [switch] $IncludeRootChangelog, + [switch] $IncludeRootLicense, + [ValidateSet('Internals','Root','Both','None')] + [string] $ReadmeDestination = 'Internals', + [ValidateSet('Internals','Root','Both','None')] + [string] $ChangelogDestination = 'Internals', + [ValidateSet('Internals','Root','Both','None')] + [string] $LicenseDestination = 'Internals', + [System.Collections.IDictionary[]] $ImportantLinks, + [string[]] $IntroText, + [string[]] $UpgradeText, + [string] $IntroFile, + [string] $UpgradeFile + ) + + if (-not $Enable) { return } + + $delivery = [ordered] @{ + Enable = $true + InternalsPath = $InternalsPath + IncludeRootReadme = $IncludeRootReadme.IsPresent + IncludeRootChangelog = $IncludeRootChangelog.IsPresent + ReadmeDestination = $ReadmeDestination + ChangelogDestination = $ChangelogDestination + LicenseDestination = $LicenseDestination + IncludeRootLicense = $IncludeRootLicense.IsPresent + ImportantLinks = $ImportantLinks + IntroText = $IntroText + UpgradeText = $UpgradeText + IntroFile = $IntroFile + UpgradeFile = $UpgradeFile + Schema = '1.2' + } + + [ordered] @{ + Type = 'Options' + Options = [ordered] @{ Delivery = $delivery } + } +} diff --git a/Public/New-ConfigurationExecute.ps1 b/Public/New-ConfigurationExecute.ps1 index e39223ff..d6e50982 100644 --- a/Public/New-ConfigurationExecute.ps1 +++ b/Public/New-ConfigurationExecute.ps1 @@ -1,6 +1,16 @@ function New-ConfigurationExecute { - [CmdletBinding()] - param( + <# + .SYNOPSIS + Reserved placeholder for future execution-time configuration. + + .DESCRIPTION + This cmdlet currently acts as a placeholder to keep backward compatibility with + older build scripts that may reference it. It returns nothing and performs no action. - ) -} \ No newline at end of file + .EXAMPLE + New-ConfigurationExecute + Does nothing and returns no output. + #> + [CmdletBinding()] + param() +} diff --git a/Public/New-ConfigurationFormat.ps1 b/Public/New-ConfigurationFormat.ps1 index a2498023..9930bc38 100644 --- a/Public/New-ConfigurationFormat.ps1 +++ b/Public/New-ConfigurationFormat.ps1 @@ -1,4 +1,114 @@ function New-ConfigurationFormat { + <# + .SYNOPSIS + Builds formatting options for code and manifest generation during the build. + + .DESCRIPTION + Produces a configuration object that controls how script and manifest files are formatted + during merge and in the default (non-merged) module. You can toggle specific PSScriptAnalyzer + rules, whitespace/indentation behavior, comment removal, and choose PSD1 output style. + + .PARAMETER ApplyTo + One or more targets to apply formatting to: OnMergePSM1, OnMergePSD1, DefaultPSM1, DefaultPSD1. + + .PARAMETER EnableFormatting + When set, enables formatting for the chosen ApplyTo targets even if no specific rule switches are provided. + + .PARAMETER Sort + Optional ordering hint for internal processing. Accepts None, Asc, or Desc. + + .PARAMETER RemoveComments + Remove comments in the formatted output. + + .PARAMETER RemoveEmptyLines + Remove empty lines while preserving readability. + + .PARAMETER RemoveAllEmptyLines + Remove all empty lines (more aggressive than RemoveEmptyLines). + + .PARAMETER RemoveCommentsInParamBlock + Remove comments within the param() block. + + .PARAMETER RemoveCommentsBeforeParamBlock + Remove comments that appear immediately before the param() block. + + .PARAMETER PlaceOpenBraceEnable + Enable PSPlaceOpenBrace rule and configure its behavior. + + .PARAMETER PlaceOpenBraceOnSameLine + For PSPlaceOpenBrace: place opening brace on the same line. + + .PARAMETER PlaceOpenBraceNewLineAfter + For PSPlaceOpenBrace: enforce a new line after the opening brace. + + .PARAMETER PlaceOpenBraceIgnoreOneLineBlock + For PSPlaceOpenBrace: ignore single-line blocks. + + .PARAMETER PlaceCloseBraceEnable + Enable PSPlaceCloseBrace rule and configure its behavior. + + .PARAMETER PlaceCloseBraceNewLineAfter + For PSPlaceCloseBrace: enforce a new line after the closing brace. + + .PARAMETER PlaceCloseBraceIgnoreOneLineBlock + For PSPlaceCloseBrace: ignore single-line blocks. + + .PARAMETER PlaceCloseBraceNoEmptyLineBefore + For PSPlaceCloseBrace: do not allow an empty line before a closing brace. + + .PARAMETER UseConsistentIndentationEnable + Enable PSUseConsistentIndentation rule and configure its behavior. + + .PARAMETER UseConsistentIndentationKind + Indentation style: 'space' or 'tab'. + + .PARAMETER UseConsistentIndentationPipelineIndentation + Pipeline indentation mode: IncreaseIndentationAfterEveryPipeline or NoIndentation. + + .PARAMETER UseConsistentIndentationIndentationSize + Number of spaces for indentation when Kind is 'space'. + + .PARAMETER UseConsistentWhitespaceEnable + Enable PSUseConsistentWhitespace rule and configure which elements to check. + + .PARAMETER UseConsistentWhitespaceCheckInnerBrace + For PSUseConsistentWhitespace: check inner brace spacing. + + .PARAMETER UseConsistentWhitespaceCheckOpenBrace + For PSUseConsistentWhitespace: check open brace spacing. + + .PARAMETER UseConsistentWhitespaceCheckOpenParen + For PSUseConsistentWhitespace: check open parenthesis spacing. + + .PARAMETER UseConsistentWhitespaceCheckOperator + For PSUseConsistentWhitespace: check operator spacing. + + .PARAMETER UseConsistentWhitespaceCheckPipe + For PSUseConsistentWhitespace: check pipeline operator spacing. + + .PARAMETER UseConsistentWhitespaceCheckSeparator + For PSUseConsistentWhitespace: check separator (comma) spacing. + + .PARAMETER AlignAssignmentStatementEnable + Enable PSAlignAssignmentStatement rule and optionally check hashtable alignment. + + .PARAMETER AlignAssignmentStatementCheckHashtable + For PSAlignAssignmentStatement: align hashtable assignments. + + .PARAMETER UseCorrectCasingEnable + Enable PSUseCorrectCasing rule. + + .PARAMETER PSD1Style + Style for generated manifests (PSD1) for the selected ApplyTo targets. 'Minimal' or 'Native'. + + .EXAMPLE + New-ConfigurationFormat -ApplyTo 'OnMergePSD1','DefaultPSD1' -PSD1Style 'Minimal' + Minimizes PSD1 output during merge and default builds. + + .EXAMPLE + New-ConfigurationFormat -ApplyTo 'OnMergePSM1' -EnableFormatting -UseConsistentIndentationEnable -UseConsistentIndentationKind space -UseConsistentIndentationIndentationSize 4 + Enables indentation and whitespace rules for merged PSM1. + #> [CmdletBinding()] param( [Parameter(Mandatory)] @@ -185,4 +295,4 @@ } # $Config = New-ConfigurationFormat -ApplyTo OnMergePSD1, DefaultPSD1 -PSD1Style Minimal -# $Config.Options \ No newline at end of file +# $Config.Options diff --git a/Public/New-ConfigurationInformation.ps1 b/Public/New-ConfigurationInformation.ps1 index 49c43e12..e98b333d 100644 --- a/Public/New-ConfigurationInformation.ps1 +++ b/Public/New-ConfigurationInformation.ps1 @@ -1,4 +1,48 @@ function New-ConfigurationInformation { + <# + .SYNOPSIS + Describes what to include/exclude in the module build and how libraries are organized. + + .DESCRIPTION + Emits a configuration block with folder-level include/exclude rules and optional library + locations that the builder uses to stage content prior to merge/packaging. + + .PARAMETER FunctionsToExportFolder + Folder name containing public functions to export (e.g., 'Public'). + + .PARAMETER AliasesToExportFolder + Folder name containing public aliases to export (e.g., 'Public'). + + .PARAMETER ExcludeFromPackage + Paths or patterns to exclude from artefacts (e.g., 'Ignore','Docs','Examples'). + + .PARAMETER IncludeRoot + File patterns from the root to include (e.g., '*.psm1','*.psd1','License*'). + + .PARAMETER IncludePS1 + Folder names where PS1 files should be included (e.g., 'Private','Public','Enums','Classes'). + + .PARAMETER IncludeAll + Folder names to include entirely (e.g., 'Images','Resources','Templates','Bin','Lib','Data'). + + .PARAMETER IncludeCustomCode + Scriptblock executed during staging to add custom files/folders. + + .PARAMETER IncludeToArray + Advanced form to pass IncludeRoot/IncludePS1/IncludeAll as a single hashtable. + + .PARAMETER LibrariesCore + Relative path to libraries compiled for Core (default 'Lib/Core'). + + .PARAMETER LibrariesDefault + Relative path to libraries for classic .NET (default 'Lib/Default'). + + .PARAMETER LibrariesStandard + Relative path to libraries for .NET Standard (default 'Lib/Standard'). + + .EXAMPLE + New-ConfigurationInformation -IncludeAll 'Internals\' -IncludePS1 'Private','Public' -ExcludeFromPackage 'Ignore','Docs' + #> [cmdletbinding()] param( [string] $FunctionsToExportFolder, @@ -34,4 +78,4 @@ Configuration = $Configuration } $Option -} \ No newline at end of file +} diff --git a/Public/New-ConfigurationManifest.ps1 b/Public/New-ConfigurationManifest.ps1 index 5a4f5653..0dc306e3 100644 --- a/Public/New-ConfigurationManifest.ps1 +++ b/Public/New-ConfigurationManifest.ps1 @@ -45,6 +45,11 @@ .PARAMETER LicenseUri Specifies the URI for the module's license. + .PARAMETER RequireLicenseAcceptance + When set, indicates the module requires explicit user license acceptance (PowerShellGet). + If enabled, ensure a license file exists at the module root; the builder will + normalize any LICENSE/License.md to 'license.txt' in the final package. + .PARAMETER Prerelease Specifies the prerelease tag for the module. @@ -81,6 +86,7 @@ [string] $ProjectUri, [string] $DotNetFrameworkVersion, [string] $LicenseUri, + [switch] $RequireLicenseAcceptance, [alias('PrereleaseTag')][string] $Prerelease, [string[]] $FunctionsToExport, [string[]] $CmdletsToExport, @@ -102,6 +108,7 @@ ProjectUri = $ProjectUri DotNetFrameworkVersion = $DotNetFrameworkVersion LicenseUri = $LicenseUri + RequireLicenseAcceptance = $RequireLicenseAcceptance.IsPresent Prerelease = $Prerelease FunctionsToExport = $FunctionsToExport CmdletsToExport = $CmdletsToExport @@ -115,4 +122,4 @@ Configuration = $Manifest } $Option -} \ No newline at end of file +} diff --git a/Public/New-ConfigurationTest.ps1 b/Public/New-ConfigurationTest.ps1 index 7febe231..8ddd6ab4 100644 --- a/Public/New-ConfigurationTest.ps1 +++ b/Public/New-ConfigurationTest.ps1 @@ -1,4 +1,25 @@ function New-ConfigurationTest { + <# + .SYNOPSIS + Configures running Pester tests as part of the build. + + .DESCRIPTION + Emits test configuration that the builder uses to run tests. Currently, tests + are triggered AfterMerge. When -Enable is not provided, nothing is emitted. + + .PARAMETER TestsPath + Path to the folder containing Pester tests. + + .PARAMETER Enable + Enable test execution in the build. + + .PARAMETER Force + Force running tests even if they already ran or when caching would skip them. + + .EXAMPLE + New-ConfigurationTest -Enable -TestsPath 'Tests' -Force + Configures tests to run after merge from the 'Tests' folder. + #> [CmdletBinding()] param( #[Parameter(Mandatory)][ValidateSet('BeforeMerge', 'AfterMerge')][string[]] $When, @@ -28,4 +49,4 @@ $Configuration } } -} \ No newline at end of file +} diff --git a/Public/Register-Certificate.ps1 b/Public/Register-Certificate.ps1 index 5e4a9fc4..7d6c456c 100644 --- a/Public/Register-Certificate.ps1 +++ b/Public/Register-Certificate.ps1 @@ -1,4 +1,41 @@ function Register-Certificate { + <# + .SYNOPSIS + Signs files in a path using a code-signing certificate (Windows and PowerShell Core supported). + + .DESCRIPTION + Locates a code-signing certificate (by thumbprint from the Windows cert store or from a PFX) + and applies Authenticode signatures to matching files under -Path. + On Windows, uses Set-AuthenticodeSignature; on non-Windows, uses OpenAuthenticode module if available. + + .PARAMETER CertificatePFX + A PFX file to use for signing. Mutually exclusive with -LocalStore/-Thumbprint. + + .PARAMETER LocalStore + Certificate store to search ('LocalMachine' or 'CurrentUser') when using a certificate from the store. + + .PARAMETER Thumbprint + Certificate thumbprint to select a single certificate from the chosen -LocalStore. + + .PARAMETER Path + Root directory containing files to sign. + + .PARAMETER TimeStampServer + RFC3161 timestamp server URL. Default: http://timestamp.digicert.com + + .PARAMETER IncludeChain + Which portion of the chain to include in the signature: All, NotRoot, or Signer. Default: All. + + .PARAMETER Include + File patterns to include during signing. Defaults to scripts only: '*.ps1','*.psd1','*.psm1'. + You may pass additional patterns if needed (e.g., '*.dll'). + + .PARAMETER ExcludePath + One or more path substrings to exclude from signing. Useful for skipping folders like 'Internals' unless opted-in. + + .PARAMETER HashAlgorithm + Hash algorithm for the signature. Default: SHA256. + #> [cmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ParameterSetName = 'PFX')][string] $CertificatePFX, @@ -7,7 +44,8 @@ [Parameter(Mandatory)][string] $Path, [string] $TimeStampServer = 'http://timestamp.digicert.com', [ValidateSet('All', 'NotRoot', 'Signer')] [string] $IncludeChain = 'All', - [string[]] $Include = @('*.ps1', '*.psd1', '*.psm1', '*.dll', '*.cat'), + [string[]] $Include = @('*.ps1', '*.psd1', '*.psm1'), + [string[]] $ExcludePath, [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')][string] $HashAlgorithm = 'SHA256' ) if ($PSBoundParameters.Keys -contains 'LocalStore') { @@ -65,9 +103,16 @@ } else { $IncludeOption = 'None' } - Get-ChildItem -Path $Path -Filter * -Include $Include -Recurse -ErrorAction SilentlyContinue | Where-Object { - ($_ | Get-OpenAuthenticodeSignature).Status -eq 'NotSigned' - } | Set-OpenAuthenticodeSignature -Certificate $Certificate -TimeStampServer $TimeStampServer -IncludeChain $IncludeOption -HashAlgorithm $HashAlgorithm + $items = Get-ChildItem -Path $Path -Filter * -Include $Include -Recurse -ErrorAction SilentlyContinue + if ($ExcludePath) { + $items = $items | Where-Object { + $full = $_.FullName + ($full -notlike '*\Internals\*' -and $full -notlike '*/Internals/*') -and ( + ($ExcludePath | ForEach-Object { $full -like ("*" + $_ + "*") }) -notcontains $true) + } + } + $items | Where-Object { ($_ | Get-OpenAuthenticodeSignature).Status -eq 'NotSigned' } | + Set-OpenAuthenticodeSignature -Certificate $Certificate -TimeStampServer $TimeStampServer -IncludeChain $IncludeOption -HashAlgorithm $HashAlgorithm } else { # This is for Windows, we need to use PKI module, it's usually installed by default @@ -76,10 +121,17 @@ Write-Warning -Message "Register-Certificate - Code signing commands not found. Skipping signing." return } - Get-ChildItem -Path $Path -Filter * -Include $Include -Recurse -ErrorAction SilentlyContinue | Where-Object { - ($_ | Get-AuthenticodeSignature).Status -eq 'NotSigned' - } | Set-AuthenticodeSignature -Certificate $Certificate -TimestampServer $TimeStampServer -IncludeChain $IncludeChain -HashAlgorithm $HashAlgorithm + $items = Get-ChildItem -Path $Path -Filter * -Include $Include -Recurse -ErrorAction SilentlyContinue + if ($ExcludePath) { + $items = $items | Where-Object { + $full = $_.FullName + ($full -notlike '*\Internals\*' -and $full -notlike '*/Internals/*') -and ( + ($ExcludePath | ForEach-Object { $full -like ("*" + $_ + "*") }) -notcontains $true) + } + } + $items | Where-Object { ($_ | Get-AuthenticodeSignature).Status -eq 'NotSigned' } | + Set-AuthenticodeSignature -Certificate $Certificate -TimestampServer $TimeStampServer -IncludeChain $IncludeChain -HashAlgorithm $HashAlgorithm } } } -} \ No newline at end of file +} diff --git a/Public/Show-ModuleDocumentation.ps1 b/Public/Show-ModuleDocumentation.ps1 new file mode 100644 index 00000000..7c09c94c --- /dev/null +++ b/Public/Show-ModuleDocumentation.ps1 @@ -0,0 +1,228 @@ +function Show-ModuleDocumentation { + <# + .SYNOPSIS + Shows README/CHANGELOG or a chosen document for a module, with a simple console view. + + .DESCRIPTION + Finds a module (by name or PSModuleInfo) and renders README/CHANGELOG from the module root + or from its Internals folder (as defined in PrivateData.PSData.PSPublishModuleDelivery). + You can also point directly to a docs folder via -DocsPath (e.g., output of Install-ModuleDocumentation). + + .PARAMETER Name + Module name to show documentation for. Accepts pipeline by value. + + .PARAMETER Module + A PSModuleInfo object (e.g., from Get-Module -ListAvailable) to operate on directly. + + .PARAMETER RequiredVersion + Specific version of the module to target. If omitted, selects the highest available. + + .PARAMETER DocsPath + A folder that contains documentation to display (e.g., the destination created by Install-ModuleDocumentation). + When provided, the cmdlet does not look up the module and shows docs from this folder. + + .PARAMETER Readme + Show README*. If both root and Internals copies exist, the root copy is preferred unless -PreferInternals is set. + + .PARAMETER Changelog + Show CHANGELOG*. If both root and Internals copies exist, the root copy is preferred unless -PreferInternals is set. + + .PARAMETER License + Show LICENSE. If multiple variants exist (LICENSE.md, LICENSE.txt), the resolver prefers a normalized 'license.txt' in + the chosen area (root vs Internals). + + .PARAMETER Intro + Show introduction text defined in PrivateData.PSData.PSPublishModuleDelivery.IntroText when available. If not defined, + falls back to README resolution (root vs Internals honoring -PreferInternals). + + .PARAMETER Upgrade + Show upgrade text defined in PrivateData.PSData.PSPublishModuleDelivery.UpgradeText when available. If not defined, + looks for an UPGRADE* file; otherwise throws. + + .PARAMETER File + Relative path to a specific file to display (relative to module root or Internals). If rooted, used as-is. + + .PARAMETER PreferInternals + Prefer the Internals copy of README/CHANGELOG when both exist. + + .PARAMETER List + List available README/CHANGELOG files found (root and Internals) instead of displaying content. + + .PARAMETER Raw + Output the raw file content (no styling). + + .PARAMETER Open + Open the resolved file in the system default viewer instead of rendering in the console. + + .EXAMPLE + Show-ModuleDocumentation -Name EFAdminManager -Readme + + .EXAMPLE + Get-Module -ListAvailable EFAdminManager | Show-ModuleDocumentation -Changelog + + .EXAMPLE + Show-ModuleDocumentation -DocsPath 'C:\Docs\EFAdminManager\3.0.0' -Readme -Open + + .EXAMPLE + Show-ModuleDocumentation -Name EFAdminManager -License + + .EXAMPLE + Show-ModuleDocumentation -Name EFAdminManager -Intro + + .EXAMPLE + Show-ModuleDocumentation -Name EFAdminManager -Upgrade + + .EXAMPLE + Show-ModuleDocumentation -Name EFAdminManager -List + #> + [CmdletBinding(DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='ByName', Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] + [Alias('ModuleName')] + [string] $Name, + [Parameter(ParameterSetName='ByModule', ValueFromPipeline=$true)] + [Alias('InputObject','ModuleInfo')] + [System.Management.Automation.PSModuleInfo] $Module, + [version] $RequiredVersion, + [Parameter(ParameterSetName='ByPath')] + [string] $DocsPath, + [switch] $Readme, + [switch] $Changelog, + [switch] $License, + [switch] $Intro, + [switch] $Upgrade, + [string] $File, + [switch] $PreferInternals, + [switch] $List, + [switch] $Raw, + [switch] $Open + ) + + begin {} + process { + $rootBase = $null + $internalsBase = $null + $moduleName = $null + $moduleVersion = $null + + if ($PSCmdlet.ParameterSetName -eq 'ByPath') { + if (-not $DocsPath) { throw 'Specify -DocsPath for the ByPath parameter set.' } + if (-not (Test-Path -LiteralPath $DocsPath)) { throw "DocsPath '$DocsPath' not found." } + $rootBase = $DocsPath + $intCandidate = Join-Path $DocsPath 'Internals' + if (Test-Path -LiteralPath $intCandidate) { $internalsBase = $intCandidate } + } else { + if ($PSCmdlet.ParameterSetName -eq 'ByName') { + if (-not $Name) { throw "Specify -Name or pipe a module via -Module." } + $candidates = Get-Module -ListAvailable -Name $Name + if (-not $candidates) { throw "Module '$Name' not found." } + if ($RequiredVersion) { + $Module = $candidates | Where-Object { $_.Version -eq $RequiredVersion } | Sort-Object Version -Descending | Select-Object -First 1 + if (-not $Module) { throw "Module '$Name' with version $RequiredVersion not found." } + } else { + $Module = $candidates | Sort-Object Version -Descending | Select-Object -First 1 + } + } elseif ($PSCmdlet.ParameterSetName -eq 'ByModule') { + if (-not $Module) { throw "Pipeline didn't pass a PSModuleInfo object. Use -Name or pipe Get-Module output." } + if ($RequiredVersion -and $Module.Version -ne $RequiredVersion) { + $resolved = Get-Module -ListAvailable -Name $Module.Name | Where-Object { $_.Version -eq $RequiredVersion } | Sort-Object Version -Descending | Select-Object -First 1 + if ($resolved) { $Module = $resolved } else { throw "Module '$($Module.Name)' with version $RequiredVersion not found." } + } + } + $rootBase = $Module.ModuleBase + $moduleName = $Module.Name + $moduleVersion = $Module.Version + + $manifestPath = Join-Path $rootBase ("{0}.psd1" -f $Module.Name) + $delivery = $null + if (Test-Path -LiteralPath $manifestPath) { + try { $manifest = Test-ModuleManifest -Path $manifestPath; $delivery = $manifest.PrivateData.PSData.PSPublishModuleDelivery } catch {} + } + $internalsRel = if ($delivery -and $delivery.InternalsPath) { [string]$delivery.InternalsPath } else { 'Internals' } + $intCandidate = Join-Path $rootBase $internalsRel + if (Test-Path -LiteralPath $intCandidate) { $internalsBase = $intCandidate } + } + + if ($List) { + $rows = @() + if ($rootBase) { + $rows += Get-ChildItem -LiteralPath $rootBase -Filter 'README*' -File -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Name=$_.Name; FullName=$_.FullName; Area='Root' } } + $rows += Get-ChildItem -LiteralPath $rootBase -Filter 'CHANGELOG*' -File -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Name=$_.Name; FullName=$_.FullName; Area='Root' } } + $rows += Get-ChildItem -LiteralPath $rootBase -Filter 'LICENSE*' -File -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Name=$_.Name; FullName=$_.FullName; Area='Root' } } + } + if ($internalsBase) { + $rows += Get-ChildItem -LiteralPath $internalsBase -Filter 'README*' -File -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Name=$_.Name; FullName=$_.FullName; Area='Internals' } } + $rows += Get-ChildItem -LiteralPath $internalsBase -Filter 'CHANGELOG*' -File -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Name=$_.Name; FullName=$_.FullName; Area='Internals' } } + $rows += Get-ChildItem -LiteralPath $internalsBase -Filter 'LICENSE*' -File -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Name=$_.Name; FullName=$_.FullName; Area='Internals' } } + } + if ($rows.Count -eq 0) { Write-Warning 'No README/CHANGELOG found.' } else { $rows } + return + } + + $target = $null + if ($File) { + if ([System.IO.Path]::IsPathRooted($File)) { + if (-not (Test-Path -LiteralPath $File)) { throw "File '$File' not found." } + $target = (Get-Item -LiteralPath $File).FullName + } else { + $try1 = if ($rootBase) { Join-Path $rootBase $File } + $try2 = if ($internalsBase) { Join-Path $internalsBase $File } + if ($try1 -and (Test-Path -LiteralPath $try1)) { $target = (Get-Item -LiteralPath $try1).FullName } + elseif ($try2 -and (Test-Path -LiteralPath $try2)) { $target = (Get-Item -LiteralPath $try2).FullName } + else { throw "File '$File' not found under root or Internals." } + } + } elseif ($Readme) { + $f = Resolve-DocFile -Kind 'README' -RootBase $rootBase -InternalsBase $internalsBase -PreferInternals:$PreferInternals + if ($f) { $target = $f.FullName } else { throw 'README not found.' } + } elseif ($Changelog) { + $f = Resolve-DocFile -Kind 'CHANGELOG' -RootBase $rootBase -InternalsBase $internalsBase -PreferInternals:$PreferInternals + if ($f) { $target = $f.FullName } else { throw 'CHANGELOG not found.' } + } elseif ($License) { + $f = Resolve-DocFile -Kind 'LICENSE' -RootBase $rootBase -InternalsBase $internalsBase -PreferInternals:$PreferInternals + if ($f) { $target = $f.FullName } else { throw 'LICENSE not found.' } + } elseif ($Intro) { + if ($manifest -and $manifest.PrivateData -and $manifest.PrivateData.PSData -and $manifest.PrivateData.PSData.PSPublishModuleDelivery -and $manifest.PrivateData.PSData.PSPublishModuleDelivery.IntroText) { + $title = if ($moduleName) { "$moduleName $moduleVersion — Introduction" } else { 'Introduction' } + Write-Heading -Text $title + foreach ($line in [string[]]$manifest.PrivateData.PSData.PSPublishModuleDelivery.IntroText) { Write-Host $line } + return + } else { + $f = Resolve-DocFile -Kind 'README' -RootBase $rootBase -InternalsBase $internalsBase -PreferInternals:$PreferInternals + if ($f) { $target = $f.FullName } else { throw 'Introduction not defined; README not found.' } + } + } elseif ($Upgrade) { + if ($manifest -and $manifest.PrivateData -and $manifest.PrivateData.PSData -and $manifest.PrivateData.PSData.PSPublishModuleDelivery -and $manifest.PrivateData.PSData.PSPublishModuleDelivery.UpgradeText) { + $title = if ($moduleName) { "$moduleName $moduleVersion — Upgrade" } else { 'Upgrade' } + Write-Heading -Text $title + foreach ($line in [string[]]$manifest.PrivateData.PSData.PSPublishModuleDelivery.UpgradeText) { Write-Host $line } + return + } else { + $f = Resolve-DocFile -Kind 'UPGRADE' -RootBase $rootBase -InternalsBase $internalsBase -PreferInternals:$PreferInternals + if ($f) { $target = $f.FullName } else { throw 'Upgrade instructions not defined and no UPGRADE file found.' } + } + } else { + # Default: README else CHANGELOG + $f = Resolve-DocFile -Kind 'README' -RootBase $rootBase -InternalsBase $internalsBase -PreferInternals:$PreferInternals + if (-not $f) { $f = Resolve-DocFile -Kind 'CHANGELOG' -RootBase $rootBase -InternalsBase $internalsBase -PreferInternals:$PreferInternals } + if ($f) { $target = $f.FullName } else { throw 'No README or CHANGELOG found.' } + } + + if ($Open) { + Start-Process -FilePath $target | Out-Null + return + } + + if ($Raw) { + Get-Content -LiteralPath $target -Raw -ErrorAction Stop + } else { + $title = if ($moduleName) { "$moduleName $moduleVersion — $([IO.Path]::GetFileName($target))" } else { [IO.Path]::GetFileName($target) } + Write-Heading -Text $title + try { + $content = Get-Content -LiteralPath $target -Raw -ErrorAction Stop + Write-Host $content + } catch { + Write-Warning "Failed to read '$target': $($_.Exception.Message)" + } + } + } +} diff --git a/Sources/PSPublishModule/Initialize.cs b/Sources/PSPublishModule/Initialize.cs index 814811c1..ca8a01c6 100644 --- a/Sources/PSPublishModule/Initialize.cs +++ b/Sources/PSPublishModule/Initialize.cs @@ -3,6 +3,4 @@ /// /// Dummy class to make the module visible to the module manager. /// -public class Initialize { - -} +public class Initialize { } diff --git a/Sources/PSPublishModule/OnImportAndRemove.cs b/Sources/PSPublishModule/OnImportAndRemove.cs index 2d0de44e..b49f1c3f 100644 --- a/Sources/PSPublishModule/OnImportAndRemove.cs +++ b/Sources/PSPublishModule/OnImportAndRemove.cs @@ -3,37 +3,60 @@ using System.Management.Automation; using System.Reflection; +/// +/// Namespace for module import and removal handling. +/// public class OnModuleImportAndRemove : IModuleAssemblyInitializer, IModuleAssemblyCleanup { + /// + /// Handles module import event. + /// public void OnImport() { if (IsNetFramework()) { AppDomain.CurrentDomain.AssemblyResolve += MyResolveEventHandler; } } + /// + /// Handles module removal event. + /// + /// public void OnRemove(PSModuleInfo module) { if (IsNetFramework()) { AppDomain.CurrentDomain.AssemblyResolve -= MyResolveEventHandler; } } + /// + /// Custom assembly resolver to load assemblies from the module directory. + /// + /// + /// + /// private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args) { //This code is used to resolve the assemblies //Console.WriteLine($"Resolving {args.Name}"); var directoryPath = Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location); - var filesInDirectory = Directory.GetFiles(directoryPath); + if (directoryPath != null) { + var filesInDirectory = Directory.GetFiles(directoryPath); - foreach (var file in filesInDirectory) { - var fileName = Path.GetFileName(file); - var assemblyName = Path.GetFileNameWithoutExtension(file); + foreach (var file in filesInDirectory) { + var fileName = Path.GetFileName(file); + var assemblyName = Path.GetFileNameWithoutExtension(file); - if (args.Name.StartsWith(assemblyName)) { - //Console.WriteLine($"Loading {args.Name} assembly {fileName}"); - return Assembly.LoadFile(file); + if (args.Name.StartsWith(assemblyName)) { + //Console.WriteLine($"Loading {args.Name} assembly {fileName}"); + return Assembly.LoadFile(file); + } } } + return null; } + /// + /// Determines if the current runtime is .NET Framework. + /// + /// private bool IsNetFramework() { // Get the version of the CLR Version clrVersion = System.Environment.Version; @@ -41,10 +64,18 @@ private bool IsNetFramework() { return clrVersion.Major == 4; } + /// + /// Determines if the current runtime is .NET Core. + /// + /// private bool IsNetCore() { return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase); } + /// + /// Determines if the current runtime is .NET 5 or higher. + /// + /// private bool IsNet5OrHigher() { return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET 5", StringComparison.OrdinalIgnoreCase) || System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET 6", StringComparison.OrdinalIgnoreCase) ||