Skip to content

Commit

Permalink
Deprecate solidus_frontend & allow installing solidus_starter_frontend
Browse files Browse the repository at this point in the history
We're recommending new stores use solidus_starter_frontend [1] as their
storefront. However, we still want to give the option to install
solidus_frontend for now, as some extensions rely on it. We're modifying
the Solidus installer to allow installing one or the other.

Nonetheless, solidus_frontend is part of the solidus meta-gem [2], which
is what is usually bundled in a new installation. At this point, we
can't remove solidus_frontend from the meta-gem, as users upgrading from
old Solidus versions could see their storefronts suddenly gone. As that
will be a breaking change, we're deferring it until v4.0.

Until that happens, we must prevent users choosing the new frontend from
pulling solidus_frontend. Because of that, we're breaking down the
solidus gem into solidus_core, solidus_api, solidus_backend &
solidus_sample. We leverage Bundler's injector [3] to add dependencies
matching the main solidus gem constraints in the Gemfile. We also use it
to remove the meta-gem. Still, we need to extend the upstream injector
to support the `path:` option, which is required for local testing
(e.g., the sandbox application). We also must break down the meta-gem
when solidus_frontend is installed until we remove the frontend
directory from this repo. That prevents Bundler from resolving it from
there when `path:` or `git:` are used as sources.

As per the logic to detect which frontend gets installed, the first of
the following conditions met wins:

- It's explicitly selected with the `FRONTEND` environment variable
  (useful for sandbox applications).
- It's explicitly selected with the `frontend` option.
- If `solidus_frontend` is in the Gemfile, that's the one used.
- If the `auto_accept` option is given, `solidus_starter_frontend` is
  chosen.
- The user is prompted to select one or the other.

Extensions have been modified to explicitly depend on `solidus_frontend`
on their Gemfile, so the dummy app generator used for testing will work
OOO matching the third condition. As per its sandbox application,
they'll default to `solidus_starter_frontend` because they use
`auto_accept`, but they can select `solidus_frontend` through the
`FRONTEND` variable.

[1] - https://github.com/solidusio/solidus_starter_frontend
[2] - https://github.com/solidusio/solidus/blob/a22723079f88387f53f72b503db875b9d97aa3f5/solidus.gemspec#L27
[3] - https://github.com/rubygems/bundler/blob/master/lib/bundler/injector.rb
  • Loading branch information
waiting-for-dev committed Aug 9, 2022
1 parent 9398ab9 commit ebfd2ed
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 43 deletions.
65 changes: 56 additions & 9 deletions core/lib/generators/solidus/install/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,23 @@

require 'rails/generators'
require 'rails/version'
require_relative 'install_generator/bundler_context'
require_relative 'install_generator/support_solidus_frontend_extraction'
require_relative 'install_generator/install_frontend'

module Solidus
# @private
class InstallGenerator < Rails::Generators::Base
CORE_MOUNT_ROUTE = "mount Spree::Core::Engine"

LEGACY_FRONTEND = 'solidus_frontend'
DEFAULT_FRONTEND = 'solidus_starter_frontend'
FRONTENDS = [
DEFAULT_FRONTEND,
LEGACY_FRONTEND,
'none'
].freeze

class_option :migrate, type: :boolean, default: true, banner: 'Run Solidus migrations'
class_option :seed, type: :boolean, default: true, banner: 'Load seed data (migrations must be run)'
class_option :sample, type: :boolean, default: true, banner: 'Load sample data (migrations must be run)'
Expand All @@ -19,6 +30,11 @@ class InstallGenerator < Rails::Generators::Base
class_option :lib_name, type: :string, default: 'spree'
class_option :with_authentication, type: :boolean, default: true
class_option :enforce_available_locales, type: :boolean, default: nil
class_option :frontend,
type: :string,
enum: FRONTENDS,
default: nil,
desc: "Indicates which frontend to install."

def self.source_paths
paths = superclass.source_paths
Expand Down Expand Up @@ -74,15 +90,9 @@ def setup_assets
empty_directory 'app/assets/images'

%w{javascripts stylesheets images}.each do |path|
empty_directory "vendor/assets/#{path}/spree/frontend" if defined?(Spree::Frontend) || Rails.env.test?
empty_directory "vendor/assets/#{path}/spree/backend" if defined?(Spree::Backend) || Rails.env.test?
end

