Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/skills/skills/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: skills
description: Index folder for vendored shared skills used by NovaModuleTools scaffolding and local guidance.
---

# Skill: skills

## When to use

Use this folder only as a container for nested shared skills that are maintained separately from the repository-local skill entry points.

## Relevant files

- `.github/skills/skills/*`

## Expected practices

- Do not reference this container skill directly from prompts or agents.
- Keep nested shared skills organized beneath this folder when they are intentionally vendored for local reuse.
- Prefer the repository-local top-level skills under `.github/skills/*/SKILL.md` for normal NovaModuleTools task routing.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- `project.json` now supports `Manifest.PrivateData` for arbitrary consumer-defined manifest metadata.
- Nova still uses `New-ModuleManifest` for the standard manifest flow, but now serializes structured `PrivateData` itself so nested objects, arrays, booleans, numbers, strings, and `null` values survive in the generated `.psd1`.
- Generated manifests now preserve both PowerShell/Nova-managed `PrivateData.PSData` metadata and custom `Manifest.PrivateData` entries, and reject reserved-key collisions instead of overwriting `PSData` silently.

### Changed

### Deprecated
Expand Down Expand Up @@ -541,4 +545,3 @@ This release was yanked because it removed the implicit `Pester` dependency, bef
[0.0.6]: https://github.com/stiwicourage/NovaModuleTools/compare/Version_0.0.5...Version_0.0.6
[0.0.5]: https://github.com/stiwicourage/NovaModuleTools/compare/Version_0.0.4...Version_0.0.5
[0.0.4]: https://github.com/stiwicourage/NovaModuleTools/compare/Version_0.0.3...Version_0.0.4

5 changes: 4 additions & 1 deletion RELEASE_NOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang

### Added

- `project.json` now supports `Manifest.PrivateData` for arbitrary consumer-defined module manifest metadata.
- Nova still uses `New-ModuleManifest` for the standard manifest flow, but now serializes structured `PrivateData` itself so nested values remain valid in the generated `.psd1`.
- Generated manifests preserve both `PrivateData.PSData` and custom `Manifest.PrivateData` entries, and reject reserved-key collisions instead of overwriting manifest metadata silently.

### Changed

### Deprecated
Expand Down Expand Up @@ -279,4 +283,3 @@ This release was yanked because it removed the implicit `Pester` dependency befo
## [0.0.4] - 2024-06-25
### Added
- First PowerShell Gallery release of NovaModuleTools with the initial module workflow support.

