diff --git a/lib/tapioca/cli.rb b/lib/tapioca/cli.rb index 33576f3c5..0ed6c727f 100644 --- a/lib/tapioca/cli.rb +++ b/lib/tapioca/cli.rb @@ -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" @@ -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" @@ -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 diff --git a/lib/tapioca/commands.rb b/lib/tapioca/commands.rb index 050df09e9..b0d8842b5 100644 --- a/lib/tapioca/commands.rb +++ b/lib/tapioca/commands.rb @@ -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 diff --git a/lib/tapioca/commands/init.rb b/lib/tapioca/commands/init.rb new file mode 100644 index 000000000..3e8a4348b --- /dev/null +++ b/lib/tapioca/commands/init.rb @@ -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 diff --git a/lib/tapioca/loaders/gem.rb b/lib/tapioca/loaders/gem.rb index 150593f1e..7558d9af7 100644 --- a/lib/tapioca/loaders/gem.rb +++ b/lib/tapioca/loaders/gem.rb @@ -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 @@ -35,6 +36,11 @@ def load require_gem_file end + def unload + puts "Unloading" + $autoloader.unload if $autoloader + end + protected sig do diff --git a/lib/tapioca/loaders/loader.rb b/lib/tapioca/loaders/loader.rb index a1242aefb..8a658c26d 100644 --- a/lib/tapioca/loaders/loader.rb +++ b/lib/tapioca/loaders/loader.rb @@ -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| @@ -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 }