require "io/wait" namespace :bt do desc "Symlink registered gems in `./tmp/gems` so their views, etc. can be inspected by Tailwind CSS." task link: :environment do if Dir.exist?("tmp/gems") puts "Removing previously linked gems." `rm -f tmp/gems/*` else if File.exist?("tmp/gems") raise "A file named `tmp/gems` already exists? It has to be removed before we can create the required directory." end puts "Creating 'tmp/gems' directory." `mkdir tmp/gems` end `touch tmp/gems/.keep` BulletTrain.linked_gems.each do |linked_gem| target = `bundle show #{linked_gem}`.chomp if target.present? puts "Linking '#{linked_gem}' to '#{target}'." `ln -s #{target} tmp/gems/#{linked_gem}` end end end end namespace :bullet_train do desc "Figure out where something is coming from." task :resolve, [:all_options] => :environment do |t, arguments| ARGV.pop while ARGV.any? arguments[:all_options]&.split&.each do |argument| ARGV.push(argument) end if ARGV.include?("--interactive") puts "\nOK, paste what you've got for us and hit !\n".blue input = $stdin.gets.strip # we only resolve the first line of user input and ignore the rest $stdin.getc while $stdin.ready? # Extract absolute paths from annotated views. if input =~ // input = $1 elsif input =~ // input = $1 end # Append the main application's path if the file is a local file. # i.e. - app/views/layouts/_head.html.erb if input.match?(/^app/) input = "#{Rails.root}/#{input}" end ARGV.unshift input.strip end if ARGV.first.present? BulletTrain::Resolver.new(ARGV.first).run(eject: ARGV.include?("--eject"), open: ARGV.include?("--open"), force: ARGV.include?("--force"), interactive: ARGV.include?("--interactive")) else warn <<~MSG 🚅 Usage: #{"`bin/resolve [path, partial, or URL] (--eject) (--open)`".blue} OR #{"`bin/resolve --interactive`".blue} When you use the interactive flag, we will prompt you to pass an annotated partial like so and either eject or open the file. These annotated paths can be found in your browser when inspecting elements: MSG end end desc "Eject files from the Bullet Train's core logic to your application." task :eject, [:type] => :environment do |task, args| if args[:type] == "locales" gem_names = I18n.t("framework_packages").map { |key, value| key.to_s } gem_names.each do |gem| puts "Searching for locales in #{gem}...".blue gem_path = `bundle show #{gem}`.chomp locales = Dir.glob("#{gem_path}/**/config/locales/**/*.yml").reject { |path| path.match?("dummy") } next if locales.empty? puts "Found locales. Ejecting to your application...".green locales.each do |locale| relative_path = locale.split("/config/locales").pop path_parts = relative_path.split("/") base_path = path_parts.join("/") FileUtils.mkdir_p("./config/locales#{base_path}") unless Dir.exist?("./config/locales#{base_path}") unless File.exist?("config/locales#{relative_path}") puts "Ejecting #{relative_path}..." File.new("config/locales#{relative_path}", "w") `cp #{locale} config/locales#{relative_path}` end end end end end task :hack, [:all_options] => :environment do |t, arguments| def stream(command, prefix = " ") puts "" begin trap("SIGINT") { throw :ctrl_c } IO.popen(command) do |io| while (line = io.gets) puts "#{prefix}#{line}" end end rescue UncaughtThrowError puts "Received a . Exiting the child process.".blue end puts "" end framework_packages = I18n.t("framework_packages") # Process any flags that were passed. if arguments[:all_options].present? flags_with_values = [] arguments[:all_options].split(/\s+/).each do |option| if option.match?(/^--/) flags_with_values << {flag: option, values: []} else flags_with_values.last[:values] << option end end if flags_with_values.any? flags_with_values.each do |process| case process[:flag] when "--help" puts "" puts "bin/hack: " + "Clone bullet_train-core and link up gems (will only link up gems if already cloned).".blue puts "bin/hack --link: " + "Link all of your Bullet Train gems to `local/bullet_train-core`.".blue puts "bin/hack --link github: " + "Link all of your Bullet Train gems to the public repositories on GitHub".blue puts "bin/hack --link (version-number): " + "Link all of your Bullet Train gems to the version number passed.".blue puts "bin/hack --link (custom path to package): " + "Pass a custom path to where your bullet_train-core gems are.".blue puts "bin/hack --reset: " + "Resets all of your gems to their original definition.".blue puts "bin/hack --watch-js: " + "Watches for any changes in JavaScript files gems that have an npm package.".blue puts "bin/hack --clean-js: " + "Resets all of your npm packages from `local/bullet_train-core` to their original definition.".blue exit when "--link", "--reset" link_flag_value = process[:values].pop set_core_gems(process[:flag], link_flag_value, framework_packages) # Bundler will throw an error if we try to `bundle install` right after adding the GitHub link to the Gemfile. if link_flag_value == "github" puts "" puts "Now you can run `bundle install` to check out the public repositories on GitHub." else stream "bundle install" end when "--watch-js", "--clean-js" package_name = process[:values].pop framework_package = framework_packages.select { |k, v| k.to_s == package_name } if framework_package.empty? puts "Sorry, we couldn't find the package you're looking for.".red puts "" npm_packages = framework_packages.select { |name, details| details[:npm].present? } puts "Please enter one of the following package names when running `bin/hack --watch-js` or `bin/hack --clean-js`:" npm_packages.each_with_index do |package, idx| puts "#{idx + 1}. #{package.first}" end exit 1 end set_npm_package(process[:flag], framework_package) end end exit end end puts "Welcome! Let's get hacking.".blue # Adding these flags enables us to execute git commands in the gem from our starter repo. work_tree_flag = "--work-tree=local/bullet_train-core" git_dir_flag = "--git-dir=local/bullet_train-core/.git" if File.exist?("local/bullet_train-core") puts "We found the repository in `local/bullet_train-core`. We will try to use what's already there.".yellow puts "" git_status = `git #{work_tree_flag} #{git_dir_flag} status` unless git_status.match?("nothing to commit, working tree clean") puts "This package currently has uncommitted changes.".red puts "Please make sure the branch is clean and try again.".red exit end current_branch = `git #{work_tree_flag} #{git_dir_flag} branch`.split("\n").select { |branch_name| branch_name.match?(/^\*\s/) }.pop.gsub(/^\*\s/, "") unless current_branch == "main" puts "Previously on #{current_branch}.".blue puts "Switching local/bullet_train-core to main branch.".blue stream("git #{work_tree_flag} #{git_dir_flag} checkout main") end puts "Updating the main branch with the latest changes.".blue stream("git #{work_tree_flag} #{git_dir_flag} pull origin main") else # Use https:// URLs when using this task in Gitpod. stream "git clone #{(`whoami`.chomp == "gitpod") ? "https://github.com/" : "git@github.com:"}/bullet-train-co/bullet_train-core.git local/bullet_train-core" end stream("git #{work_tree_flag} #{git_dir_flag} fetch") stream("git #{work_tree_flag} #{git_dir_flag} branch -r") puts "The above is a list of remote branches.".blue puts "If there's one you'd like to work on, please enter the branch name and press .".blue puts "If not, just press to continue.".blue input = $stdin.gets.strip unless input.empty? puts "Switching to #{input.gsub("origin/", "")}".blue # TODO: Should we remove origin/ here if the developer types it? stream("git #{work_tree_flag} #{git_dir_flag} checkout #{input}") end # Link all of the local gems to the current Gemfile. puts "Now we'll try to link up the Bullet Train core repositories in the `Gemfile`.".blue set_core_gems("--link", nil, framework_packages) puts "" puts "Now we'll run `bundle install`.".blue stream "bundle install" puts "" puts "We'll restart any running Rails server now.".blue stream "rails restart" puts "" puts "OK, we're opening bullet_train-core in your IDE, `#{ENV["IDE"] || "code"}`. (You can configure this with `export IDE=whatever`.)".blue `#{ENV["IDE"] || "code"} local/bullet_train-core` puts "" puts "Bullet Train has a few npm packages, but we can only watch one at a time, so we will watch the `bullet_train` package.".blue puts "Any changes in your JavaScript files in this package will be recompiled as we go.".blue puts "Run `bin/hack --watch-js` to see which other npm packages you can watch.".blue puts "When you're done, you can hit and we'll clean all off this up.".blue puts "" bt_package = framework_packages.select { |k, v| k == :bullet_train } set_npm_package("--watch-js", bt_package) puts "" puts "OK, here's a list of things this script still doesn't do you for you:".yellow puts "1. It doesn't clean up the repository that was cloned into `local`.".yellow puts "2. Unless you remove it, it won't update that repository the next time you link to it.".yellow end # Pass "--link" or "--reset" as a flag to set the gems. def set_core_gems(flag, link_flag_value, framework_packages) packages = framework_packages.keys gemfile_lines = File.readlines("./Gemfile") version_regexp = /^[\d|.]$/ packages.each do |package| original_path = "gem \"#{package}\"" local_path = "gem \"#{package}\", path: \"local/bullet_train-core/#{package}\"" match_found = false new_lines = gemfile_lines.map do |line| if line.match?(/"#{package}"/) match_found = true if flag == "--link" if `cat Gemfile | grep "gem \\\"#{package}\\\", path: \\\"local/bullet_train-core/#{package}\\\""`.chomp.present? puts "#{package} is already linked to a checked out copy in `local` in the `Gemfile`.".green # We can update this later to match a regular expression at the end # (git: "...", require: "...", etc.), but this will do for now. elsif `cat Gemfile | grep "gem \\\"#{package}\\\", path:"`.chomp.present? puts "#{package} already has some sort of alternative source configured in the `Gemfile`.".yellow puts "We can't do anything with this. Sorry! We'll proceed, but you have to link this package yourself.".red elsif `cat Gemfile | grep "gem \\\"#{package}\\\""`.chomp.present? puts "#{package} is directly present in the `Gemfile`, so we'll update that line.".green line = if link_flag_value == "github" "#{line.chomp}, git: 'http://github.com/bullet-train-co/bullet_train-core.git'\n" elsif link_flag_value&.match?(version_regexp) "#{line.chomp}, \"#{link_flag_value}\"\n" elsif link_flag_value.present? link_flag_value.gsub!(/\/$/, "") # Ensure the custom path doesn't have a slash at the end. local_path.gsub(/path:.*/, "path: \"#{link_flag_value}/#{package}\"\n") else line.gsub(original_path, local_path) end end elsif flag == "--reset" if line.match?(/bullet_train/) line.gsub!(/,.*$/, "") end puts "Resetting '#{package}' package in the Gemfile...".blue end end line end # Add/Remove any packages that aren't primarily in the Gemfile. if flag == "--link" unless match_found puts "Could not find #{package}. Adding to the end of the Gemfile.".blue new_lines << if link_flag_value == "github" "#{original_path.chomp}, git: 'http://github.com/bullet-train-co/bullet_train-core.git'\n" elsif link_flag_value&.match?(version_regexp) "#{original_path.chomp}, \"#{link_flag_value}\"\n" else "#{local_path}\n" end end elsif flag == "--reset" gem_regexp = /bullet_train-[a-z|A-Z_-]+/ while new_lines.last.match?(gem_regexp) puts "Removing #{new_lines.last.scan(gem_regexp).first} from the Gemfile.".yellow new_lines.pop end end gemfile_lines = new_lines end File.write("./Gemfile", gemfile_lines.join) end def set_npm_package(flag, package) package.each do |name, details| if flag == "--watch-js" puts "Make sure your server is running before proceeding. When you're ready, press ".blue $stdin.gets.strip puts "Linking JavaScript for #{name}".blue stream "cd local/bullet_train-core/#{name} && yarn install && npm_config_yes=true && npx yalc link && cd ../../.. && npm_config_yes=true npx yalc link \"#{details[:npm]}\"" puts "#{name} has been linked.".blue puts "Preparing to watch changes.".blue stream "yarn --cwd local/bullet_train-core/#{name} watch" # Provide a help message after the developer kills the process with `Ctrl + C`. puts "Run `bin/hack --clean-js #{name}` to revert to using the original npm package in your application.".blue elsif flag == "--clean-js" puts "Going back to using original `#{name}` npm package in application.".blue puts "" system "yarn yalc remove #{details[:npm]}" system "yarn add #{details[:npm]}" end end end end