39 changes: 37 additions & 2 deletions docs/project-json-reference.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ <h2>How to read this file</h2>
<strong>Schema coverage.</strong>
<p>The unified JSON schema covers all documented fields including
<code>Manifest.ReleaseNotes</code>, <code>Manifest.LicenseUri</code>,
<code>Manifest.IconUri</code>, and <code>Pester.TestResult.OutputFormat</code>. VS Code picks
<code>Manifest.IconUri</code>, <code>Manifest.PrivateData</code>, and <code>Pester.TestResult.OutputFormat</code>. VS Code picks
up the schema automatically when the <code>$schema</code> field is present in
<code>project.json</code>.</p>
</div>
Expand Down Expand Up @@ -142,7 +142,13 @@ <h2>Complete example</h2>
"Tags": ["Example", "NovaModuleTools", "PowerShell"],
"ProjectUri": "https://www.novamoduletools.com/",
"ReleaseNotes": "https://www.novamoduletools.com/release-notes.html",
"LicenseUri": "https://www.novamoduletools.com/license.html"
"LicenseUri": "https://www.novamoduletools.com/license.html",
"PrivateData": {
"ExampleProduct": {
"Enabled": true,
"ApiVersion": "1"
}
}
},
"Package": {
"Id": "NovaExampleModule",
Expand Down Expand Up @@ -372,12 +378,41 @@ <h2><code>Manifest</code> settings</h2>
publicly.
</td>
</tr>
<tr>
<td><code>PrivateData</code></td>
<td>No</td>
<td>Adds arbitrary consumer-defined metadata to the manifest <code>PrivateData</code> hashtable.</td>
<td>Nova serializes this data itself so arbitrary property names and nested objects, arrays, booleans, numbers, strings, and <code>null</code> values remain valid PowerShell data in the generated manifest.</td>
</tr>
</tbody>
</table>
</div>
<p class="tiny-note">If <code>Tags</code>, <code>ProjectUri</code>, <code>ReleaseNotes</code>, or <code>LicenseUri</code>
are omitted, packaging still succeeds. The corresponding package metadata fields are simply left
out.</p>
<p>Use <code>Manifest.PrivateData</code> for arbitrary module metadata. Nova keeps the standard PowerShell-generated
<code>PrivateData.PSData</code> metadata and merges your configured keys into the same top-level
<code>PrivateData</code> hashtable. Property names remain consumer-defined, so keys such as
<code>Example Product</code>, <code>Feature-Flag</code>, or <code>123abc</code> are serialized safely.</p>
<p class="tiny-note">Nova validates the rewritten manifest by re-importing the generated
<code>.psd1</code>. PowerShell's own <code>Test-ModuleManifest</code> cmdlet can still reject
consumer-defined top-level <code>PrivateData</code> entries even when the manifest is valid
PowerShell data, so Nova treats the data-file import round-trip as the compatibility check for
custom metadata.</p>
<pre><code>{
"Manifest": {
"PrivateData": {
"Example Product": {
"Feature-Flag": true,
"ApiVersion": "1",
"RetryCount": 3,
"Nested Data": {
"Mode": "Test"
}
}
}
}
}</code></pre>
</section>

<section class="content-section" id="package">
Expand Down
5 changes: 5 additions & 0 deletions docs/schema/v3/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@
"type": "string",
"description": "Sets the copyright string in the module manifest."
},
"PrivateData": {
"type": "object",
"additionalProperties": true,
"description": "Arbitrary consumer-defined module metadata serialized into the generated manifest PrivateData hashtable."
},
"RequiredModules": {
"type": "array",
"description": "Lists modules that must be imported before this module. Each entry is either a module name string or a module-specification object.",
Expand Down
125 changes: 85 additions & 40 deletions src/private/build/BuildManifest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,66 +6,111 @@ function Build-Manifest {

Write-Verbose 'Building psd1 data file Manifest'
$data = Get-NovaBuildProjectInfo -ProjectInfo $ProjectInfo

$PubFunctionFiles = @(Get-ChildItem -Path $data.PublicDir -Filter *.ps1)
$functionToExport = @()
$aliasToExport = @()
foreach ($pubFunctionFile in $PubFunctionFiles) {
$functionToExport += Get-FunctionNameFromFile -filePath $pubFunctionFile.FullName
$aliasToExport += Get-AliasInFunctionFromFile -filePath $pubFunctionFile.FullName
}

## Import Format.ps1xml (if any)
$FormatsToProcess = @()
Get-ChildItem -Path $data.ResourcesDir -File -Filter '*Format.ps1xml' -ErrorAction SilentlyContinue | ForEach-Object {
if ($data.CopyResourcesToModuleRoot) {
$FormatsToProcess += $_.Name
} else {
$FormatsToProcess += Join-Path -Path 'resources' -ChildPath $_.Name
}
}

## Import Types.ps1xml1 (if any)
$TypesToProcess = @()
Get-ChildItem -Path $data.ResourcesDir -File -Filter '*Types.ps1xml' -ErrorAction SilentlyContinue | ForEach-Object {
if ($data.CopyResourcesToModuleRoot) {
$TypesToProcess += $_.Name
} else {
$TypesToProcess += Join-Path -Path 'resources' -ChildPath $_.Name
}
}
$exportDefinition = Get-ManifestExportDefinition -PublicDir $data.PublicDir
$formatFiles = Get-ManifestResourceFilePath -ResourcesDir $data.ResourcesDir -CopyResourcesToModuleRoot:$data.CopyResourcesToModuleRoot -Filter '*Format.ps1xml'
$typeFiles = Get-ManifestResourceFilePath -ResourcesDir $data.ResourcesDir -CopyResourcesToModuleRoot:$data.CopyResourcesToModuleRoot -Filter '*Types.ps1xml'

$ManfiestAllowedParams = (Get-Command New-ModuleManifest).Parameters.Keys
Assert-ManifestSchema -Manifest $data.Manifest -AllowedParameter $ManfiestAllowedParams
$sv = [semver]$data.Version
$ParmsManifest = @{
Path = $data.ManifestFilePSD1
Description = $data.Description
FunctionsToExport = $functionToExport
AliasesToExport = $aliasToExport
FunctionsToExport = $exportDefinition.FunctionToExport
AliasesToExport = $exportDefinition.AliasToExport
RootModule = "$( $data.ProjectName ).psm1"
ModuleVersion = [version]$sv
FormatsToProcess = $FormatsToProcess
TypesToProcess = $TypesToProcess
FormatsToProcess = $formatFiles
TypesToProcess = $typeFiles
}

## Release lable
if ($sv.PreReleaseLabel) {
$ParmsManifest['Prerelease'] = $sv.PreReleaseLabel
}

# Accept only valid Manifest Parameters
$data.Manifest.Keys | ForEach-Object {
if ($ManfiestAllowedParams -contains $_) {
if ($data.Manifest.$_) {
$ParmsManifest.add($_, $data.Manifest.$_)
}
}
}
Add-AllowedManifestParameterEntry -ManifestParameters $ParmsManifest -Manifest $data.Manifest -AllowedParameter $ManfiestAllowedParams

try {
New-ModuleManifest @ParmsManifest
} catch {
Stop-NovaOperation -Message ('Failed to create Manifest: {0}' -f $_.Exception.Message) -ErrorId 'Nova.Dependency.ModuleManifestCreationFailed' -Category OpenError -TargetObject $data.ManifestFilePSD1
}

if ($data.Manifest.Contains('PrivateData') -and $data.Manifest['PrivateData'] -is [System.Collections.IDictionary]) {
Update-ManifestPrivateData -ManifestPath $data.ManifestFilePSD1 -PrivateData $data.Manifest['PrivateData']
Assert-GeneratedManifestPrivateDataCanBeImported -ManifestPath $data.ManifestFilePSD1
}
}

function Assert-GeneratedManifestPrivateDataCanBeImported {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$ManifestPath
)

try {
$null = Import-PowerShellDataFile -LiteralPath $ManifestPath -ErrorAction Stop
} catch {
Stop-NovaOperation -Message ('Generated manifest contains invalid PrivateData: {0}' -f $_.Exception.Message) -ErrorId 'Nova.Dependency.ModuleManifestPrivateDataValidationFailed' -Category InvalidData -TargetObject $ManifestPath
}
}

