multitenancy-rails 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.standard.yml +3 -0
  4. data/AGENTS.md +67 -0
  5. data/CLAUDE.md +67 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +574 -0
  8. data/Rakefile +10 -0
  9. data/docs/README.md +72 -0
  10. data/docs/api.md +130 -0
  11. data/docs/generator.md +88 -0
  12. data/docs/getting-started.md +111 -0
  13. data/docs/integrations.md +145 -0
  14. data/docs/rake-tasks.md +53 -0
  15. data/docs/themes.md +159 -0
  16. data/lib/generators/multitenancy/multitenancy_generator.rb +88 -0
  17. data/lib/generators/multitenancy/templates/application_controller.rb +7 -0
  18. data/lib/generators/multitenancy/templates/home/index.html.erb +2 -0
  19. data/lib/generators/multitenancy/templates/home_controller.rb +9 -0
  20. data/lib/generators/multitenancy/templates/importmap.rb +4 -0
  21. data/lib/generators/multitenancy/templates/javascript/application.js +12 -0
  22. data/lib/generators/multitenancy/templates/javascript/controllers/hello_controller.js +7 -0
  23. data/lib/generators/multitenancy/templates/layouts/application.html.erb +32 -0
  24. data/lib/generators/multitenancy/templates/locales/en.yml +4 -0
  25. data/lib/generators/multitenancy/templates/routes.rb +6 -0
  26. data/lib/generators/multitenancy/templates/stylesheets/application.css +13 -0
  27. data/lib/generators/multitenancy/templates/tailwind/application.css +4 -0
  28. data/lib/multitenancy/controller.rb +25 -0
  29. data/lib/multitenancy/integrations/factory_bot.rb +15 -0
  30. data/lib/multitenancy/integrations/importmap.rb +79 -0
  31. data/lib/multitenancy/integrations/minitest.rb +49 -0
  32. data/lib/multitenancy/integrations/rails.rb +15 -0
  33. data/lib/multitenancy/integrations/rspec.rb +58 -0
  34. data/lib/multitenancy/integrations/tailwind_css.rb +51 -0
  35. data/lib/multitenancy/integrations.rb +12 -0
  36. data/lib/multitenancy/rails.rb +8 -0
  37. data/lib/multitenancy/railtie.rb +32 -0
  38. data/lib/multitenancy/stim.rb +39 -0
  39. data/lib/multitenancy/theme.rb +66 -0
  40. data/lib/multitenancy/version.rb +5 -0
  41. data/lib/multitenancy-rails.rb +1 -0
  42. data/lib/multitenancy.rb +74 -0
  43. data/lib/tasks/multitenancy_tailwindcss.rake +58 -0
  44. metadata +141 -0
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/base"
5
+
6
+ module Multitenancy
7
+ class MultitenancyGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ argument :theme_name, type: :string, desc: "Name of the theme to generate"
11
+ class_option :importmap, type: :boolean, default: true, desc: "Generate importmap configuration"
12
+ class_option :tailwindcss, type: :boolean, default: false, desc: "Generate Tailwind CSS configuration"
13
+
14
+ def create_theme_structure
15
+ @theme_name = theme_name.parameterize
16
+ @theme_class_name = theme_name.camelize
17
+ @theme_namespace = "Themes::#{@theme_class_name}"
18
+
19
+ create_directories
20
+ create_controllers
21
+ create_views
22
+ create_assets
23
+ create_config_files
24
+ end
25
+
26
+ private
27
+
28
+ def create_directories
29
+ dirs = %w[
30
+ app/controllers
31
+ app/views/home
32
+ app/views/layouts
33
+ config/locales
34
+ ]
35
+
36
+ if options[:tailwindcss]
37
+ dirs << "app/assets/tailwind/#{@theme_name}"
38
+ dirs << "app/assets/builds/#{@theme_name}"
39
+ else
40
+ dirs << "app/assets/stylesheets/#{@theme_name}"
41
+ end
42
+
43
+ dirs.each do |dir|
44
+ empty_directory File.join("themes", @theme_name, dir)
45
+ end
46
+
47
+ if options[:importmap]
48
+ empty_directory File.join("themes", @theme_name, "app/javascript/#{@theme_name}")
49
+ empty_directory File.join("themes", @theme_name, "app/javascript/#{@theme_name}/controllers")
50
+ end
51
+ end
52
+
53
+ def create_controllers
54
+ template "application_controller.rb", File.join("themes", @theme_name, "app/controllers/application_controller.rb")
55
+ template "home_controller.rb", File.join("themes", @theme_name, "app/controllers/home_controller.rb")
56
+ end
57
+
58
+ def create_views
59
+ template "layouts/application.html.erb", File.join("themes", @theme_name, "app/views/layouts/application.html.erb")
60
+ template "home/index.html.erb", File.join("themes", @theme_name, "app/views/home/index.html.erb")
61
+ end
62
+
63
+ def create_assets
64
+ if options[:tailwindcss]
65
+ template "tailwind/application.css", File.join("themes", @theme_name, "app/assets/tailwind/#{@theme_name}/application.css")
66
+ create_file File.join("themes", @theme_name, "app/assets/builds/#{@theme_name}/.keep")
67
+ else
68
+ template "stylesheets/application.css", File.join("themes", @theme_name, "app/assets/stylesheets/#{@theme_name}/application.css")
69
+ end
70
+
71
+ if options[:importmap]
72
+ template "javascript/application.js", File.join("themes", @theme_name, "app/javascript/#{@theme_name}/application.js")
73
+ template "javascript/controllers/hello_controller.js",
74
+ File.join("themes", @theme_name, "app/javascript/#{@theme_name}/controllers/hello_controller.js")
75
+ end
76
+ end
77
+
78
+ def create_config_files
79
+ template "routes.rb", File.join("themes", @theme_name, "config/routes.rb")
80
+ template "locales/en.yml", File.join("themes", @theme_name, "config/locales/en.yml")
81
+
82
+ if options[:importmap]
83
+ template "importmap.rb", File.join("themes", @theme_name, "config/importmap.rb")
84
+ end
85
+ end
86
+ end
87
+ end
88
+
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= @theme_namespace %>
4
+ class ApplicationController < ActionController::Base
5
+ include Multitenancy::Controller
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ <h1>Home#index</h1>
2
+ <p>Find me in themes/<%= @theme_name %>/views/home/index.html.erb</p>
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= @theme_namespace %>
4
+ class HomeController < ApplicationController
5
+ def index
6
+ end
7
+ end
8
+ end
9
+
@@ -0,0 +1,4 @@
1
+ pin "<%= @theme_name %>/application", to: "<%= @theme_name %>/application.js"
2
+
3
+ pin_all_from "themes/<%= @theme_name %>/app/javascript/<%= @theme_name %>/controllers",
4
+ under: "<%= @theme_name %>/controllers", to: "<%= @theme_name %>/controllers"
@@ -0,0 +1,12 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
3
+
4
+ const application = Application.start()
5
+ application.debug = false
6
+ window.Stimulus = application
7
+
8
+ // Shared controllers from the main app (data-controller="hello")
9
+ eagerLoadControllersFrom("controllers", application)
10
+
11
+ // Theme-specific controllers (overrides shared if same name)
12
+ eagerLoadControllersFrom("<%= @theme_name %>/controllers", application)
@@ -0,0 +1,7 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ this.element.textContent = "Hello from <%= @theme_class_name %> Theme!"
6
+ }
7
+ }
@@ -0,0 +1,32 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%%= content_for(:title) %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="mobile-web-app-capable" content="yes">
8
+ <%%= csrf_meta_tags %>
9
+ <%%= csp_meta_tag %>
10
+
11
+ <%%= yield :head %>
12
+
13
+ <%%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
14
+ <%%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
15
+
16
+ <link rel="icon" href="/icon.png" type="image/png">
17
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
18
+ <link rel="apple-touch-icon" href="/icon.png">
19
+
20
+ <%# Includes all stylesheet files in app/assets/stylesheets %>
21
+ <%%= stylesheet_link_tag "<%= @theme_name %>/application", "data-turbo-track": "reload" %>
22
+ <% if options[:importmap] %>
23
+ <%%= javascript_importmap_tags "<%= @theme_name %>/application", importmap: <%= @theme_namespace %>::Engine.importmap %>
24
+ <% end %>
25
+ </head>
26
+
27
+ <body>
28
+ <h1><%= @theme_class_name %> Layout</h1>
29
+ <%%= yield %>
30
+ </body>
31
+ </html>
32
+
@@ -0,0 +1,4 @@
1
+ en:
2
+ themes:
3
+ <%= @theme_name %>:
4
+ hello: "Hello world from the <%= @theme_name %> theme"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ <%= @theme_namespace %>::Engine.routes.draw do
4
+ root "home#index"
5
+ end
6
+
@@ -0,0 +1,13 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into <%= @theme_name %>/application.css.
3
+ *
4
+ * Any CSS file within this directory can be referenced here using a relative path.
5
+ *
6
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
7
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
8
+ * files in this directory. Styles in this file should be added after the last require_* statement.
9
+ * It is generally better to create a new file per style scope.
10
+ *
11
+ *= require_tree .
12
+ *= require_self
13
+ */
@@ -0,0 +1,4 @@
1
+ @import "tailwindcss";
2
+ @source "../../../views/**/*.html.erb";
3
+ @source "../../../controllers/**/*.rb";
4
+ @source "../../../javascript/<%= @theme_name %>/**/*.js";
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Multitenancy
6
+ module Controller
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ engine = module_parent::Engine
11
+ prepend_view_path engine.root.join("app/views")
12
+ layout "application"
13
+ before_action { engine.importmap_reloader&.execute_if_updated }
14
+ end
15
+
16
+ private
17
+
18
+ def _prefixes
19
+ namespace_prefix = "#{self.class.module_parent.name.underscore}/"
20
+ super.map do |prefix|
21
+ prefix.sub(/^#{Regexp.escape(namespace_prefix)}/, "")
22
+ end.reject(&:empty?)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multitenancy
4
+ module Integrations
5
+ class FactoryBot
6
+ def self.call(app)
7
+ return unless app.config.respond_to?(:factory_bot)
8
+
9
+ Multitenancy.themes.each do |theme|
10
+ app.config.factory_bot.definition_file_paths << theme.relative_path.join("spec/factories").to_s
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/core_ext/module/delegation"
5
+
6
+ module Multitenancy
7
+ module Integrations
8
+
9
+ class Importmap
10
+ class Reloader < Struct.new(:app, :engine)
11
+ delegate :execute_if_updated, :execute, :updated?, to: :updater
12
+
13
+ def reload!
14
+ engine.importmap = ::Importmap::Map.new
15
+ app_importmap_paths.each { |path| engine.importmap.draw(path) }
16
+ engine.importmap_paths.each { |path| engine.importmap.draw(path) }
17
+ end
18
+
19
+ private
20
+
21
+ def updater
22
+ @updater ||= app.config.file_watcher.new(engine.importmap_paths, cache_sweepers) { reload! }
23
+ end
24
+
25
+ def cache_sweepers
26
+ engine.importmap_cache_sweepers.each_with_object({}) do |path, hash|
27
+ hash[path] = ["js"]
28
+ end
29
+ end
30
+
31
+ def app_importmap_paths
32
+ app.config.importmap.paths
33
+ end
34
+ end
35
+
36
+ def self.call(app)
37
+ return unless app.config.respond_to?(:importmap)
38
+
39
+ Multitenancy.themes.each do |theme|
40
+ theme.engine.singleton_class.send(:attr_accessor, :importmap)
41
+ theme.engine.singleton_class.send(:attr_accessor, :importmap_paths)
42
+ theme.engine.singleton_class.send(:attr_accessor, :importmap_cache_sweepers)
43
+ theme.engine.singleton_class.send(:attr_accessor, :importmap_reloader)
44
+ theme.engine.importmap = ::Importmap::Map.new
45
+
46
+ # Store theme importmap paths separately — engine.config.importmap.paths
47
+ # is a shared object across all railties, so we can't use it per-theme.
48
+ importmap_file = theme.path.join("config/importmap.rb").to_s
49
+ theme.engine.importmap_paths = [importmap_file]
50
+
51
+ # Watch theme JavaScript directories for new/changed/deleted files
52
+ js_path = theme.path.join("app/javascript")
53
+ theme.engine.importmap_cache_sweepers = js_path.exist? ? [js_path.to_s] : []
54
+
55
+ # Draw the main app's importmap first (Turbo, Stimulus, shared libs)
56
+ app.config.importmap.paths.each { |path| theme.engine.importmap.draw(path) }
57
+
58
+ # Then draw the theme's own importmap (can override app pins)
59
+ theme.engine.importmap_paths.each { |path| theme.engine.importmap.draw(path) }
60
+
61
+ # Development reloader: re-draw theme importmap when pins or JS change
62
+ unless app.config.cache_classes
63
+ Multitenancy::Integrations::Importmap::Reloader.new(app, theme.engine).tap do |reloader|
64
+ theme.engine.importmap_reloader = reloader
65
+ app.reloaders << reloader
66
+ app.reloader.to_run { reloader.execute_if_updated }
67
+ end
68
+ end
69
+ end
70
+
71
+ # Remove theme importmap paths from the main app's shared config
72
+ # so the main app doesn't draw theme-specific pins into its own importmap.
73
+ app.config.importmap.paths.delete_if do |path|
74
+ path.to_s =~ %r{^#{Multitenancy.themes_root}/}
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multitenancy
4
+ module Integrations
5
+ class Minitest
6
+ def self.call(app)
7
+ return unless defined?(::Minitest)
8
+ return unless defined?(::Rails::TestUnit::Runner)
9
+
10
+ integration = self
11
+
12
+ ::Rails::TestUnit::Runner.singleton_class.prepend(Module.new do
13
+ define_method(:load_tests) do |argv|
14
+ if argv.any?
15
+ # Explicit paths: expand theme directory paths to their test/ subdirs,
16
+ # including nested themes (e.g. themes/alpha also picks up themes/alpha-v2).
17
+ expanded = argv.flat_map do |path|
18
+ if (theme = Multitenancy.themes.find { |t| t.relative_path.to_s == path })
19
+ [theme, *integration.nested_themes_for(theme)].filter_map do |t|
20
+ test_path = t.relative_path.join("test")
21
+ test_path.to_s if test_path.exist?
22
+ end
23
+ else
24
+ path
25
+ end
26
+ end
27
+ super(expanded)
28
+ else
29
+ # Default run (rails test with no args): load main app tests first,
30
+ # then load each theme's test/ directory separately.
31
+ theme_test_dirs = Multitenancy.themes.filter_map do |theme|
32
+ test_path = theme.relative_path.join("test")
33
+ test_path.to_s if test_path.exist?
34
+ end
35
+ super(argv)
36
+ super(theme_test_dirs) unless theme_test_dirs.empty?
37
+ end
38
+ end
39
+ end)
40
+ end
41
+
42
+ def self.nested_themes_for(parent_theme)
43
+ Multitenancy.themes.select do |theme|
44
+ theme.name != parent_theme.name && theme.name.include?(parent_theme.name)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflections"
4
+
5
+ module Multitenancy
6
+ module Integrations
7
+ class Rails
8
+ def self.call(app)
9
+ Multitenancy.themes.each do |theme|
10
+ theme.bootstrap(app)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multitenancy
4
+ module Integrations
5
+ class RSpec
6
+ def self.call(app)
7
+ return unless defined?(::RSpec)
8
+ return unless ::RSpec.respond_to?(:configuration)
9
+
10
+ # This is the list of directories RSpec was told to run.
11
+ to_run = ::RSpec.configuration.instance_variable_get(:@files_or_directories_to_run)
12
+ default_path = ::RSpec.configuration.default_path
13
+
14
+ if to_run == [default_path]
15
+ # This is the default case when you run `rspec`. We want to add all the theme's spec paths
16
+ # to the collection of directories to run.
17
+
18
+ theme_paths = Multitenancy.themes.map do |theme|
19
+ spec_path = theme.relative_path.join(default_path)
20
+ spec_path.to_s if spec_path.exist?
21
+ end
22
+
23
+ to_run.concat(theme_paths)
24
+ else
25
+ # This is when `rspec` is run with a list of directories or files. We scan this list to see
26
+ # if any of them matches a theme's directory. If it does, we concat the `default_path` to the
27
+ # end of it.
28
+ #
29
+ # themes/my_theme => themes/my_theme/spec
30
+ #
31
+ # If it doesn't match a theme path, we leave it alone.
32
+ to_run.map! do |path|
33
+ if (theme = Multitenancy.themes.find { |t| t.relative_path.to_s == path })
34
+ [
35
+ theme,
36
+ *nested_themes_for(theme)
37
+ ].map do |theme|
38
+ spec_path = theme.relative_path.join(default_path)
39
+ spec_path.to_s if spec_path.exist?
40
+ end
41
+ else
42
+ path
43
+ end
44
+ end
45
+ end
46
+
47
+ ::RSpec.configuration.files_or_directories_to_run = to_run.flatten.compact.uniq
48
+ end
49
+
50
+ def self.nested_themes_for(parent_theme)
51
+ Multitenancy.themes.select do |theme|
52
+ theme.name != parent_theme.name && theme.name.include?(parent_theme.name)
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multitenancy
4
+ module Integrations
5
+ class TailwindCss
6
+ def self.call(app)
7
+ return unless defined?(::Tailwindcss)
8
+
9
+ Multitenancy.themes.each do |theme|
10
+ tailwind_dir = theme.path.join("app/assets/tailwind")
11
+ next unless tailwind_dir.exist?
12
+
13
+ # Exclude raw Tailwind input files from Propshaft — they contain
14
+ # @import directives that can't be served directly.
15
+ app.config.assets.excluded_paths << tailwind_dir
16
+
17
+ # Ensure the builds output directory exists so the compiled CSS
18
+ # can be picked up by Propshaft.
19
+ builds_dir = theme.path.join("app/assets/builds/#{theme.name}")
20
+ FileUtils.mkdir_p(builds_dir)
21
+ end
22
+ end
23
+
24
+ def self.compilation_targets
25
+ return [] unless defined?(::Tailwindcss)
26
+
27
+ Multitenancy.themes.each_with_object([]) do |theme, targets|
28
+ input = theme.path.join("app/assets/tailwind/#{theme.name}/application.css")
29
+ next unless input.exist?
30
+
31
+ output = theme.path.join("app/assets/builds/#{theme.name}/application.css")
32
+ targets << { theme: theme, input: input, output: output }
33
+ end
34
+ end
35
+
36
+ def self.compile_command(target, debug: false)
37
+ args = [
38
+ Tailwindcss::Ruby.executable,
39
+ "--input", target[:input].to_s,
40
+ "--output", target[:output].to_s
41
+ ]
42
+ args << "--minify" unless debug
43
+ args
44
+ end
45
+
46
+ def self.watch_command(target, debug: false)
47
+ compile_command(target, debug: debug) + ["--watch"]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,12 @@
1
+ require 'active_support'
2
+
3
+ module Multitenancy
4
+ module Integrations
5
+ autoload :FactoryBot, "multitenancy/integrations/factory_bot"
6
+ autoload :Importmap, "multitenancy/integrations/importmap"
7
+ autoload :Rails, "multitenancy/integrations/rails"
8
+ autoload :TailwindCss, "multitenancy/integrations/tailwind_css"
9
+ autoload :Minitest, "multitenancy/integrations/minitest"
10
+ autoload :RSpec, "multitenancy/integrations/rspec"
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "multitenancy"
4
+
5
+ module Multitenancy
6
+ module Rails
7
+ end
8
+ end
@@ -0,0 +1,32 @@
1
+ require "rails/railtie"
2
+
3
+ module Multitenancy
4
+ class Railtie < ::Rails::Railtie
5
+ config.before_configuration do |app|
6
+ Integrations::Rails.call(app)
7
+ Integrations::FactoryBot.call(app)
8
+ Integrations::RSpec.call(app)
9
+ Integrations::Minitest.call(app)
10
+
11
+ # Must run before Propshaft's `propshaft.append_assets_path` initializer,
12
+ # which is where `config.assets.excluded_paths` is consumed
13
+ # (`paths.without(excluded_paths)`). Registering the exclusion any later —
14
+ # e.g. in `after_initialize` — leaves the raw Tailwind input directory on
15
+ # the asset load path, where it collides with the compiled `builds/`
16
+ # output and can be served as un-compiled `@import "tailwindcss"` source.
17
+ Integrations::TailwindCss.call(app)
18
+
19
+ # This is not used within multitenancy. Rather, this allows OTHER tools to
20
+ # hook into multitenancy via ActiveSupport hooks.
21
+ ActiveSupport.run_load_hooks(:multitenancy, Multitenancy)
22
+ end
23
+
24
+ config.after_initialize do |app|
25
+ Integrations::Importmap.call(app)
26
+ end
27
+
28
+ rake_tasks do
29
+ load "tasks/multitenancy_tailwindcss.rake"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multitenancy
4
+ class Stim < Module
5
+ attr_reader :theme, :namespace
6
+
7
+ def initialize(theme, namespace)
8
+ @theme = theme
9
+ @namespace = namespace
10
+ super()
11
+ end
12
+
13
+ def included(engine)
14
+ engine.called_from = theme.path
15
+ engine.isolate_namespace(namespace)
16
+ engine.config.root = theme.path
17
+
18
+ views_path = theme.path.join("app/views")
19
+ if views_path.exist?
20
+ engine.paths["app/views"] = [views_path]
21
+ end
22
+
23
+ assets_path = theme.path.join("app/assets")
24
+ if assets_path.exist?
25
+ engine.paths["app/assets"] = [assets_path]
26
+ end
27
+
28
+ locale_path = theme.path.join("config/locales")
29
+ if locale_path.exist?
30
+ engine.paths["config/locales"] = [locale_path]
31
+ end
32
+
33
+ js_path = theme.path.join("app/javascript")
34
+ if js_path.exist?
35
+ engine.paths["app/javascript"] = [js_path]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflections"
4
+ require "rails/engine"
5
+
6
+ module Multitenancy
7
+ class Theme
8
+ attr_reader :path, :namespace, :engine
9
+
10
+ def initialize(path:)
11
+ @path = path
12
+ @_bootstrapped = false
13
+ end
14
+
15
+ def bootstrap(app)
16
+ return if @_bootstrapped
17
+
18
+ @namespace = create_namespace
19
+ @engine = create_engine
20
+ inject_paths(app)
21
+
22
+ @_bootstrapped = true
23
+ end
24
+
25
+ def relative_path
26
+ path.relative_path_from(::Rails.root)
27
+ end
28
+
29
+ def name
30
+ path.basename.to_s.parameterize
31
+ end
32
+
33
+ private
34
+
35
+ def create_namespace
36
+ namespace_name = "themes/#{name}"
37
+ namespace = ActiveSupport::Inflector.camelize(namespace_name)
38
+ namespace.split("::").reduce(Object) do |base, mod|
39
+ if base.const_defined?(mod, false)
40
+ base.const_get(mod, false)
41
+ else
42
+ base.const_set(mod, Module.new)
43
+ end
44
+ end
45
+ end
46
+
47
+ def create_engine
48
+ stim = Stim.new(self, namespace)
49
+ namespace.const_set("Engine", Class.new(::Rails::Engine)).include(stim)
50
+ end
51
+
52
+ def inject_paths(app)
53
+ Multitenancy.config.paths.each do |path|
54
+ location = relative_path.join(path)
55
+
56
+ next unless location.exist?
57
+ next unless location.directory?
58
+
59
+ app.autoloaders.main.push_dir(location.to_s, namespace: namespace)
60
+ end
61
+
62
+ js_path = self.path.join("app/javascript")
63
+ app.config.assets.paths << js_path.to_s if js_path.exist?
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multitenancy
4
+ VERSION = "0.0.2"
5
+ end