Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tapioca init: A tutorial #2127

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 6 additions & 70 deletions lib/tapioca/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Cli < Thor
include CliHelper
include ConfigHelper
include EnvHelper
include Commands::Init

FILE_HEADER_OPTION_DESC = "Add a \"This file is generated\" header on top of each generated RBI file"

Expand All @@ -24,21 +25,12 @@ class Cli < Thor
default: false

desc "init", "Get project ready for type checking"
option :tutorial,
type: :boolean,
desc: "Run tapioca init in tutorial mode where it explains what it's doing",
default: true
def init
# We need to make sure that trackers stay enabled until the `gem` command is invoked
Runtime::Trackers.with_trackers_enabled do
invoke(:configure)
invoke(:annotations)
invoke(:gem)
end

# call the command directly to skip deprecation warning
Commands::Todo.new(
todo_file: DEFAULT_TODO_FILE,
file_header: true,
).run

print_init_next_steps
command = init_execute
end

desc "configure", "Initialize folder structure and type checking configuration"
Expand Down Expand Up @@ -379,61 +371,5 @@ def exit_on_failure?
end
end
end

private

def print_init_next_steps
say(<<~OUTPUT)
#{set_color("This project is now set up for use with Sorbet and Tapioca", :bold)}

The sorbet/ folder should exist and look something like this:

├── config # Default options to be passed to Sorbet on every run
└── rbi/
├── annotations/ # Type definitions pulled from the rbi-central repository
├── gems/ # Autogenerated type definitions for your gems
└── todo.rbi # Constants which were still missing after RBI generation
└── tapioca/
├── config.yml # Default options to be passed to Tapioca
└── require.rb # A file where you can make requires from gems that might be needed for gem RBI generation

Please check this folder into version control.

#{set_color("🤔 What's next", :bold)}

1. Many Ruby applications use metaprogramming DSLs to dynamically generate constants and methods.
To generate type definitions for any DSLs in your application, run:

#{set_color("bin/tapioca dsl", :cyan)}

2. Check whether the constants in the #{set_color("sorbet/rbi/todo.rbi", :cyan)} file actually exist in your project.
It is possible that some of these constants are typos, and leaving them in #{set_color("todo.rbi", :cyan)} will
hide errors in your application. Ideally, you should be able to remove all definitions
from this file and delete it.

3. Typecheck your project:

#{set_color("bundle exec srb tc", :cyan)}

There should not be any typechecking errors.

4. Upgrade a file marked "#{set_color("# typed: false", :cyan)}" to "#{set_color("# typed: true", :cyan)}".
Then, run: #{set_color("bundle exec srb tc", :cyan)} and try to fix any errors.

You can use Spoom to bump files for you:

#{set_color("spoom bump --from false --to true", :cyan)}

To learn more about Spoom, visit: #{set_color("https://github.com/Shopify/spoom", :cyan)}

5. Add signatures to your methods with #{set_color("sig", :cyan)}. To learn how, read: #{set_color("https://sorbet.org/docs/sigs", :cyan)}

#{set_color("Documentation", :bold)}
We recommend skimming these docs to get a feel for how to use Sorbet:
- Gradual Type Checking: #{set_color("https://sorbet.org/docs/gradual", :cyan)}
- Enabling Static Checks: #{set_color("https://sorbet.org/docs/static", :cyan)}
- RBI Files: #{set_color("https://sorbet.org/docs/rbi", :cyan)}
OUTPUT
end
end
end
1 change: 1 addition & 0 deletions lib/tapioca/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ module Commands
autoload :GemVerify, "tapioca/commands/gem_verify"
autoload :Require, "tapioca/commands/require"
autoload :Todo, "tapioca/commands/todo"
autoload :Init, "tapioca/commands/init"
end
end
178 changes: 178 additions & 0 deletions lib/tapioca/commands/init.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# typed: true
# frozen_string_literal: true
require "debug"

module Tapioca
module Commands
module Init # TODO: Being mixed in to utilize "invoke" which means we don't have to redefine defaults
extend T::Helpers

requires_ancestor { Thor }

def init_execute
return execute_without_tutorial unless options[:tutorial]
say(<<~WELCOME)
Welcome to the Tapioca tutorial.
If you know what you're doing and would like to skip this you can pass #{set_color("--no-tutorial", :yellow)} and rerun the command.

