core/pretty_print.go
core/pretty_print.goBrowse 1970 files
2,504 tokens
9,386 bytes
Token encoding: o200k_base
Snapshot 21a254f
← Back to SKILL.md
1package core2 3import (4 "fmt"5 "io"6 "slices"7 "strings"8 9 "github.com/charmbracelet/lipgloss"10 "github.com/muesli/termenv"11 "github.com/railwayapp/railpack/core/logger"12 "github.com/railwayapp/railpack/core/plan"13 "github.com/railwayapp/railpack/core/resolver"14 "github.com/railwayapp/railpack/internal/utils"15 "github.com/tidwall/pretty"16)17 18const (19 AnsiRed = "1"20 AnsiYellow = "3"21 AnsiBlue = "4"22 AnsiMagenta = "5"23 AnsiCyan = "6"24 AnsiWhite = "7"25 AnsiGray = "8"26 AnsiBrightBlue = "12"27 AnsiBrightCyan = "14"28 AnsiBrightWhite = "15"29 AnsiBrightMagenta = "13"30 AnsiDarkGray = "238"31 AnsiMediumGray = "245"32)33 34var (35 highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}36 37 headerStyle = lipgloss.NewStyle().38 BorderStyle(lipgloss.RoundedBorder()).39 BorderForeground(highlight).40 MarginTop(1).41 Padding(0, 1)42 43 sectionHeaderStyle = lipgloss.NewStyle().44 Bold(true).45 Width(10).46 MarginLeft(2).47 MarginTop(2).48 BorderStyle(lipgloss.RoundedBorder()).49 BorderForeground(lipgloss.Color(AnsiDarkGray)).50 BorderBottom(true)51 52 packageNameStyle = lipgloss.NewStyle().53 MarginLeft(2).54 Foreground(lipgloss.Color(AnsiBrightMagenta))55 56 versionStyle = lipgloss.NewStyle().57 Foreground(lipgloss.Color(AnsiBrightCyan))58 59 sourceStyle = lipgloss.NewStyle()60 61 separatorStyle = lipgloss.NewStyle().62 Foreground(lipgloss.Color(AnsiDarkGray)).63 Margin(0, 2)64 65 indentedStepHeaderStyle = lipgloss.NewStyle().66 Foreground(lipgloss.Color(AnsiBrightMagenta)).67 MarginLeft(2)68 69 commandPrefixStyle = lipgloss.NewStyle().70 Foreground(lipgloss.Color(AnsiMediumGray)).71 MarginLeft(4)72 73 commandStyle = lipgloss.NewStyle().74 Bold(true)75 76 logInfoStyle = lipgloss.NewStyle().77 Foreground(lipgloss.Color(AnsiBrightWhite)).78 MarginLeft(2)79 80 logWarnStyle = lipgloss.NewStyle().81 Foreground(lipgloss.Color(AnsiYellow)).82 MarginLeft(2)83 84 logDeprecationStyle = lipgloss.NewStyle().85 Foreground(lipgloss.Color(AnsiBrightMagenta)).86 MarginLeft(2)87 88 logSuggestionStyle = lipgloss.NewStyle().89 Foreground(lipgloss.Color(AnsiCyan)).90 MarginLeft(2)91 92 logDocsLinkStyle = lipgloss.NewStyle().93 Foreground(lipgloss.Color(AnsiBrightBlue)).94 Underline(true)95 96 logErrorStyle = lipgloss.NewStyle().97 Foreground(lipgloss.Color(AnsiRed)).98 Bold(true).99 MarginLeft(2)100 101 metadataStyle = lipgloss.NewStyle().102 MarginLeft(2)103 104 metadataSeparatorStyle = lipgloss.NewStyle().105 Foreground(lipgloss.Color(AnsiMediumGray)).106 MarginRight(1)107 108 metadataValueStyle = lipgloss.NewStyle().109 Bold(true)110 111 boxStyle = lipgloss.NewStyle().112 BorderStyle(lipgloss.RoundedBorder()).113 BorderForeground(highlight).114 MarginTop(1).115 Padding(0, 1)116 117 highlightedTextStyle = lipgloss.NewStyle().118 Bold(true).119 Foreground(lipgloss.Color(AnsiBrightCyan))120)121 122type PrintOptions struct {123 Metadata bool124 Version string125}126 127func PrettyPrintBuildResult(buildResult *BuildResult, options ...PrintOptions) {128 output := FormatBuildResult(buildResult, options...)129 fmt.Print(output)130}131 132func PrettyPrintSectionHeader(output io.Writer, title string) {133 style := sectionHeaderStyle.134 MarginTop(0).135 Width(max(10, lipgloss.Width(title)))136 _, _ = fmt.Fprintf(output, "%s\n\n", style.Render(title))137}138 139func PrettyPrintBox(content string) {140 fmt.Println(boxStyle.Render(content))141}142 143func FormatHighlight(content string) string {144 return highlightedTextStyle.Render(content)145}146 147func PrettyPrintJSON(output io.Writer, content []byte) {148 if lipgloss.ColorProfile() != termenv.Ascii {149 content = pretty.Color(content, nil)150 }151 152 _, _ = fmt.Fprintln(output, string(content))153}154 155func FormatBuildResult(br *BuildResult, options ...PrintOptions) string {156 var opts PrintOptions157 if len(options) > 0 {158 opts = options[0]159 }160 var output strings.Builder161 162 formatHeader(&output, opts.Version)163 formatLogs(&output, br.Logs)164 formatPackages(&output, br.ResolvedPackages)165 formatSteps(&output, br)166 formatDeploy(&output, br)167 formatMetadata(&output, br.Metadata, opts.Metadata)168 169 output.WriteString("\n\n")170 return output.String()171}172 173func formatHeader(output *strings.Builder, version string) {174 header := fmt.Sprintf("Railpack %s", version)175 output.WriteString(headerStyle.Render(header))176 output.WriteString("\n")177}178 179func formatLogs(output *strings.Builder, logs []logger.Msg) {180 if len(logs) == 0 {181 return182 }183 184 output.WriteString("\n")185 186 for _, log := range logs {187 msg := utils.CapitalizeFirst(log.Msg)188 189 switch log.Level {190 case logger.Info:191 output.WriteString(logInfoStyle.Render(fmt.Sprintf("↳ %s", msg)))192 case logger.Warn:193 output.WriteString(logWarnStyle.Render(fmt.Sprintf("⚠ %s", msg)))194 case logger.Deprecation:195 output.WriteString(logDeprecationStyle.Render(fmt.Sprintf("⚑ Deprecated: %s", msg)))196 case logger.Suggestion:197 rendered := logSuggestionStyle.Render(fmt.Sprintf("→ %s", msg))198 if log.DocsPath != "" {199 rendered += " " + logDocsLinkStyle.Render(logger.DocsURL(log.DocsPath))200 }201 output.WriteString(rendered)202 case logger.Error:203 lines := strings.Split(msg, "\n")204 for i, line := range lines {205 if i == 0 {206 output.WriteString(logErrorStyle.Render(fmt.Sprintf("✖ %s", line)))207 } else {208 fmt.Fprintf(output, " %s", line)209 }210 if i < len(lines)-1 {211 output.WriteString("\n")212 }213 }214 default:215 output.WriteString(logInfoStyle.Render(fmt.Sprintf("• %s", msg)))216 }217 output.WriteString("\n")218 }219}220 221func formatPackages(output *strings.Builder, packages map[string]*resolver.ResolvedPackage) {222 if len(packages) == 0 {223 return224 }225 226 output.WriteString(sectionHeaderStyle.MarginTop(1).Render("Packages"))227 output.WriteString("\n")228 229 nameWidth, versionWidth := 1, 1230 for _, pkg := range packages {231 nameWidth = max(nameWidth, len(pkg.Name))232 if pkg.ResolvedVersion != nil {233 versionWidth = max(versionWidth, len(*pkg.ResolvedVersion))234 }235 }236 237 localPackageNameStyle := packageNameStyle.Width(nameWidth).MaxWidth(20)238 localVersionStyle := versionStyle.Width(versionWidth).MaxWidth(20)239 separator := separatorStyle.Render("│")240 241 for _, pkg := range packages {242 name := localPackageNameStyle.Render(pkg.Name)243 244 version := "-"245 if pkg.ResolvedVersion != nil {246 version = *pkg.ResolvedVersion247 }248 version = localVersionStyle.Render(version)249 source := sourceStyle.Render(formatSource(pkg))250 fmt.Fprintf(output, "%s%s%s%s%s", name, separator, version, separator, source)251 output.WriteString("\n")252 }253}254 255func formatSteps(output *strings.Builder, br *BuildResult) {256 stepsToPrint := getStepsToPrint(br)257 if len(stepsToPrint) == 0 {258 return259 }260 261 output.WriteString(sectionHeaderStyle.MarginTop(1).Render("Steps"))262 output.WriteString("\n")263 264 for i, step := range stepsToPrint {265 commands := getCommandsToPrint(step.Commands)266 if len(commands) == 0 {267 continue268 }269 270 currentStepStyle := indentedStepHeaderStyle271 if i > 0 {272 currentStepStyle = currentStepStyle.MarginTop(1)273 }274 275 output.WriteString(currentStepStyle.Render(fmt.Sprintf("▸ %s", step.Name)))276 output.WriteString("\n")277 278 for _, cmd := range commands {279 cmdText := cmd.Cmd280 if cmd.CustomName != "" {281 cmdText = cmd.CustomName282 }283 fmt.Fprintf(output, "%s %s", commandPrefixStyle.Render("$"), commandStyle.Render(cmdText))284 output.WriteString("\n")285 }286 }287}288 289func formatDeploy(output *strings.Builder, br *BuildResult) {290 if br.Plan != nil && br.Plan.Deploy.StartCmd != "" {291 output.WriteString(sectionHeaderStyle.MarginTop(1).Render("Deploy"))292 output.WriteString("\n")293 fmt.Fprintf(output, "%s %s", commandPrefixStyle.Render("$"), commandStyle.Render(br.Plan.Deploy.StartCmd))294 }295}296 297func formatMetadata(output *strings.Builder, metadata map[string]string, showMetadata bool) {298 if !showMetadata || metadata == nil || len(metadata) == 0 {299 return300 }301 302 output.WriteString(sectionHeaderStyle.MarginTop(2).Render("Metadata"))303 output.WriteString("\n")304 305 separator := metadataSeparatorStyle.Render(":")306 307 for key, value := range metadata {308 output.WriteString(metadataStyle.Render(fmt.Sprintf("%s%s%s", key, separator, metadataValueStyle.Render(value))))309 output.WriteString("\n")310 }311}312 313func getStepsToPrint(br *BuildResult) []*plan.Step {314 execSteps := []*plan.Step{}315 if br.Plan == nil {316 return execSteps317 }318 319 for _, step := range br.Plan.Steps {320 if !strings.HasPrefix(step.Name, "packages") && step.Commands != nil {321 commands := getCommandsToPrint(step.Commands)322 if len(commands) > 0 {323 execSteps = append(execSteps, &step)324 }325 }326 }327 return execSteps328}329 330// TODO we should really just mark a command as skippable on the ExecCommand instead331var skippableCommands = []string{332 "mkdir -p /app/node_modules/.cache",333 "mkdir -p config deps _build",334}335 336func getCommandsToPrint(commands []plan.Command) []plan.ExecCommand {337 if commands == nil {338 return []plan.ExecCommand{}339 }340 341 execCommands := []plan.ExecCommand{}342 for _, cmd := range commands {343 if execCmd, ok := cmd.(plan.ExecCommand); ok {344 if !isSkippableCommand(execCmd.Cmd) {345 execCommands = append(execCommands, execCmd)346 }347 }348 }349 return execCommands350}351 352func isSkippableCommand(cmd string) bool {353 return slices.Contains(skippableCommands, cmd)354}355 356func formatSource(pkg *resolver.ResolvedPackage) string {357 if pkg.RequestedVersion != nil {358 return fmt.Sprintf("%s (%s)", pkg.Source, *pkg.RequestedVersion)359 }360 return pkg.Source361}362