Skip to content

Commit

Permalink
Merge 8d0afd9 into 096b8b8
Browse files Browse the repository at this point in the history
  • Loading branch information
tknickman committed Dec 5, 2022
2 parents 096b8b8 + 8d0afd9 commit 603c2de
Show file tree
Hide file tree
Showing 8 changed files with 611 additions and 3 deletions.
289 changes: 286 additions & 3 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ members = [
"crates/turbopack-swc-utils",
"crates/turbopack",
"crates/turbopack-tests",
"crates/turbo-updater",
"shim",
"xtask",
]
Expand Down
16 changes: 16 additions & 0 deletions crates/turbo-updater/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "turbo-updater"
version = "0.1.0"
edition = "2021"
description = "Minimal wrapper around update-informer to provide npm registry support and consistent UI"
license = "MPL-2.0"
publish = false

[dependencies]
colored = "2.0"
serde = { version = "1.0.126", features = ["derive"] }
strip-ansi-escapes = "0.1.1"
terminal_size = "0.2"
thiserror = "1.0"
update-informer = "0.5.0"
ureq = { version = "2.3.0", features = ["json"] }
76 changes: 76 additions & 0 deletions crates/turbo-updater/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::time::Duration;

use colored::*;
use serde::Deserialize;
use thiserror::Error as ThisError;
use update_informer::{Check, Package, Registry, Result as UpdateResult};

mod ui;

const DEFAULT_TIMEOUT: Duration = Duration::from_millis(800);
const DEFAULT_INTERVAL: Duration = Duration::from_secs(60 * 60 * 24);

#[derive(ThisError, Debug)]
pub enum UpdateNotifierError {
#[error("Failed to write to terminal")]
RenderError(#[from] ui::utils::GetDisplayLengthError),
}

#[derive(Deserialize)]
struct NpmVersionData {
version: String,
}

struct NPMRegistry;

impl Registry for NPMRegistry {
const NAME: &'static str = "npm_registry";
fn get_latest_version(pkg: &Package, _timeout: Duration) -> UpdateResult<Option<String>> {
let url = format!(
"https://turbo.build/api/binaries/latest?package={pkg}",
pkg = pkg
);
let resp = ureq::get(&url).timeout(_timeout).call()?;
let result = resp.into_json::<NpmVersionData>().unwrap();
Ok(Some(result.version))
}
}

pub fn check_for_updates(
package_name: &str,
github_repo: &str,
footer: Option<&str>,
current_version: &str,
timeout: Option<Duration>,
interval: Option<Duration>,
) -> Result<(), UpdateNotifierError> {
let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT);
let interval = interval.unwrap_or(DEFAULT_INTERVAL);
let informer = update_informer::new(NPMRegistry, package_name, current_version)
.timeout(timeout)
.interval(interval);
if let Ok(Some(version)) = informer.check_version() {
let latest_version = version.to_string();
let msg = format!(
"
Update available {version_prefix}{current_version} ≫ {latest_version}
Changelog: {github_repo}/releases/tag/{latest_version}
Run \"{update_cmd}\" to update
",
version_prefix = "v".dimmed(),
current_version = current_version.dimmed(),
latest_version = latest_version.green().bold(),
github_repo = github_repo,
// TODO: make this package manager aware
update_cmd = "npm i -g turbo".cyan().bold(),
);

if let Some(footer) = footer {
return ui::message(&format!("{}\n{}", msg, footer));
}

return ui::message(&msg);
}

Ok(())
}
61 changes: 61 additions & 0 deletions crates/turbo-updater/src/ui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use terminal_size::{terminal_size, Width};

use crate::UpdateNotifierError;
pub mod utils;

const DEFAULT_PADDING: usize = 8;

pub fn message(text: &str) -> Result<(), UpdateNotifierError> {
let size = terminal_size();
let lines: Vec<&str> = text.split('\n').map(|line| line.trim()).collect();

// get the display width of each line so we can center it within the box later
let lines_display_width = lines
.iter()
.map(|line| utils::get_display_length(line))
.collect::<Result<Vec<_>, _>>()?;

// find the longest line to determine layout
let longest_line = lines_display_width
.iter()
.max()
.copied()
.unwrap_or_default();
let full_message_width = longest_line + DEFAULT_PADDING;

// create a curried render function to reduce verbosity when calling
let render_at_layout = |layout: utils::Layout, width: usize| {
utils::render_message(
layout,
width,
lines,
lines_display_width,
full_message_width,
)
};

// render differently depending on viewport
if let Some((Width(term_width), _)) = size {
// if possible, pad this value slightly
let term_width = if term_width > 2 {
usize::from(term_width) - 2
} else {
term_width.into()
};

let can_fit_box = term_width >= full_message_width;
let can_center_text = term_width >= longest_line;

if can_fit_box {
render_at_layout(utils::Layout::Large, term_width);
} else if can_center_text {
render_at_layout(utils::Layout::Medium, term_width);
} else {
render_at_layout(utils::Layout::Small, term_width);
}
} else {
render_at_layout(utils::Layout::Unknown, 0);
}

Ok(())
}
150 changes: 150 additions & 0 deletions crates/turbo-updater/src/ui/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use std::{io::Error as IOError, string::FromUtf8Error};

use colored::*;
use strip_ansi_escapes::strip as strip_ansi_escapes;
use thiserror::Error as ThisError;