This tutorial will guide you step by step on how Tapioca operates. This information will be #{set_color("useful", :bold)} when you run into type checking errors in the future. A more detailed description is available in the README.

WELCOME

say "#{set_color("Step 1. Configuration", :yellow, :bold)}"
say(<<~CONFIG)
Before Sorbet or Tapioca can run they need a few configuration files to be setup which we'll generate now.Benefit of using these config files is that your whole team will be on the same page and you won't have to remember which options to supply to the executables.

Press #{set_color("enter", :yellow)} to run #{set_color("bin/tapioca configure", :yellow)}.
CONFIG
STDIN.gets
call(:configure)
say(<<~CONFIG)

Above are the generated files. Feel free to take a look inside them.

- #{set_color("sorbet/config", :yellow)} is how you pass options to Sorbet for type checking. For more info go to #{set_color("https://sorbet.org/docs/cli#config-file", :yellow)}
- #{set_color("sorbet/tapioca/config", :yellow)} is how you pass options to various Tapioca commands #{set_color("https://github.com/Shopify/tapioca?tab=readme-ov-file#configuration", :yellow)}
- #{set_color("sorbet/tapioca/require.rb", :yellow)} by default Tapioca require's a gem using #{set_color('require "$gem_name"', :yellow)} which may not load all parts of a gem. Add your extra require's to this file
CONFIG
say("To continue to the next step, press #{set_color("enter", :yellow)}")
STDIN.gets

say "#{set_color("Step 2. Gem RBIs", :yellow, :bold)}"
say(<<~GEM)
Sorbet doesn't know about the source code of gems used in your application. That's why Tapioca generates constant and method definitions for them and makes it available to Sorbet.
You can run #{set_color("bin/tapioca gem", :yellow)} to generate RBIs for gems that are out of date or supply the #{set_color("--all", :yellow)} flag to regenerate for all gems.

Press #{set_color("enter", :yellow)} to run #{set_color("bin/tapioca gem --all", :yellow)}. Note: This might take long depending on the number of gems.
GEM
STDIN.gets
show_wait_spinner { call(:gem) } # TODO: all
say("To continue to the next step, press #{set_color("enter", :yellow)}")
STDIN.gets

say "#{set_color("Step 3. Annotation RBIs", :yellow, :bold)}"
say(<<~ANNOTATION)
Gem RBIs generated by Tapioca lack signatures unless they were in the gem source. Ideally, if you want to contribute signatures they should live in the gem. However, not every gem will accept them so we have a repository that contains annotated gem RBIs #{set_color("https://github.com/Shopify/rbi-central/", :yellow)}.
Annotations command downloads annotation RBIs for the gems you use.

Press #{set_color("enter", :yellow)} to run #{set_color("bin/tapioca annotations", :yellow)}.
ANNOTATION
STDIN.gets
call(:annotations)
say("To continue to the next step, press #{set_color("enter", :yellow)}")
STDIN.gets

say "#{set_color("Step 4. DSL RBIs", :yellow, :bold)}"
say(<<~DSL)
Ruby and Rails development involes meta-programming that isn't statically available to Sorbet. To expose the relevant method definitions, Tapioca generates DSL RBIs for common Rails idioms using what we call DSL compilers.
Note that Tapioca needs to be able to boot up your app to achieve this.

Press #{set_color("enter", :yellow)} to run #{set_color("bin/tapioca dsl", :yellow)}.
DSL
STDIN.gets
begin
# $gem_loader.unload
# call(:dsl)
system("bin/tapioca dsl")
rescue
say(set_color("Error: Couldn't generate DSL RBIs. Ensure your app can be booted locally", :red))
end

say("To continue to the next step, press #{set_color("enter", :yellow)}")
STDIN.gets
say(<<~CONCLUDE)
We've explored all the tools available in Tapioca to help type check your application! If you want to learn more on any of these check out the #{set_color("README", :yellow)}.

This information will be useful when you're dealing with type checking errors. For example, now you know that if you update your gem you need to run the #{set_color("gem", :yellow)} command. Or if you define an association in Rails using a DSL you need to run the #{set_color("dsl", :yellow)} command.

We suggest you run type check now with #{set_color("bundle exec srb tc", :yellow)} and use the appropriate Sorbet or Tapioca tools to resolve your errors. Happy Typing.
CONCLUDE
end

def execute_without_tutorial
Runtime::Trackers.with_trackers_enabled do
invoke(:configure, [], {})
invoke(:annotations, [], {})
invoke(:gem, [], {})
end
end