function Get-ManifestExportDefinition {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$PublicDir
)

$functionToExport = @()
$aliasToExport = @()
foreach ($publicFunctionFile in @(Get-ChildItem -Path $PublicDir -Filter *.ps1)) {
$functionToExport += Get-FunctionNameFromFile -filePath $publicFunctionFile.FullName
$aliasToExport += Get-AliasInFunctionFromFile -filePath $publicFunctionFile.FullName
}

return [pscustomobject]@{
FunctionToExport = $functionToExport
AliasToExport = $aliasToExport
}
}

function Get-ManifestResourceFilePath {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$ResourcesDir,
[Parameter(Mandatory)][string]$Filter,
[switch]$CopyResourcesToModuleRoot
)

$resourceFilePath = @()
Get-ChildItem -Path $ResourcesDir -File -Filter $Filter -ErrorAction SilentlyContinue | ForEach-Object {
if ($CopyResourcesToModuleRoot) {
$resourceFilePath += $_.Name
return
}

$resourceFilePath += Join-Path -Path 'resources' -ChildPath $_.Name
}

return $resourceFilePath
}

function Add-AllowedManifestParameterEntry {
[CmdletBinding()]
param(
[Parameter(Mandatory)][hashtable]$ManifestParameters,
[Parameter(Mandatory)][System.Collections.IDictionary]$Manifest,
[Parameter(Mandatory)][string[]]$AllowedParameter
)

foreach ($name in $Manifest.Keys) {
if ($name -eq 'PrivateData' -or $AllowedParameter -notcontains $name) {
continue
}

if ($Manifest.$name) {
$ManifestParameters.add($name, $Manifest.$name)
}
}
}
Loading
Loading