if defined?(Spree::Frontend) || Rails.env.test?
template "vendor/assets/javascripts/spree/frontend/all.js"
template "vendor/assets/stylesheets/spree/frontend/all.css"
end

if defined?(Spree::Backend) || Rails.env.test?
template "vendor/assets/javascripts/spree/backend/all.js"
template "vendor/assets/stylesheets/spree/backend/all.css"
Expand Down Expand Up @@ -112,6 +122,9 @@ def install_auth_plugin
Solidus has a default authentication extension that uses Devise.
You can find more info at https://github.com/solidusio/solidus_auth_devise.
Regardless of what you answer here, it'll be installed if you choose
solidus_starter_frontend as your storefront in a later step.
Would you like to install it? (Y/n)"))

@plugins_to_be_installed << 'solidus_auth_devise'
Expand Down Expand Up @@ -142,14 +155,30 @@ def run_bundle_install_if_needed_by_plugins
gem plugin_name
end

bundle_cleanly{ run "bundle install" } if @plugins_to_be_installed.any?
BundlerContext.bundle_cleanly { run "bundle install" } if @plugins_to_be_installed.any?
run "spring stop" if defined?(Spring)

@plugin_generators_to_run.each do |plugin_generator_name|
generate "#{plugin_generator_name} --skip_migrations=true"
end
end

def install_frontend
return if options[:frontend] == 'none'

bundler_context = BundlerContext.new

frontend = detect_frontend_to_install(bundler_context)

support_solidus_frontend_extraction(bundler_context)

say_status :installing, frontend

InstallFrontend.
new(bundler_context: bundler_context, generator_context: self).
call(frontend, installer_adds_auth: @plugins_to_be_installed.include?('solidus_auth_devise'))
end

def run_migrations
if @run_migrations
say_status :running, "migrations"
Expand Down Expand Up @@ -218,8 +247,26 @@ def complete

private

def bundle_cleanly(&block)
Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&block) : Bundler.with_clean_env(&block)
def detect_frontend_to_install(bundler_context)
ENV['FRONTEND'] ||
options[:frontend] ||
(bundler_context.component_in_gemfile?(:frontend) && LEGACY_FRONTEND) ||
(options[:auto_accept] && DEFAULT_FRONTEND) ||
ask(<<~MSG.indent(8), limited_to: FRONTENDS, default: DEFAULT_FRONTEND)
Which frontend would you like to use? solidus_starter_frontend is
recommended. However, some extensions are still only compatible with
the now deprecated solidus_frontend.
MSG
end

def support_solidus_frontend_extraction(bundler_context)
say_status "break down", "solidus"

SupportSolidusFrontendExtraction.
new(bundler_context: bundler_context).
call
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# frozen_string_literal: true

module Solidus
class InstallGenerator < Rails::Generators::Base
# Bundler context during the install process.
#
# This class gives access to information about the bundler context in which
# the install generator is run. I.e., which solidus components are present
# in the user's Gemfile. It also allows modifying the Gemfile to add or
# remove gems.
#
# @api private
class BundlerContext
# Write and remove into a Gemfile, supporting the `path: ` option.
#
# This custom injector adds support for path sources, which is missing in
# bundler's upstream injector.
#
# @api private
class InjectorWithPathSupport < Bundler::Injector
private def build_gem_lines(conservative_versioning)
@deps.map do |d|
name = d.name.dump
local = d.source.is_a?(Bundler::Source::Path)

requirement = if local
", path: \"#{d.source.path}\""
elsif conservative_versioning
", \"#{conservative_version(@definition.specs[d.name][0])}\""
else
", #{d.requirement.as_list.map(&:dump).join(", ")}"
end

if d.groups != Array(:default)
group = d.groups.size == 1 ? ", :group => #{d.groups.first.inspect}" : ", :groups => #{d.groups.inspect}"
end

source = ", :source => \"#{d.source}\"" unless local || d.source.nil?
git = ", :git => \"#{d.git}\"" unless d.git.nil?
branch = ", :branch => \"#{d.branch}\"" unless d.branch.nil?

%(gem #{name}#{requirement}#{group}#{source}#{git}#{branch})
end.join("\n")
end
end

attr_reader :dependencies, :injector

def self.bundle_cleanly(&block)
Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&block) : Bundler.with_clean_env(&block)
end

def initialize
@dependencies = Bundler.locked_gems.dependencies
@injector = InjectorWithPathSupport
end

def solidus_in_gemfile?
!solidus_dependency.nil?
end