private

def call(name)
invoke(name, [], {})
end

def show_wait_spinner(fps=10)
chars = %w[| / - \\]
delay = 1.0/fps
iter = 0
spinner = Thread.new do
while iter do # Keep spinning until told otherwise
print chars[(iter+=1) % chars.length]
sleep delay
print "\b"
end
end
yield.tap{ # After yielding to the block, save the return value
iter = false # Tell the thread to exit, cleaning up after itself…
spinner.join # …and wait for it to do so.
} # Use the block's return value as the method's
end

# def print_init_next_steps
# say(<<~OUTPUT)
# #{set_color("This project is now set up for use with Sorbet and Tapioca", :bold)}
#
# The sorbet/ folder should exist and look something like this:
#
# ├── config # Default options to be passed to Sorbet on every run
# └── rbi/
# ├── annotations/ # Type definitions pulled from the rbi-central repository
# ├── gems/ # Autogenerated type definitions for your gems
# └── todo.rbi # Constants which were still missing after RBI generation
# └── tapioca/
# ├── config.yml # Default options to be passed to Tapioca
# └── require.rb # A file where you can make requires from gems that might be needed for gem RBI generation
#
# Please check this folder into version control.
#
# #{set_color("🤔 What's next", :bold)}
#
# 1. Many Ruby applications use metaprogramming DSLs to dynamically generate constants and methods.
# To generate type definitions for any DSLs in your application, run:
#
# #{set_color("bin/tapioca dsl", :cyan)}
#
# 2. Check whether the constants in the #{set_color("sorbet/rbi/todo.rbi", :cyan)} file actually exist in your project.
# It is possible that some of these constants are typos, and leaving them in #{set_color("todo.rbi", :cyan)} will
# hide errors in your application. Ideally, you should be able to remove all definitions
# from this file and delete it.
#
# 3. Typecheck your project:
#
# #{set_color("bundle exec srb tc", :cyan)}
#
# There should not be any typechecking errors.
#
# 4. Upgrade a file marked "#{set_color("# typed: false", :cyan)}" to "#{set_color("# typed: true", :cyan)}".
# Then, run: #{set_color("bundle exec srb tc", :cyan)} and try to fix any errors.
#
# You can use Spoom to bump files for you:
#
# #{set_color("spoom bump --from false --to true", :cyan)}
#
# To learn more about Spoom, visit: #{set_color("https://github.com/Shopify/spoom", :cyan)}
#
# 5. Add signatures to your methods with #{set_color("sig", :cyan)}. To learn how, read: #{set_color("https://sorbet.org/docs/sigs", :cyan)}
#
# #{set_color("Documentation", :bold)}
# We recommend skimming these docs to get a feel for how to use Sorbet:
# - Gradual Type Checking: #{set_color("https://sorbet.org/docs/gradual", :cyan)}
# - Enabling Static Checks: #{set_color("https://sorbet.org/docs/static", :cyan)}
# - RBI Files: #{set_color("https://sorbet.org/docs/rbi", :cyan)}
# OUTPUT
# end
end
end
end
6 changes: 6 additions & 0 deletions lib/tapioca/loaders/gem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def load_application(bundle:, prerequire:, postrequire:, default_command:, halt_
halt_upon_load_error: halt_upon_load_error,
)
loader.load
$gem_loader = loader
end
end

Expand All @@ -35,6 +36,11 @@ def load
require_gem_file
end

def unload
puts "Unloading"
$autoloader.unload if $autoloader
end

protected

sig do
Expand Down
6 changes: 3 additions & 3 deletions lib/tapioca/loaders/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def load_engines_in_zeitwerk_mode
managed_dirs = Zeitwerk::Registry.loaders.flat_map(&:dirs).to_set
# We use a fresh loader to load the engine directories, so that we don't interfere with
# any of the existing loaders.
autoloader = Zeitwerk::Loader.new
$autoloader = Zeitwerk::Loader.new

engines.each do |engine|
eager_load_paths(engine).each do |path|
Expand All @@ -125,11 +125,11 @@ def load_engines_in_zeitwerk_mode
# We should not add directories that are already managed by a Zeitwerk loader.
next if managed_dirs.member?(path)

autoloader.push_dir(path)
$autoloader.push_dir(path)
end
end

autoloader.setup
$autoloader.setup
end

sig { void }
Expand Down
Loading