pub enum BorderAlignment {
Divider,
Top,
Bottom,
}

pub enum Layout {
Unknown,
Small,
Medium,
Large,
}

const TOP_LEFT: &str = "╭";
const TOP_RIGHT: &str = "╮";
const BOTTOM_LEFT: &str = "╰";
const BOTTOM_RIGHT: &str = "╯";
const HORIZONTAL: &str = "─";
const VERTICAL: &str = "│";
const SPACE: &str = " ";

#[derive(ThisError, Debug)]
pub enum GetDisplayLengthError {
#[error("Could not strip ANSI escape codes from string")]
StripError(#[from] IOError),
#[error("Could not convert to string")]
ConvertError(#[from] FromUtf8Error),
}

pub fn get_display_length(line: &str) -> Result<usize, GetDisplayLengthError> {
// strip any ansi escape codes (for color)
let stripped = strip_ansi_escapes(line)?;
let stripped = String::from_utf8(stripped)?;
// count the chars instead of the bytes (for unicode)
return Ok(stripped.chars().count());
}

pub fn x_border(width: usize, position: BorderAlignment) {
match position {
BorderAlignment::Top => {
println!(
"{}{}{}",
TOP_LEFT.yellow(),
HORIZONTAL.repeat(width).yellow(),
TOP_RIGHT.yellow()
);
}
BorderAlignment::Bottom => {
println!(
"{}{}{}",
BOTTOM_LEFT.yellow(),
HORIZONTAL.repeat(width).yellow(),
BOTTOM_RIGHT.yellow()
);
}
BorderAlignment::Divider => {
println!("{}", HORIZONTAL.repeat(width).yellow(),);
}
}
}

pub fn render_message(
layout: Layout,
width: usize,
lines: Vec<&str>,
lines_display_width: Vec<usize>,
full_message_width: usize,
) {
match layout {
// Left aligned text with no border.
// Used when term width is unknown.
Layout::Unknown => {
for line in lines.iter() {
println!("{}", line);
}
}

// Left aligned text with top and bottom border.
// Used when text cannot be centered without wrapping
Layout::Small => {
x_border(width, BorderAlignment::Divider);
for (line, line_display_width) in lines.iter().zip(lines_display_width.iter()) {
if *line_display_width == 0 {
println!("{}", SPACE.repeat(width));
} else {
println!("{}", line);
}
}
x_border(width, BorderAlignment::Divider);
}

// Centered text with top and bottom border.
// Used when text can be centered without wrapping, but
// there isn't enough room to include the box with padding.
Layout::Medium => {
x_border(width, BorderAlignment::Divider);
for (line, line_display_width) in lines.iter().zip(lines_display_width.iter()) {
if *line_display_width == 0 {
println!("{}", SPACE.repeat(width));
} else {
let line_padding = (width - line_display_width) / 2;
// for lines of odd length, tack the reminder to the end
let line_padding_remainder = width - (line_padding * 2) - line_display_width;
println!(
"{}{}{}",
SPACE.repeat(line_padding),
line,
SPACE.repeat(line_padding + line_padding_remainder),
);
}
}
x_border(width, BorderAlignment::Divider);
}

// Centered text with border on all sides
Layout::Large => {
x_border(full_message_width, BorderAlignment::Top);
for (line, line_display_width) in lines.iter().zip(lines_display_width.iter()) {
if *line_display_width == 0 {
println!(
"{}{}{}",
VERTICAL.yellow(),
SPACE.repeat(full_message_width),
VERTICAL.yellow()
);
} else {
let line_padding = (full_message_width - line_display_width) / 2;
// for lines of odd length, tack the reminder to the end
let line_padding_remainder =
full_message_width - (line_padding * 2) - line_display_width;
println!(
"{}{}{}{}{}",
VERTICAL.yellow(),
SPACE.repeat(line_padding),
line,
SPACE.repeat(line_padding + line_padding_remainder),
VERTICAL.yellow()
);
}
}
x_border(full_message_width, BorderAlignment::Bottom);
}
}
}
2 changes: 2 additions & 0 deletions shim/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ serde = { version = "1.0.145", features = ["derive"] }
serde_json = "1.0.86"
serde_yaml = "0.8.26"
walkdir = "2"
turbo-updater = { path = "../crates/turbo-updater" }
tiny-gradient = "0.1"
19 changes: 19 additions & 0 deletions shim/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ use std::{
use anyhow::{anyhow, Result};
use clap::{CommandFactory, Parser, Subcommand};
use serde::Serialize;
use tiny_gradient::{GradientStr, RGB};
use turbo_updater::check_for_updates;

use crate::{
ffi::{nativeRunWithArgs, nativeRunWithTurboState, GoString},
Expand Down Expand Up @@ -446,6 +448,23 @@ fn get_version() -> &'static str {
}

fn main() -> Result<()> {
// custom footer for update message
let footer = format!(
"Follow {username} for updates: {url}",
username = "@turborepo".gradient([RGB::new(0, 153, 247), RGB::new(241, 23, 18)]),
url = "https://twitter.com/turborepo"
);

// check for updates
let _ = check_for_updates(
"turbo",
"https://github.com/vercel/turbo",
Some(&footer),
get_version(),
None,
None,
);

let clap_args = Args::parse();
// --help doesn't work with ignore_errors in clap.
if clap_args.help {
Expand Down

0 comments on commit 603c2de

Please sign in to comment.