From b63411a1d5a5b95daea6e35361ceaac842ca5d78 Mon Sep 17 00:00:00 2001 From: Kaan Ozkan Date: Thu, 14 Nov 2024 19:38:40 -0500 Subject: [PATCH 1/5] wip --- lib/tapioca/cli.rb | 4 ++++ lib/tapioca/commands/init.rb | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 lib/tapioca/commands/init.rb diff --git a/lib/tapioca/cli.rb b/lib/tapioca/cli.rb index 33576f3c5..646f70863 100644 --- a/lib/tapioca/cli.rb +++ b/lib/tapioca/cli.rb @@ -24,6 +24,10 @@ 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 diff --git a/lib/tapioca/commands/init.rb b/lib/tapioca/commands/init.rb new file mode 100644 index 000000000..6c34008de --- /dev/null +++ b/lib/tapioca/commands/init.rb @@ -0,0 +1,10 @@ + +# typed: strict +# frozen_string_literal: true + +module Tapioca + module Commands + class Init + end + end +end From 66b3bac594a11cb78c144833c59589e135a9270d Mon Sep 17 00:00:00 2001 From: Kaan Ozkan Date: Wed, 11 Dec 2024 17:43:52 -0500 Subject: [PATCH 2/5] Invoke hack --- lib/tapioca/cli.rb | 72 +-------------------------------- lib/tapioca/commands.rb | 1 + lib/tapioca/commands/init.rb | 78 ++++++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 73 deletions(-) diff --git a/lib/tapioca/cli.rb b/lib/tapioca/cli.rb index 646f70863..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" @@ -29,20 +30,7 @@ class Cli < Thor 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" @@ -383,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 index 6c34008de..400f489dd 100644 --- a/lib/tapioca/commands/init.rb +++ b/lib/tapioca/commands/init.rb @@ -1,10 +1,82 @@ - -# typed: strict +# typed: true # frozen_string_literal: true +require "debug" module Tapioca module Commands - class Init + 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 @tutorial + + say "Welcome to tapioca init tutorial. Pass `--no-tutorial` and rerun the command if you know what you're doing." + say "Executing gem RBI generation" + end + + def execute_without_tutorial + Runtime::Trackers.with_trackers_enabled do + invoke(:configure, [], {}) + invoke(:annotations, [], {}) + invoke(:gem, [], {}) + end + 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 From e5d91730cf6dc408f4e0cc9e35319070579c0f1f Mon Sep 17 00:00:00 2001 From: Kaan Ozkan Date: Thu, 12 Dec 2024 14:20:51 -0500 Subject: [PATCH 3/5] Tutorial for configure, gem, annotations, dsl --- lib/tapioca/commands/init.rb | 76 ++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/lib/tapioca/commands/init.rb b/lib/tapioca/commands/init.rb index 400f489dd..1e663a2d6 100644 --- a/lib/tapioca/commands/init.rb +++ b/lib/tapioca/commands/init.rb @@ -10,10 +10,74 @@ module Init # TODO: Being mixed in to utilize "invoke" which means we don't have requires_ancestor { Thor } def init_execute - return execute_without_tutorial unless @tutorial + 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. - say "Welcome to tapioca init tutorial. Pass `--no-tutorial` and rerun the command if you know what you're doing." - say "Executing gem RBI generation" + 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 adds it to your project thus making 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)}. + GEM + STDIN.gets + call(:gem) + 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 from the default and any custom repositories you supply. + + Press #{set_color("enter", :yellow)} to run #{set_color("bin/tapioca annotations", :yellow)}. + ANNOTATION + STDIN.gets + call(:annotation) + 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 + begin + call(:dsl) + rescue + say(set_color("Error: Couldn't generate DSL RBIs", :red)) + end + + say("To continue to the next step, press #{set_color("enter", :yellow)}") + STDIN.gets + # TODO: Conclude end def execute_without_tutorial @@ -24,6 +88,12 @@ def execute_without_tutorial end end + private + + def call(name) + invoke(name, [], {}) + end + # def print_init_next_steps # say(<<~OUTPUT) # #{set_color("This project is now set up for use with Sorbet and Tapioca", :bold)} From 0a9c83fb51371fdf7ad665a6d020cb8545517d77 Mon Sep 17 00:00:00 2001 From: Kaan Ozkan Date: Thu, 12 Dec 2024 16:42:27 -0500 Subject: [PATCH 4/5] Conclude --- lib/tapioca/commands/init.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/tapioca/commands/init.rb b/lib/tapioca/commands/init.rb index 1e663a2d6..6a21061a5 100644 --- a/lib/tapioca/commands/init.rb +++ b/lib/tapioca/commands/init.rb @@ -40,25 +40,25 @@ def init_execute 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 adds it to your project thus making it available to Sorbet. + 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)}. + 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 - call(:gem) + 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 from the default and any custom repositories you supply. + 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(:annotation) + call(:annotations) say("To continue to the next step, press #{set_color("enter", :yellow)}") STDIN.gets @@ -77,7 +77,13 @@ def init_execute say("To continue to the next step, press #{set_color("enter", :yellow)}") STDIN.gets - # TODO: Conclude + 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 From 198d3588908fbef450fc79a61c4b581aac04915e Mon Sep 17 00:00:00 2001 From: Kaan Ozkan Date: Fri, 13 Dec 2024 15:04:35 -0500 Subject: [PATCH 5/5] Spinner & run DSL in child process --- lib/tapioca/commands/init.rb | 26 +++++++++++++++++++++++--- lib/tapioca/loaders/gem.rb | 6 ++++++ lib/tapioca/loaders/loader.rb | 6 +++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/lib/tapioca/commands/init.rb b/lib/tapioca/commands/init.rb index 6a21061a5..3e8a4348b 100644 --- a/lib/tapioca/commands/init.rb +++ b/lib/tapioca/commands/init.rb @@ -46,7 +46,7 @@ def init_execute 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 - call(:gem) # TODO: all + show_wait_spinner { call(:gem) } # TODO: all say("To continue to the next step, press #{set_color("enter", :yellow)}") STDIN.gets @@ -69,10 +69,13 @@ def init_execute Press #{set_color("enter", :yellow)} to run #{set_color("bin/tapioca dsl", :yellow)}. DSL + STDIN.gets begin - call(:dsl) + # $gem_loader.unload + # call(:dsl) + system("bin/tapioca dsl") rescue - say(set_color("Error: Couldn't generate DSL RBIs", :red)) + 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)}") @@ -100,6 +103,23 @@ 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)} 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 }