Skip to content

Replace rainbow gem with native NextRails::Tint wrapper#183

Open
JuanVqz wants to merge 6 commits intomainfrom
chore/replace-rainbow-with-tint
Open

Replace rainbow gem with native NextRails::Tint wrapper#183
JuanVqz wants to merge 6 commits intomainfrom
chore/replace-rainbow-with-tint

Conversation

@JuanVqz
Copy link
Copy Markdown
Member

@JuanVqz JuanVqz commented May 5, 2026

Summary

Replaces the rainbow runtime dependency with a small native ANSI wrapper, NextRails::Tint, dropping an external gem in favor of ~30 lines of pure Ruby.

Why drop the dependency

next_rails is an upgrade toolkit that has to run inside other people's Gemfiles across Ruby 2.3 → 4.0. Every runtime dependency we carry is one more constraint on the host application's bundle, one more compatibility surface to maintain across that long Ruby range, and one more thing that can conflict during the very upgrade we're trying to help with. The less we ask of the host environment, the less intrusive we are. Color formatting is trivial enough (a handful of ANSI escape codes) that pulling in an external gem isn't worth the cost.

  • New NextRails::Tint class (lib/next_rails/tint.rb) with a chainable styling API: NextRails::Tint("text").red.bold. Style methods return new instances, so Tint is effectively immutable and chains never accumulate codes on a shared reference.
  • Module-level factory NextRails.Tint(string) so call sites read NextRails::Tint("hello").red.bold (no Kernel pollution, mirrors how Rainbow(...) was invoked).
  • All previous Rainbow(...) call sites updated to NextRails::Tint(...)
  • rainbow removed from next_rails.gemspec
  • Refactored RubyVersionCompatibility#message and RailsVersionCompatibility#gem_header to Array#join, removing the need for Tint#+ / Tint#<< and keeping the wrapper's surface minimal (constructor + style chain + to_s / to_str)
  • New specs in spec/next_rails/tint_spec.rb (covers immutability) and spec/deprecation_tracker_spec.rb (locks ANSI escape codes in the raised UnexpectedDeprecations message via Ruby's to_str coercion on raise)
  • Per-file require "next_rails/tint" (no global require) so files stay loadable standalone
  • All syntax kept compatible with Ruby 2.3 → 4.0 (frozen_string_literal, squiggly heredoc, Hash#freeze, define_method)

Test plan

  • bundle exec rake passes locally on a supported Ruby
  • CI matrix (Ruby 2.3 → 4.0) green
  • Manual smoke: bundle_report outdated renders gem names wrapped in \e[1;37m...\e[0m; bundle_report compatibility renders the incompatible count wrapped in \e[31m...\e[0m
  • deprecation_tracker compare-mode error still rendered red — locked by new spec asserting the raised message starts with \e[31m and ends with \e[0m

Drop the rainbow runtime dependency in favor of a small (~30 LOC) ANSI
wrapper class, NextRails::Tint, with a chainable styling API.

- Add lib/next_rails/tint.rb (chainable, pure Ruby, 2.3+ compatible)
- Replace all Rainbow(x) call sites with NextRails::Tint[x]
- Remove rainbow from next_rails.gemspec
- Refactor RubyVersionCompatibility#message to a heredoc
- Refactor RailsVersionCompatibility#gem_header to Array#join
- Add spec/next_rails/tint_spec.rb
- Update CHANGELOG
@JuanVqz JuanVqz self-assigned this May 5, 2026
@JuanVqz JuanVqz requested review from arielj and etagwerker and removed request for arielj and etagwerker May 5, 2026 18:05
JuanVqz added 3 commits May 5, 2026 12:08
Ensures #generate always returns a String, matching the heredoc-based
#message branch and existing spec expectations using String#include?.

Previously, invalid_message returned a NextRails::Tint instance, which
worked with the old Rainbow gem because its Presenter inherited from
String. NextRails::Tint does not, so call .to_s at the boundary.
Keep internal helpers Tint-native and stringify only at the public
return point. One conversion site, contract lives where the public
API is defined.
@JuanVqz JuanVqz requested review from arielj and etagwerker May 5, 2026 18:12
@JuanVqz JuanVqz marked this pull request as ready for review May 5, 2026 18:15
Copy link
Copy Markdown
Member

@etagwerker etagwerker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanup. The rationale for dropping the runtime dep is solid, and Tint is small enough to maintain. A few questions and suggestions inline.

One thing I couldn't comment inline on (since the file isn't in the diff): Gemfile.lock still shows rainbow (>= 3) and rainbow (3.1.1). Could you regenerate it with bundle install so the lockfile matches the gemspec?

Also, the test plan still has two unchecked manual smoke checks. Could you confirm bundle_report compatible / bundle_report outdated / the deprecation_tracker compare-mode output all render with the expected colors before we merge?

Comment thread lib/next_rails/bundle_report/cli.rb Outdated
Comment thread lib/next_rails/bundle_report/rails_version_compatibility.rb Outdated
Comment thread lib/next_rails/bundle_report/ruby_version_compatibility.rb Outdated
Comment thread lib/deprecation_tracker.rb Outdated
Comment thread lib/next_rails/tint.rb Outdated
Comment thread lib/next_rails/tint.rb Outdated
white: 37
}.freeze

def self.[](string)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we add this "constructor" method? Is it a convention taken from somewhere else, I have not come across this commonly in other projects. The original rainbow gem (it seems) made use of the standard initialize constructor, so I don't currently see the value of NextRails::Tint["hello"] vs NextRails::Tint("hello")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it looks weird but it is used I think TTY::Color gem or even ruby core Array[1,2,3], Hash[:a,1], Set[1,2,3]

I updated to use() anyways.

@JuanVqz
Copy link
Copy Markdown
Member Author

JuanVqz commented May 5, 2026

One thing I couldn't comment inline on (since the file isn't in the diff): Gemfile.lock still shows rainbow (>= 3) and rainbow (3.1.1). Could you regenerate it with bundle install so the lockfile matches the gemspec?

The lockfile is gitignored, that was in your local env, just run bundle install

Also, the test plan still has two unchecked manual smoke checks. Could you confirm bundle_report compatible / bundle_report outdated / the deprecation_tracker compare-mode output all render with the expected colors before we merge?

Confirmed. Smokes pass with expected colors:

  • bundle_report outdated — gem names wrap in \e[1;37m...\e[0m (bold white)
  • bundle_report compatibility — incompatible count wraps in \e[31m...\e[0m (red)
  • RubyVersionCompatibility invalid path — \e[31;1m...\e[0m (red+bold); empty incompat list — \e[37;1m header, single blank line, \e[31m footer
    (matches original += spacing)
  • deprecation_tracker compare raise — now locked by spec asserting the raised message starts with \e[31m and ends with \e[0m

@JuanVqz JuanVqz requested review from etagwerker and fbuys May 5, 2026 22:10
- Switch Tint construction from `Tint[str]` to `NextRails::Tint(str)`
  factory on the module, mirroring Kernel-style wrappers like
  `Rainbow(...)`, `Pathname(...)`, `BigDecimal(...)` without polluting
  Kernel.
- Make Tint effectively immutable: style methods return a new instance
  carrying `@codes + [code]` instead of mutating `@codes` on self, so a
  shared reference can't accumulate styles across chains.
- Add `require "next_rails/tint"` to cli.rb and
  rails_version_compatibility.rb so every call-site file is loadable
  standalone.
- Rewrite RubyVersionCompatibility#message with an array + join instead
  of a heredoc so the empty-incompatible case keeps the original
  single-blank-line spacing between header and footer.
- Lock the deprecation_tracker compare-mode raise format with a spec
  asserting the message is wrapped in red ANSI escape codes (the raise
  relies on Tint#to_str coercion, so this contract is load-bearing).
- Add a Tint spec asserting style chains do not mutate a shared base.
@JuanVqz JuanVqz force-pushed the chore/replace-rainbow-with-tint branch from a0ad793 to 73705fb Compare May 5, 2026 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants