process/bundleBuilder.psm1
|
using module ..\models\bundlerConfig.psm1 using module ..\models\fileInfo.psm1 class BundleBuilder { [BundlerConfig]$_config BundleBuilder ([BundlerConfig]$config) { $this._config = $config } [string]build([hashtable]$importsMap, [hashtable]$replacementsInfo, [string]$bundleName) { try { $entryFile = $this.GetEntryFile($importsMap) $bundleName = $this.GetBundleName($bundleName, $entryFile) $outputPath = Join-Path $this._config.outDir $bundleName if ((Test-Path $outputPath)) { Remove-Item -Path $outputPath -Force } New-Item -ItemType Directory -Force -Path (Split-Path $outputPath) | Out-Null $headerContent = $this.getHeaders($replacementsInfo) $modulesContent = $this.getModulesContent($entryFile, $replacementsInfo) $this.addContentToFile($outputPath, $headerContent) $this.addContentToFile($outputPath, $modulesContent) return $outputPath } catch { throw "HANDLED: Error creating bundle: $($_.Exception.Message)" } } [string]getHeaders ([hashtable]$replacementsInfo) { $result = "" if ($replacementsInfo.headerComments) { $result += ( $replacementsInfo.headerComments + [Environment]::NewLine * 2) } $assemblies = $this.getNamespacesString($replacementsInfo.assemblies) if ($assemblies) { $result += ($assemblies + [Environment]::NewLine * 2) } $namespaces = $this.getNamespacesString($replacementsInfo.namespaces) if ($namespaces) { $result += ($namespaces + [Environment]::NewLine * 2) } if ($replacementsInfo.paramBlock) { $result += ($replacementsInfo.paramBlock + [Environment]::NewLine * 2) } $addTypes = $this.getAddTypesString($replacementsInfo.addTypes) if ($addTypes -and $result) { $result += ( $addTypes + [Environment]::NewLine * 2) } $classes = $this.getClassesString($replacementsInfo.classes) if ($classes) { $result += ($classes + [Environment]::NewLine * 2) } return $result } [string]getAssembliesString ([System.Collections.Specialized.OrderedDictionary]$assemblies) { return $assemblies.Values -join [Environment]::NewLine } [string]getNamespacesString ([System.Collections.Specialized.OrderedDictionary]$namespaces) { return $namespaces.Values -join [Environment]::NewLine } [string]getAddTypesString ([System.Collections.Specialized.OrderedDictionary]$addTypes) { return $addTypes.Values -join [Environment]::NewLine } [string]getClassesString ([System.Collections.Specialized.OrderedDictionary]$classes) { if ($classes.Count -eq 0) { return "" } $classesStr = $classes.Values -join ([Environment]::NewLine + [Environment]::NewLine) if (-not $this._config.deferClassesCompilation) { return $classesStr } $uuid = [Guid]::NewGuid().ToString("N") if (-not $this._config.embedClassesAsBase64) { return "`$__CLASSES_SOURCE_$uuid = @'" + [Environment]::NewLine ` + $classesStr + [Environment]::NewLine ` + "'@" + [Environment]::NewLine ` + "Invoke-Expression `$__CLASSES_SOURCE_$uuid" + [Environment]::NewLine ` + "`$__CLASSES_SOURCE_$uuid = `$null" } $bytes = [Text.Encoding]::UTF8.GetBytes($classesStr) $classesStr = [Convert]::ToBase64String($bytes) return "`$__CLASSES_B64_$uuid = '$classesStr'" + [Environment]::NewLine ` + "`$__CLASSES_BYTES_$uuid = [System.Convert]::FromBase64String(`$__CLASSES_B64_$uuid)" + [Environment]::NewLine ` + "`$__CLASSES_SOURCE_$uuid = [System.Text.Encoding]::UTF8.GetString(`$__CLASSES_BYTES_$uuid)" + [Environment]::NewLine ` + "Invoke-Expression `$__CLASSES_SOURCE_$uuid" + [Environment]::NewLine ` + "`$__CLASSES_BYTES_$uuid = `$null" + [Environment]::NewLine ` + "`$__CLASSES_SOURCE_$uuid = `$null" + [Environment]::NewLine ` + "`$__CLASSES_B64_$uuid = `$null" } [FileInfo]getEntryFile ([hashtable]$importsMap) { foreach ($file in $importsMap.Values) { if ($file.isEntry) { return $file } } throw "Entry file is not found in imports map" } [hashtable[]]normalizeReplacements([hashtable[]] $replacements) { # WORKAROUND: System.Collections.ArrayList may unfold hashtables when sorting. So we must use [hashtable[]] [hashtable[]]$sorted = $replacements | Sort-Object { $_['Start'] } $normalized = @() if ($sorted.Count -eq 0) { return $normalized } $current = $sorted[0] for ($i = 1; $i -lt $sorted.Count; $i++) { $r = $sorted[$i] $currStart = [int]$current.Start $currEnd = $currStart + [int]$current.Length $rStart = [int]$r.Start $rEnd = $rStart + [int]$r.Length if ($rStart -lt $currEnd) { # Merge overlapping $newStart = [Math]::Min($currStart, $rStart) $newEnd = [Math]::Max($currEnd, $rEnd) $newLength = $newEnd - $newStart $current = @{ Start = $newStart Length = $newLength Replacement = "$($current.Replacement)$($r.Replacement)" } } else { # Commit current and move on $normalized += $current $current = $r } } # Add the final one $normalized += $current [hashtable[]]$sortedNormalized = $normalized | Sort-Object { $_['Start'] } -Descending return $sortedNormalized } [string]PrepareSource ([FileInfo]$file, [System.Collections.ArrayList]$replacements) { $source = $file.ast.Extent.Text $sb = [System.Text.StringBuilder]::new($source) $replacements = $this.NormalizeReplacements($replacements) #$replacements = $replacements | Sort-Object { $_['Start'] } -Descending foreach ($r in $replacements) { $sb.Remove($r.Start, $r.Length) $sb.Insert($r.Start, $r.Value) } return $sb.ToString().Trim() } [string]getModulesContent([FileInfo]$entryFile, [hashtable]$replacementsInfo) { $contentList = [System.Collections.ArrayList]::new() $contentList.Add('$global:' + $this._config.modulesSourceMapVarName + ' = @{}' + [Environment]::NewLine) $this.fillModulesContentList($entryFile, $replacementsInfo, $contentList, "", @{}) if ($contentList.Count -eq 1) { return "" } return $contentList -join [Environment]::NewLine * 2 } [void]fillModulesContentList([FileInfo]$file, [hashtable]$replacementsInfo, [System.Collections.ArrayList]$contentList, [string]$importType, [hashtable]$processed = @{}) { if ($file.imports.Values.Count -gt 0) { foreach ($importInfo in $file.imports.Values) { $importFile = $importInfo.file if ($processed[$importFile.path]) { continue } $this.fillModulesContentList($importFile, $replacementsInfo, $contentList, $importInfo.Type, $processed) } } $processed[$file.path] = $true if ($file.typesOnly) { Write-Host " File '$($file.path)' processed." -ForegroundColor Green; return } $source = $this.PrepareSource($file, $replacementsInfo.replacementsMap[$file.id]) if (-not $source) { Write-Host " File '$($file.path)' processed." -ForegroundColor Green; return } if (-not $file.isEntry) { $source = '$global:' + $this._config.modulesSourceMapVarName + '["' + $file.id + '"] = ' + $this.bracketWrap($source, " ") } $contentList.Add($source) Write-Host " File '$($file.path)' processed." -ForegroundColor Green return } # Wraps string in { ... } and make indents [string]bracketWrap([string]$str, [string]$indent = " ") { return "{" + [Environment]::NewLine + (($str -split "\r?\n" | ForEach-Object { "$indent$_" }) -join [Environment]::NewLine) + [Environment]::NewLine + "}" } [void]addContentToFile([string]$path, [string]$content) { Add-Content -Path $path -Value $content -Encoding UTF8 | Out-Null } [string]GetBundleName ($bundleName, [FileInfo]$entryFile) { $version = $this.ParseVersion($entryFile) if (-not $version) { return $bundleName } Write-Verbose " Bundle version detected: $version" $name = [System.IO.Path]::GetFileNameWithoutExtension($bundleName) $ext = [System.IO.Path]::GetExtension($bundleName) return "$name-$version$ext" } [string]ParseVersion([FileInfo]$file) { $tokens = $file.tokens # English comment: regex for "#version: 1.2.3" (accepts 1 or 2 dots) $versionRegex = '#\s*version[:]?\s*([0-9]+(?:\.[0-9]+){0,3})' $tokenKind = [System.Management.Automation.Language.TokenKind] foreach ($token in $tokens) { if ($token.Kind -ne $tokenKind::Comment -and $token.Kind -ne $tokenKind::NewLine) { break } if ($token.Extent.Text -match $versionRegex) { return $matches[1] } } return "" } } |