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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/AGENTS.md +67 -0
- data/CLAUDE.md +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +574 -0
- data/Rakefile +10 -0
- data/docs/README.md +72 -0
- data/docs/api.md +130 -0
- data/docs/generator.md +88 -0
- data/docs/getting-started.md +111 -0
- data/docs/integrations.md +145 -0
- data/docs/rake-tasks.md +53 -0
- data/docs/themes.md +159 -0
- data/lib/generators/multitenancy/multitenancy_generator.rb +88 -0
- data/lib/generators/multitenancy/templates/application_controller.rb +7 -0
- data/lib/generators/multitenancy/templates/home/index.html.erb +2 -0
- data/lib/generators/multitenancy/templates/home_controller.rb +9 -0
- data/lib/generators/multitenancy/templates/importmap.rb +4 -0
- data/lib/generators/multitenancy/templates/javascript/application.js +12 -0
- data/lib/generators/multitenancy/templates/javascript/controllers/hello_controller.js +7 -0
- data/lib/generators/multitenancy/templates/layouts/application.html.erb +32 -0
- data/lib/generators/multitenancy/templates/locales/en.yml +4 -0
- data/lib/generators/multitenancy/templates/routes.rb +6 -0
- data/lib/generators/multitenancy/templates/stylesheets/application.css +13 -0
- data/lib/generators/multitenancy/templates/tailwind/application.css +4 -0
- data/lib/multitenancy/controller.rb +25 -0
- data/lib/multitenancy/integrations/factory_bot.rb +15 -0
- data/lib/multitenancy/integrations/importmap.rb +79 -0
- data/lib/multitenancy/integrations/minitest.rb +49 -0
- data/lib/multitenancy/integrations/rails.rb +15 -0
- data/lib/multitenancy/integrations/rspec.rb +58 -0
- data/lib/multitenancy/integrations/tailwind_css.rb +51 -0
- data/lib/multitenancy/integrations.rb +12 -0
- data/lib/multitenancy/rails.rb +8 -0
- data/lib/multitenancy/railtie.rb +32 -0
- data/lib/multitenancy/stim.rb +39 -0
- data/lib/multitenancy/theme.rb +66 -0
- data/lib/multitenancy/version.rb +5 -0
- data/lib/multitenancy-rails.rb +1 -0
- data/lib/multitenancy.rb +74 -0
- data/lib/tasks/multitenancy_tailwindcss.rake +58 -0
- 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,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,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,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,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,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,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
|