require "semantic/semantic" module ShopifyCLI module Services module App module Create class RailsService < BaseService USER_AGENT_CODE = <<~USERAGENT module ShopifyAPI class Base < ActiveResource::Base self.headers['User-Agent'] << " | ShopifyApp/\#{ShopifyApp::VERSION} | Shopify CLI" end end USERAGENT DEFAULT_RAILS_FLAGS = %w(--skip-spring) attr_reader :name, :organization_id, :store_domain, :type, :db, :rails_opts, :context def initialize(name:, organization_id:, store_domain:, type:, db:, rails_opts:, context:) @name = name @organization_id = organization_id @store_domain = store_domain @type = type @db = db @rails_opts = rails_opts @context = context super() end def call form_options = { name: name, organization_id: organization_id, shop_domain: store_domain, type: type, } form_options[:db] = db unless db.nil? form_options[:rails_opts] = rails_opts unless rails_opts.nil? form = form_data(form_options) raise ShopifyCLI::AbortSilent if form.nil? check_dependencies build(form.name, form.db) set_custom_ua ShopifyCLI::Project.write( context, project_type: "rails", organization_id: form.organization_id, ) api_client = if ShopifyCLI::Environment.acceptance_test? { "apiKey" => "public_api_key", "apiSecretKeys" => [ { "secret" => "api_secret_key", }, ], } else ShopifyCLI::Tasks::CreateApiClient.call( context, org_id: form.organization_id, title: form.name, type: form.type, ) end ShopifyCLI::Resources::EnvFile.new( api_key: api_client["apiKey"], secret: api_client["apiSecretKeys"].first["secret"], shop: form.shop_domain, scopes: "write_products,write_customers,write_draft_orders", ).write(context) partners_url = ShopifyCLI::PartnersAPI.partners_url_for(form.organization_id, api_client["id"]) context.puts(context.message("apps.create.info.created", form.name, partners_url)) context.puts(context.message("apps.create.info.serve", form.name, ShopifyCLI::TOOL_NAME)) unless ShopifyCLI::Shopifolk.acting_as_shopify_organization? context.puts(context.message("apps.create.info.install", partners_url, form.name)) end end private def form_data(form_options) if ShopifyCLI::Environment.acceptance_test? Struct.new(:name, :organization_id, :type, :shop_domain, :db, keyword_init: true).new( name: form_options[:name], organization_id: "123", shop_domain: "test.shopify.io", type: "public", db: form_options[:db] ) else Rails::Forms::Create.ask(context, [], form_options) end end def check_dependencies check_ruby check_node check_yarn end def check_ruby ruby_version = Rails::Ruby.version(context) return if ruby_version.satisfies?("~>2.5") || ruby_version.satisfies?("~>3.1.0") context.abort(context.message("core.app.create.rails.error.invalid_ruby_version")) end def check_node cmd_path = context.which("node") if cmd_path.nil? context.abort(context.message("core.app.create.rails.error.node_required")) unless context.windows? context.puts("{{x}} {{red:" + context.message("core.app.create.rails.error.node_required") + "}}") context.puts(context.message("core.app.create.rails.info.open_new_shell", "node")) raise ShopifyCLI::AbortSilent end version, stat = context.capture2e("node", "-v") unless stat.success? context.abort(context.message("core.app.create.rails.error.node_version_failure")) unless context.windows? # execution stops above if not Windows context.puts("{{x}} {{red:" + context.message("core.app.create.rails.error.node_version_failure") + "}}") context.puts(context.message("core.app.create.rails.info.open_new_shell", "node")) raise ShopifyCLI::AbortSilent end context.done(context.message("core.app.create.rails.node_version", version)) end def check_yarn cmd_path = context.which("yarn") if cmd_path.nil? context.abort(context.message("core.app.create.rails.error.yarn_required")) unless context.windows? context.puts("{{x}} {{red:" + context.message("core.app.create.rails.error.yarn_required") + "}}") context.puts(context.message("core.app.create.rails.info.open_new_shell", "yarn")) raise ShopifyCLI::AbortSilent end version, stat = context.capture2e("yarn", "-v") unless stat.success? context.abort(context.message("core.app.create.rails.error.yarn_version_failure")) unless context.windows? context.puts("{{x}} {{red:" + context.message("core.app.create.rails.error.yarn_version_failure") + "}}") context.puts(context.message("core.app.create.rails.info.open_new_shell", "yarn")) raise ShopifyCLI::AbortSilent end context.done(context.message("core.app.create.rails.yarn_version", version)) end def build(name, db) unless install_gem("rails") context.abort(context.message("core.app.create.rails.error.install_failure", "rails")) end unless install_gem("bundler", "~>2.0") context.abort(context.message("core.app.create.rails.error.install_failure", "bundler ~>2.0")) end full_path = File.join(context.root, name) context.abort(context.message("core.app.create.rails.error.dir_exists", name)) if Dir.exist?(full_path) CLI::UI::Frame.open(context.message("core.app.create.rails.generating_app", name)) do new_command = %w(rails new) new_command << name new_command += DEFAULT_RAILS_FLAGS new_command << "--database=#{db}" new_command += rails_opts.split unless rails_opts.nil? syscall(new_command) end context.root = full_path File.open(File.join(context.root, ".gitignore"), "a") { |f| f.write(".env") } context.puts(context.message("core.app.create.rails.adding_shopify_gem")) File.open(File.join(context.root, "Gemfile"), "a") do |f| f.puts "\ngem 'shopify_app', '>=18.1.0'" end CLI::UI::Frame.open(context.message("core.app.create.rails.running_bundle_install")) do syscall(%w(bundle install)) end CLI::UI::Frame.open(context.message("core.app.create.rails.running_generator")) do syscall(%w(rails generate shopify_app --new-shopify-cli-app)) end CLI::UI::Frame.open(context.message("core.app.create.rails.running_migrations")) do syscall(%w(rails db:create)) syscall(%w(rails db:migrate RAILS_ENV=development)) end if install_webpacker? CLI::UI::Frame.open(context.message("core.app.create.rails.running_webpacker_install")) do syscall(%w(rails webpacker:install)) end end end def set_custom_ua ua_path = File.join("config", "initializers", "user_agent.rb") context.write(ua_path, USER_AGENT_CODE) end def syscall(args) args[0] = Rails::Gem.binary_path_for(context, args[0]) context.system(*args, chdir: context.root) end def install_gem(name, version = nil) Rails::Gem.install(context, name, version) end def install_webpacker? rails_version < ::Semantic::Version.new("7.0.0") && !File.exist?(File.join(context.root, "config/webpacker.yml")) end def rails_version output, status = context.capture2e("rails", "--version") unless status.success? context.abort(context.message("core.app.create.rails.error.install_failure", "rails")) end version = output.scan(/Rails \d+\.\d+\.\d+/).first.split(" ").last ::Semantic::Version.new(version) end end end end end end