TauriCraft.psm1
#!/usr/bin/env pwsh using namespace System.Collections.Generic using namespace System.Management.Automation #Requires -Modules cliHelper.logger, PsModuleBase #region Enums and Classes # Target OS enum for GitHub Actions enum TargetOS { Windows = 0 MacOS = 1 Linux = 2 } class TargetOSConfig { [string] $Title [string] $Value [bool] $Selected TargetOSConfig([string]$title, [string]$value, [bool]$selected = $true) { $this.Title = $title $this.Value = $value $this.Selected = $selected } } class ProjectConfig { [string] $ProjectName [string] $PackageName [string[]] $ReleaseOS [bool] $Overwrite [string] $TargetDirectory [string] $PackageManager ProjectConfig() { $this.ReleaseOS = @() $this.Overwrite = $false $this.PackageManager = "npm" } } # Main class class TauriCraft : PsModuleBase { static [object] $Logger = $null static [TargetOSConfig[]] $TargetOperatingSystems = @( [TargetOSConfig]::new("Windows (x64)", "windows-latest", $true), [TargetOSConfig]::new("macOS (x64)", "macos-latest", $true), [TargetOSConfig]::new("Linux (x64)", "ubuntu-latest", $true) ) static [hashtable] $RenameFiles = @{ "_gitignore" = ".gitignore" } static [string] $DefaultTargetDir = "tauri-nextjs-app" # Initialize logger static [void] InitializeLogger() { if ($null -eq [TauriCraft]::Logger) { [TauriCraft]::Logger = New-Logger -Level 1 } } # Static method to get available OS targets static [string[]] GetAllOSTargets() { [TauriCraft]::InitializeLogger() [TauriCraft]::Logger.LogDebugLine("Getting available OS targets") return [TauriCraft]::TargetOperatingSystems | ForEach-Object { $_.Value } } # Main scaffolding method static [void] CreateProject([ProjectConfig] $config) { [TauriCraft]::CreateProject($config, $PSScriptRoot) } # Main scaffolding method with module root static [void] CreateProject([ProjectConfig] $config, [string] $moduleRoot) { [TauriCraft]::InitializeLogger() [TauriCraft]::Logger.LogInfoLine("Starting TauriCraft Next.js project creation") [TauriCraft]::Logger.LogInfoLine("Project: $($config.ProjectName)") try { [TauriCraft]::ValidateConfig($config) [TauriCraft]::SetupProjectDirectory($config) [TauriCraft]::CopyTemplateFiles($config, $moduleRoot) [TauriCraft]::ProcessConfigurationFiles($config, $moduleRoot) [TauriCraft]::ShowCompletionInstructions($config) [TauriCraft]::Logger.LogInfoLine("Project creation completed successfully") } catch { [TauriCraft]::Logger | Write-LogEntry -Level Error -Message "Project creation failed" -Exception $_.Exception throw } } # Validation method static [void] ValidateConfig([ProjectConfig] $config) { [TauriCraft]::Logger.LogDebugLine("Validating project configuration") if ([string]::IsNullOrWhiteSpace($config.ProjectName)) { [TauriCraft]::Logger.LogErrorLine("Project name validation failed: empty or null") throw "Project name cannot be empty" } if ([string]::IsNullOrWhiteSpace($config.PackageName)) { [TauriCraft]::Logger.LogErrorLine("Package name validation failed: empty or null") throw "Package name cannot be empty" } [TauriCraft]::Logger.LogInfoLine("Configuration validation passed") } # Directory setup method static [void] SetupProjectDirectory([ProjectConfig] $config) { $projectRoot = [IO.Path]::Combine((Get-Location) , $config.TargetDirectory) [TauriCraft]::Logger.LogInfoLine("Setting up project directory: $projectRoot") if ([IO.Directory]::Exists($projectRoot)) { [TauriCraft]::Logger.LogDebugLine("Target directory already exists") if ($config.Overwrite) { [TauriCraft]::Logger.LogWarnLine("Overwriting existing directory contents") [TauriCraft]::EmptyDirectory($projectRoot) } elseif (![TauriCraft]::IsDirectoryEmpty($projectRoot)) { [TauriCraft]::Logger.LogErrorLine("Target directory is not empty and overwrite not specified") throw "Target directory '$projectRoot' is not empty. Use -Force to overwrite." } } else { [TauriCraft]::Logger.LogDebugLine("Creating new project directory") New-Item -Path $projectRoot -ItemType Directory -Force | Out-Null } $config.TargetDirectory = $projectRoot [TauriCraft]::Logger.LogInfoLine("Project directory setup complete.") } # Template extraction method static [void] CopyTemplateFiles([ProjectConfig] $config, [string] $moduleRoot) { $templateZipPath = [IO.Path]::Combine($moduleRoot, "Private", "nextjs-template.zip") [TauriCraft]::Logger.LogDebugLine("Template zip path: $templateZipPath") if (![IO.File]::Exists($templateZipPath)) { [TauriCraft]::Logger.LogErrorLine("Template zip file not found: $templateZipPath") throw "Template zip file not found: $templateZipPath" } [TauriCraft]::Logger.LogInfoLine("Extracting Next.js template to $($config.TargetDirectory)") try { # Extract the template zip to the target directory Expand-Archive -Path $templateZipPath -DestinationPath $config.TargetDirectory -Force -Verbose:$false [TauriCraft]::Logger.LogInfoLine("Template extraction completed successfully") # Handle file renaming if needed (e.g., _gitignore -> .gitignore) [TauriCraft]::ProcessFileRenames($config) } catch { [TauriCraft]::Logger.LogErrorLine("Failed to extract template: $($_.Exception.Message)") throw "Failed to extract template: $($_.Exception.Message)" } } # Process file renames after extraction static [void] ProcessFileRenames([ProjectConfig] $config) { [TauriCraft]::Logger.LogDebugLine("Processing file renames") foreach ($oldName in [TauriCraft]::RenameFiles.Keys) { $newName = [TauriCraft]::RenameFiles[$oldName] $oldPath = Get-ChildItem $config.TargetDirectory -Recurse -File -Name $oldName -ErrorAction SilentlyContinue foreach ($file in $oldPath) { $fullOldPath = [IO.Path]::Combine($config.TargetDirectory, $file) $fullNewPath = [IO.Path]::Combine((Split-Path $fullOldPath -Parent), $newName) if ([IO.File]::Exists($fullOldPath)) { [TauriCraft]::Logger.LogDebugLine("Renaming: $file -> $newName") Move-Item $fullOldPath $fullNewPath -Force } } } } # Configuration file processing method static [void] ProcessConfigurationFiles([ProjectConfig] $config, [string] $moduleRoot) { [TauriCraft]::Logger.LogInfoLine("Processing configuration files...") # Process package.json (now in target directory) [TauriCraft]::ProcessPackageJson($config) # Process tauri.conf.json (now in target directory) [TauriCraft]::ProcessTauriConfig($config) # Process Cargo.toml (now in target directory) [TauriCraft]::ProcessCargoToml($config) # Process GitHub Actions release.yml (if exists in extracted template) [TauriCraft]::ProcessReleaseWorkflow($config) [TauriCraft]::Logger.LogInfoLine("Configuration file processing completed") } # Package.json processing static [void] ProcessPackageJson([ProjectConfig] $config) { $packageJsonPath = [IO.Path]::Combine($config.TargetDirectory, "package.json") [TauriCraft]::Logger.LogDebugLine("Processing package.json at: $packageJsonPath") if ([IO.File]::Exists($packageJsonPath)) { $packageJson = Get-Content $packageJsonPath -Raw | ConvertFrom-Json $packageJson.name = $config.PackageName $packageJson | ConvertTo-Json -Depth 10 | Set-Content $packageJsonPath -Encoding UTF8 [TauriCraft]::Logger.LogInfoLine("Updated package.json with name: $($config.PackageName)") } else { [TauriCraft]::Logger.LogWarnLine("package.json not found in extracted template") } } # Tauri configuration processing static [void] ProcessTauriConfig([ProjectConfig] $config) { $tauriConfigPath = [IO.Path]::Combine($config.TargetDirectory, "src-tauri", "tauri.conf.json") [TauriCraft]::Logger.LogDebugLine("Processing tauri.conf.json at: $tauriConfigPath") if ([IO.File]::Exists($tauriConfigPath)) { $tauriConfig = Get-Content $tauriConfigPath -Raw | ConvertFrom-Json # Update the product name and window title based on the actual structure if ($tauriConfig.productName) { $tauriConfig.productName = $config.PackageName [TauriCraft]::Logger.LogDebugLine("Updated productName to: $($config.PackageName)") } if ($tauriConfig.app -and $tauriConfig.app.windows -and $tauriConfig.app.windows.Count -gt 0) { $tauriConfig.app.windows[0].title = $config.PackageName [TauriCraft]::Logger.LogDebugLine("Updated window title to: $($config.PackageName)") } $tauriConfig | ConvertTo-Json -Depth 10 | Set-Content $tauriConfigPath -Encoding UTF8 [TauriCraft]::Logger.LogInfoLine("Updated tauri.conf.json configuration") } else { [TauriCraft]::Logger.LogWarnLine("tauri.conf.json not found in extracted template") } } # Cargo.toml processing static [void] ProcessCargoToml([ProjectConfig] $config) { $cargoPath = [IO.Path]::Combine($config.TargetDirectory, "src-tauri", "Cargo.toml") [TauriCraft]::Logger.LogDebugLine("Processing Cargo.toml at: $cargoPath") if ([IO.File]::Exists($cargoPath)) { $cargoContent = Get-Content $cargoPath -Raw $updatedContent = $cargoContent -replace 'name\s*=\s*"[^"]*"', "name = `"$($config.PackageName)`"" Set-Content $cargoPath $updatedContent -Encoding UTF8 [TauriCraft]::Logger.LogInfoLine("Set app name to $($config.PackageName)") } else { [TauriCraft]::Logger.LogWarnLine("Cargo.toml not found in extracted template") } } # GitHub Actions release workflow processing static [void] ProcessReleaseWorkflow([ProjectConfig] $config) { $releaseYmlPath = [IO.Path]::Combine($config.TargetDirectory, ".github", "workflows", "release.yml") [TauriCraft]::Logger.LogDebugLine("Processing release.yml at: $releaseYmlPath") if ([IO.File]::Exists($releaseYmlPath)) { $releaseContent = Get-Content $releaseYmlPath -Raw $allOS = [TauriCraft]::GetAllOSTargets() $selectedOS = $config.ReleaseOS -join ", " $comment = "" if ($config.ReleaseOS.Count -lt $allOS.Count) { $excludedOS = $allOS | Where-Object { $_ -notin $config.ReleaseOS } $comment = " # $($excludedOS -join ', ')" } $updatedContent = $releaseContent -replace "platform: \[macos-latest, ubuntu-latest, windows-latest\]", "platform: [$selectedOS]$comment" Set-Content $releaseYmlPath $updatedContent -Encoding UTF8 [TauriCraft]::Logger.LogInfoLine("Updated GitHub Actions workflow for platforms: $selectedOS") } else { [TauriCraft]::Logger.LogWarnLine("release.yml not found in extracted template") } } # Completion instructions static [void] ShowCompletionInstructions([ProjectConfig] $config) { $relativePath = Resolve-Path $config.TargetDirectory -Relative Write-Host "`nNext.js Tauri project created successfully! 🎉" -ForegroundColor Green Write-Host "Log file saved in $([TauriCraft]::Logger.Logdir)" Write-Host "`nTo get started:" -ForegroundColor Yellow if ($config.TargetDirectory -ne (Get-Location).Path) { if ($relativePath.Contains(" ")) { Write-Host " cd `"$relativePath`"" -ForegroundColor Cyan } else { Write-Host " cd $relativePath" -ForegroundColor Cyan } } switch ($config.PackageManager) { "yarn" { Write-Host " yarn install" -ForegroundColor Cyan Write-Host " yarn tauri dev" -ForegroundColor Cyan } "pnpm" { Write-Host " pnpm install" -ForegroundColor Cyan Write-Host " pnpm tauri dev" -ForegroundColor Cyan } default { Write-Host " $($config.PackageManager) install" -ForegroundColor Cyan Write-Host " $($config.PackageManager) run tauri dev" -ForegroundColor Cyan } } Write-Host "" } # Utility methods static [bool] IsDirectoryEmpty([string] $path) { if (![IO.Path]::Exists($path)) { return $true } $items = Get-ChildItem $path -Force return $items.Count -eq 0 -or ($items.Count -eq 1 -and $items[0].Name -eq ".git") } static [void] EmptyDirectory([string] $path) { if (![IO.Path]::Exists($path)) { return } Get-ChildItem $path -Force | Where-Object { $_.Name -ne ".git" } | Remove-Item -Recurse -Force } static [bool] IsValidPackageName([string] $name) { return $name -match '^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$' } static [string] ToValidPackageName([string] $name) { return $name.Trim().ToLower() -replace '\s+', '-' -replace '^[._]', '' -replace '[^a-z\d\-~]+', '-' } static [string] FormatTargetDirectory([string] $targetDir) { if ([string]::IsNullOrWhiteSpace($targetDir)) { return "" } return $targetDir.Trim() -replace '/+$', '' } static [hashtable] DetectPackageManager() { $userAgent = $env:npm_config_user_agent if ([string]::IsNullOrWhiteSpace($userAgent)) { return @{ name = "npm"; version = "" } } $pkgSpec = $userAgent.Split(" ")[0] $pkgSpecArr = $pkgSpec.Split("/") return @{ name = $pkgSpecArr[0] version = if ($pkgSpecArr.Length -gt 1) { $pkgSpecArr[1] } else { "" } } } static [void] ShowTemplateInfo() { [TauriCraft]::ShowTemplateInfo($true) } static [void] ShowTemplateInfo([bool]$full) { Write-Host "TauriCraft - Next.js Template" -ForegroundColor Green Write-Host "=============================" -ForegroundColor Green Write-Host "" Write-Host "tech stack: " -NoNewline -ForegroundColor Yellow Write-Host "▲ Next.js + 🦀 Tauri-v2 backend" -ForegroundColor Cyan Write-Host " Best for:" -NoNewline -ForegroundColor Gray Write-Host "🔥 Blazingly fast, full-stack desktop apps with small bundle size" -ForegroundColor White Write-Host "" if ($Full) { Write-Host "Target OS" -ForegroundColor Green Write-Host "=========" -ForegroundColor Green Write-Host "" $targetOS = [TauriCraft]::TargetOperatingSystems foreach ($os in $targetOS) { Write-Host "• " -NoNewline -ForegroundColor Yellow Write-Host $os.Title -NoNewline -ForegroundColor Cyan Write-Host " (" -NoNewline -ForegroundColor Gray Write-Host $os.Value -NoNewline -ForegroundColor White Write-Host ")" -ForegroundColor Gray } Write-Host "" } Write-Host "Usage Examples:" -ForegroundColor Green Write-Host " New-TauriProject" -ForegroundColor Cyan Write-Host " New-TauriProject -Name 'my-app'" -ForegroundColor Cyan Write-Host " New-TauriProject -Interactive" -ForegroundColor Cyan Write-Host "" } # Dispose logger resources static [void] DisposeLogger() { if ($null -ne [TauriCraft]::Logger) { [TauriCraft]::Logger.LogInfoLine("Disposing TauriCraft logger") [TauriCraft]::Logger.Dispose() [TauriCraft]::Logger = $null } } } #endregion Classes # Types that will be available to users when they import the module. $typestoExport = @( [TauriCraft], [TargetOSConfig], [ProjectConfig], [TargetOS] ) $TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators') foreach ($Type in $typestoExport) { if ($Type.FullName -in $TypeAcceleratorsClass::Get.Keys) { $Message = @( "Unable to register type accelerator '$($Type.FullName)'" 'Accelerator already exists.' ) -join ' - ' "TypeAcceleratorAlreadyExists $Message" | Write-Debug } } # Add type accelerators for every exportable type. foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Add($Type.FullName, $Type) } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { # Dispose logger resources [TauriCraft]::DisposeLogger() # Remove type accelerators foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure(); $scripts = @(); $Public = Get-ChildItem "$PSScriptRoot/Public" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += Get-ChildItem "$PSScriptRoot/Private" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += $Public foreach ($file in $scripts) { try { if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue } . "$($file.fullname)" } catch { Write-Warning "Failed to import function $($file.BaseName): $_" $host.UI.WriteErrorLine($_) } } $Param = @{ Function = $Public.BaseName Cmdlet = '*' Alias = '*' Verbose = $false } Export-ModuleMember @Param |