def component_in_gemfile?(name)
!@dependencies["solidus_#{name}"].nil?
end

def break_down_components(components)
raise <<~MSG unless solidus_in_gemfile?
solidus meta gem needs to be present in the Gemfile to build the component dependency
MSG

@injector.inject(
components.map { |component| dependency_for_component(component) }
)
end

def remove(*args, **kwargs, &block)
@injector.remove(*args, **kwargs, &block)
end

private

def dependency_for_component(component)
Bundler::Dependency.new(
"solidus_#{component}",
solidus_dependency.requirement,
{
"groups" => solidus_dependency.groups,
"source" => solidus_dependency.source,
"git" => solidus_dependency.git,
"branch" => solidus_dependency.branch,
"autorequire" => solidus_dependency.autorequire
}
)
end

def solidus_dependency
@dependencies['solidus']
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

module Solidus
class InstallGenerator < Rails::Generators::Base
class InstallFrontend
attr_reader :bundler_context,
:generator_context

def initialize(bundler_context:, generator_context:)
@bundler_context = bundler_context
@generator_context = generator_context
end

def call(frontend, installer_adds_auth:)
case frontend
when 'solidus_frontend'
install_solidus_frontend
when 'solidus_starter_frontend'
install_solidus_starter_frontend(installer_adds_auth)
end
end

private

def install_solidus_frontend
unless @bundler_context.component_in_gemfile?(:frontend)
BundlerContext.bundle_cleanly do
`bundle add solidus_frontend --source=https://rubygems.org`
`bundle install`
end
end

@generator_context.generate('solidus_frontend:install')
end

def install_solidus_starter_frontend(installer_adds_auth)
@bundler_context.remove(['solidus_frontend']) if @bundler_context.component_in_gemfile?(:frontend)

# TODO: Move installation of solidus_auth_devise to the
# solidus_starter_frontend template
BundlerContext.bundle_cleanly { `bundle add solidus_auth_devise` } unless auth_present?(installer_adds_auth)
`LOCATION="https://raw.githubusercontent.com/solidusio/solidus_starter_frontend/main/template.rb" bin/rails app:template`
end

def auth_present?(installer_adds_auth)
installer_adds_auth || @bundler_context.component_in_gemfile?(:auth_devise)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

module Solidus
class InstallGenerator < Rails::Generators::Base
# Helper for extracting solidus_frontend from solidus meta-gem
#
# We're recommending users use newer solidus_starter_frontend. However,
# we're still shipping solidus_frontend as part of the solidus meta-gem. The
# reason is that we don't want users updating previous versions to see its
# storefront gone suddenly.
#
# In future solidus releases, solidus_frontend won't be a component anymore.
# However, until that happens:
#
# - For users of the new frontend, we need to prevent pulling
# solidus_frontend.
# - For users of the legacy frontend, we need to prevent Bundler from
# resolving it from the mono-repo while it's still there.
#
# This class is a needed companion during the deprecation
# path. It'll modify the user's Gemfile, breaking the solidus gem down into
# its components but solidus_frontend.
class SupportSolidusFrontendExtraction
attr_reader :bundler_context

def initialize(bundler_context:)
@bundler_context = bundler_context
end

def call
return unless needs_to_break_down_solidus_meta_gem?

break_down_solidus_meta_gem
end

private

def break_down_solidus_meta_gem
@bundler_context.break_down_components(%w[core backend api sample])
@bundler_context.remove(['solidus'])
end

def needs_to_break_down_solidus_meta_gem?
@bundler_context.solidus_in_gemfile?
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,6 @@ Spree.config do |config|
# to a custom users role:
# config.roles.assign_permissions :role_name, ['Spree::PermissionSets::CustomPermissionSet']

# Frontend:

# Custom logo for the frontend
# config.logo = "logo/solidus.svg"

# Template to use when rendering layout
# config.layout = "spree/layouts/spree_application"


# Admin:

# Custom logo for the admin
Expand All @@ -61,12 +52,6 @@ Spree.config do |config|
# )
end

<% if defined?(Spree::Frontend::Engine) -%>
Spree::Frontend::Config.configure do |config|
config.locale = 'en'
end
<% end -%>

<% if defined?(Spree::Backend::Engine) -%>
Spree::Backend::Config.configure do |config|
config.locale = 'en'
Expand Down

This file was deleted.

This file was deleted.

0 comments on commit ebfd2ed

Please sign in to comment.