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
data/docs/api.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## `Multitenancy` (module)
|
|
4
|
+
|
|
5
|
+
| Method | Description |
|
|
6
|
+
|--------|-------------|
|
|
7
|
+
| `Multitenancy.root` | Rails application root. |
|
|
8
|
+
| `Multitenancy.themes_root` | Path to `themes/` directory. |
|
|
9
|
+
| `Multitenancy.themes` | Array of `Theme` instances, one per `themes/<name>/` directory. Memoized. |
|
|
10
|
+
| `Multitenancy.reset!` | Clear memoized themes. Useful in tests. |
|
|
11
|
+
| `Multitenancy.config` | Global `Config` instance. |
|
|
12
|
+
|
|
13
|
+
Themes are discovered lazily on first access to `.themes` — at Rails boot time they're bootstrapped from the Railtie.
|
|
14
|
+
|
|
15
|
+
## `Multitenancy::Config`
|
|
16
|
+
|
|
17
|
+
| Attribute | Default | Purpose |
|
|
18
|
+
|-----------|---------|---------|
|
|
19
|
+
| `paths` | 20+ standard Rails paths | Autoload directories to register per theme. |
|
|
20
|
+
|
|
21
|
+
Default `paths`:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
%w[
|
|
25
|
+
app/controllers app/channels app/helpers app/services app/structs
|
|
26
|
+
app/models app/mailers app/presenters app/decorators app/queries
|
|
27
|
+
app/resources app/serializers app/transformers app/validators
|
|
28
|
+
app/workers app/jobs app/notifications app/policies lib
|
|
29
|
+
]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Customize in an initializer:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Multitenancy.config.paths = %w[app/controllers app/models lib]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## `Multitenancy::Theme`
|
|
39
|
+
|
|
40
|
+
Represents one theme directory.
|
|
41
|
+
|
|
42
|
+
| Method | Description |
|
|
43
|
+
|--------|-------------|
|
|
44
|
+
| `#name` | Theme name (basename of the directory). |
|
|
45
|
+
| `#path` | `Pathname` to the theme's root. |
|
|
46
|
+
| `#namespace` | The dynamically-created `Themes::<Name>` module. |
|
|
47
|
+
| `#engine` | The dynamically-created `Themes::<Name>::Engine` class. |
|
|
48
|
+
| `#bootstrap(app)` | Creates the namespace, builds the engine, registers paths. Called by the Railtie. |
|
|
49
|
+
|
|
50
|
+
You rarely construct `Theme`s yourself — iterate `Multitenancy.themes` instead.
|
|
51
|
+
|
|
52
|
+
## `Multitenancy::Controller`
|
|
53
|
+
|
|
54
|
+
`ActiveSupport::Concern`. Include in theme application controllers.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# themes/storefront/app/controllers/application_controller.rb
|
|
58
|
+
module Themes::Storefront
|
|
59
|
+
class ApplicationController < ::ApplicationController
|
|
60
|
+
include Multitenancy::Controller
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Configures:
|
|
66
|
+
|
|
67
|
+
- `prepend_view_path engine.root.join('app/views')` — theme views win.
|
|
68
|
+
- `layout 'application'` — use the theme's layout.
|
|
69
|
+
- `before_action { engine.importmap_reloader&.execute_if_updated }` — reload on JS changes in dev.
|
|
70
|
+
|
|
71
|
+
### Private: `_prefixes`
|
|
72
|
+
|
|
73
|
+
Overridden to strip the theme namespace prefix from `controller_path`, so view lookup works naturally:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Themes::Storefront::HomeController → home/index (not themes/storefront/home/index)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## `Multitenancy::Stim`
|
|
80
|
+
|
|
81
|
+
Internal — the mixin included into each theme's engine class. It configures:
|
|
82
|
+
|
|
83
|
+
- `isolate_namespace`
|
|
84
|
+
- `called_from` (for `Rails::Engine#root`)
|
|
85
|
+
- `paths["app/views"]`
|
|
86
|
+
- `paths["app/assets"]`
|
|
87
|
+
- `paths["config/locales"]`
|
|
88
|
+
- `paths["app/javascript"]`
|
|
89
|
+
|
|
90
|
+
You should not need to interact with it directly.
|
|
91
|
+
|
|
92
|
+
## `Multitenancy::Integrations`
|
|
93
|
+
|
|
94
|
+
Namespace for optional-gem integrations. Each is a class with a `.call(app)` class method.
|
|
95
|
+
|
|
96
|
+
| Integration | Guarded by | What it does |
|
|
97
|
+
|-------------|------------|--------------|
|
|
98
|
+
| `Rails` | always | Bootstraps every theme (creates namespace, engine, path registration). |
|
|
99
|
+
| `FactoryBot` | `defined?(::FactoryBot)` | Adds each theme's `spec/factories/` to FactoryBot's paths. |
|
|
100
|
+
| `RSpec` | `defined?(::RSpec)` | Expands `rspec themes/<name>` to `themes/<name>/spec`; auto-discovers all theme specs by default. |
|
|
101
|
+
| `Minitest` | `defined?(::Rails::TestUnit::Runner)` | Same discovery behavior for Minitest. |
|
|
102
|
+
| `Importmap` | `defined?(::Importmap::Map)` | Creates per-theme importmaps; removes theme paths from main app's importmap; wires dev reloader. |
|
|
103
|
+
| `TailwindCss` | `defined?(::Tailwindcss::Commands)` | Discovers theme Tailwind inputs, sets builds dirs, excludes raw inputs from Propshaft. |
|
|
104
|
+
|
|
105
|
+
## Load hook
|
|
106
|
+
|
|
107
|
+
An `ActiveSupport.on_load(:multitenancy)` hook fires after bootstrap completes:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# config/initializers/multitenancy_custom.rb
|
|
111
|
+
ActiveSupport.on_load(:multitenancy) do |multitenancy|
|
|
112
|
+
# multitenancy is the Multitenancy module
|
|
113
|
+
# Multitenancy.themes is fully populated at this point
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Generator
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
bin/rails generate multitenancy THEME_NAME [--importmap] [--tailwindcss]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
See [generator.md](generator.md).
|
|
124
|
+
|
|
125
|
+
## Rake tasks
|
|
126
|
+
|
|
127
|
+
- `multitenancy:tailwindcss:build`
|
|
128
|
+
- `multitenancy:tailwindcss:watch`
|
|
129
|
+
|
|
130
|
+
See [rake-tasks.md](rake-tasks.md).
|
data/docs/generator.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Generator
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
bin/rails generate multitenancy THEME_NAME [options]
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Scaffolds a new theme under `themes/<theme_name>/` with a minimal but runnable structure.
|
|
8
|
+
|
|
9
|
+
## Options
|
|
10
|
+
|
|
11
|
+
| Flag | Adds |
|
|
12
|
+
|------|------|
|
|
13
|
+
| `--importmap` | `app/javascript/<name>/application.js` and a sample Stimulus controller. |
|
|
14
|
+
| `--tailwindcss` | `app/assets/tailwind/<name>/application.css` with a Tailwind v4 `@import` and a default theme block. |
|
|
15
|
+
|
|
16
|
+
Flags can be combined:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bin/rails g multitenancy storefront --importmap --tailwindcss
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What gets created
|
|
23
|
+
|
|
24
|
+
### Always
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
themes/<name>/
|
|
28
|
+
├── app/controllers/application_controller.rb
|
|
29
|
+
├── app/controllers/home_controller.rb
|
|
30
|
+
├── app/views/layouts/application.html.erb
|
|
31
|
+
├── app/views/home/index.html.erb
|
|
32
|
+
├── config/routes.rb
|
|
33
|
+
└── config/locales/en.yml
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `ApplicationController` subclasses the main app's `::ApplicationController` and includes `Multitenancy::Controller`.
|
|
37
|
+
- `HomeController` has a single `index` action.
|
|
38
|
+
- `routes.rb` has `root to: 'home#index'`.
|
|
39
|
+
|
|
40
|
+
### With `--tailwindcss`
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
themes/<name>/
|
|
44
|
+
└── app/assets/tailwind/<name>/application.css
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```css
|
|
48
|
+
@import 'tailwindcss';
|
|
49
|
+
|
|
50
|
+
@theme {
|
|
51
|
+
/* design tokens here */
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A `bin/rails multitenancy:tailwindcss:build` run compiles this to `themes/<name>/app/assets/builds/<name>/application.css`, and it's hooked into `assets:precompile` so production deploys compile it automatically.
|
|
56
|
+
|
|
57
|
+
### With `--importmap`
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
themes/<name>/
|
|
61
|
+
└── app/javascript/<name>/
|
|
62
|
+
├── application.js # Stimulus entrypoint
|
|
63
|
+
└── controllers/
|
|
64
|
+
├── application.js # Stimulus Application instance
|
|
65
|
+
├── hello_controller.js # Example controller
|
|
66
|
+
└── index.js # Eager-register controllers
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The theme's layout is wired to draw the theme-scoped importmap and `javascript_importmap_tags` tag:
|
|
70
|
+
|
|
71
|
+
```erb
|
|
72
|
+
<%= javascript_importmap_tags "<name>/application" %>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
In development, changes to JS files trigger the theme's importmap reloader automatically (wired by the `Multitenancy::Controller` concern).
|
|
76
|
+
|
|
77
|
+
## Prerequisites for flags
|
|
78
|
+
|
|
79
|
+
- `--tailwindcss` requires `tailwindcss-rails ~> 4.4`. The gem targets Tailwind CSS v4 (CSS-first config).
|
|
80
|
+
- `--importmap` requires `importmap-rails` in your Gemfile.
|
|
81
|
+
|
|
82
|
+
If a flag's gem isn't present, the generator skips that part silently — but the theme still works, you just won't get the asset/JS scaffolding.
|
|
83
|
+
|
|
84
|
+
## After generating
|
|
85
|
+
|
|
86
|
+
1. Mount the theme in `config/routes/multitenancy.rb` (see [getting-started.md](getting-started.md)).
|
|
87
|
+
2. If you used `--tailwindcss`, run `bin/rails multitenancy:tailwindcss:build` once to generate the compiled CSS.
|
|
88
|
+
3. If you used `--importmap`, pin any shared libraries in the theme's layout or in the main `config/importmap.rb`.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
# Gemfile
|
|
7
|
+
gem 'multitenancy-rails'
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bundle install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Generate your first theme
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bin/rails generate multitenancy storefront
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Optional flags:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bin/rails generate multitenancy storefront --tailwindcss --importmap
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The generator creates `themes/storefront/` with a minimal Rails app structure:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
themes/storefront/
|
|
30
|
+
├── app/
|
|
31
|
+
│ ├── controllers/application_controller.rb # includes Multitenancy::Controller
|
|
32
|
+
│ ├── controllers/home_controller.rb
|
|
33
|
+
│ ├── views/layouts/application.html.erb
|
|
34
|
+
│ └── views/home/index.html.erb
|
|
35
|
+
├── config/
|
|
36
|
+
│ ├── routes.rb
|
|
37
|
+
│ └── locales/en.yml
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
See [generator.md](generator.md) for the full list of flags and what they add.
|
|
41
|
+
|
|
42
|
+
## Mount your themes
|
|
43
|
+
|
|
44
|
+
The gem does not mount themes for you — you decide how they reach the request path.
|
|
45
|
+
|
|
46
|
+
### Path-based
|
|
47
|
+
|
|
48
|
+
`config/routes.rb`:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Rails.application.routes.draw do
|
|
52
|
+
draw(:multitenancy)
|
|
53
|
+
root 'home#index'
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`config/routes/multitenancy.rb`:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
Multitenancy.themes.each do |theme|
|
|
61
|
+
mount theme.engine, at: "/#{theme.name}"
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`http://localhost:3000/storefront` → `Themes::Storefront::HomeController#index`.
|
|
66
|
+
|
|
67
|
+
### Subdomain-based
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# config/routes/multitenancy.rb
|
|
71
|
+
Multitenancy.themes.each do |theme|
|
|
72
|
+
constraints subdomain: theme.name do
|
|
73
|
+
mount theme.engine, at: '/'
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`http://storefront.lvh.me:3000/` → `Themes::Storefront::HomeController#index`.
|
|
79
|
+
|
|
80
|
+
## Verify
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bin/rails runner 'pp Multitenancy.themes.map(&:name)'
|
|
84
|
+
# => ["storefront"]
|
|
85
|
+
|
|
86
|
+
bin/rails server
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Visit the mount point — you should see the scaffolded `home/index` view.
|
|
90
|
+
|
|
91
|
+
## What you get per theme
|
|
92
|
+
|
|
93
|
+
Each theme is a fully isolated `Rails::Engine`:
|
|
94
|
+
|
|
95
|
+
- Namespaced under `Themes::<Name>` — classes don't collide with main app
|
|
96
|
+
- Its own view path (prepended to the lookup chain)
|
|
97
|
+
- Its own route namespace
|
|
98
|
+
- Its own `app/assets/` and `app/javascript/` (if importmap enabled)
|
|
99
|
+
- Its own locales (`config/locales/en.yml`)
|
|
100
|
+
- Its own RSpec / Minitest / FactoryBot directories (auto-discovered)
|
|
101
|
+
|
|
102
|
+
What you **don't** get automatically:
|
|
103
|
+
|
|
104
|
+
- Database isolation. Every theme talks to the same database. Scope via `tenant_id` columns or schemas yourself if you need that.
|
|
105
|
+
- Request-scoped tenant context. The theme is determined by the mount point, not by runtime lookup. If you need `Current.tenant`, add it as a `before_action`.
|
|
106
|
+
|
|
107
|
+
## Next steps
|
|
108
|
+
|
|
109
|
+
- [Themes](themes.md) — anatomy of a theme, namespace rules, view resolution quirks
|
|
110
|
+
- [Integrations](integrations.md) — ES modules, Stimulus, Tailwind v4, spec discovery
|
|
111
|
+
- [API reference](api.md)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Integrations
|
|
2
|
+
|
|
3
|
+
Every integration in this gem is optional and guard-checked: if the underlying gem isn't in your Gemfile, the integration is silently skipped.
|
|
4
|
+
|
|
5
|
+
## Importmap (`importmap-rails`)
|
|
6
|
+
|
|
7
|
+
### Per-theme importmaps
|
|
8
|
+
|
|
9
|
+
Each theme gets its own `Importmap::Map` stored on `theme.engine.importmap`. Theme pins don't leak into the main app, and vice versa.
|
|
10
|
+
|
|
11
|
+
The integration also removes theme paths from the main app's importmap config so they don't appear in the root importmap twice:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
app.config.importmap.paths.delete_if { |p| p.to_s.start_with?(themes_root) }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Development hot-reload
|
|
18
|
+
|
|
19
|
+
A `before_action` in `Multitenancy::Controller` calls `engine.importmap_reloader&.execute_if_updated` on each request. When you edit a file under `themes/<name>/app/javascript/`, the theme's importmap is re-drawn — no server restart needed.
|
|
20
|
+
|
|
21
|
+
### Drawing the importmap in a layout
|
|
22
|
+
|
|
23
|
+
```erb
|
|
24
|
+
<%# themes/storefront/app/views/layouts/application.html.erb %>
|
|
25
|
+
<%= javascript_importmap_tags "storefront/application" %>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Pinning packages
|
|
29
|
+
|
|
30
|
+
Pin inside the theme's `config/importmap.rb` (same DSL as the main app):
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# themes/storefront/config/importmap.rb
|
|
34
|
+
pin 'application', preload: true
|
|
35
|
+
pin '@hotwired/stimulus', to: '@hotwired--stimulus.js'
|
|
36
|
+
pin_all_from 'app/javascript/storefront/controllers', under: 'storefront/controllers'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tailwind CSS (`tailwindcss-rails`, v4 only)
|
|
40
|
+
|
|
41
|
+
### Compilation
|
|
42
|
+
|
|
43
|
+
Each theme has its own input file (`app/assets/tailwind/<name>/application.css`) compiled to its own output (`app/assets/builds/<name>/application.css`). Tailwind input directories are excluded from Propshaft so unprocessed CSS doesn't leak into the asset pipeline.
|
|
44
|
+
|
|
45
|
+
### Asset precompile hook
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
Rake::Task['assets:precompile'].enhance(['multitenancy:tailwindcss:build'])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This is wired automatically by the integration — no setup needed.
|
|
52
|
+
|
|
53
|
+
### Watch mode (dev)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bin/rails multitenancy:tailwindcss:watch
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Forks a watcher process per theme, traps `Ctrl-C` to clean up children.
|
|
60
|
+
|
|
61
|
+
See [rake-tasks.md](rake-tasks.md) for task details.
|
|
62
|
+
|
|
63
|
+
## RSpec (`rspec-rails`)
|
|
64
|
+
|
|
65
|
+
### Auto-discovery
|
|
66
|
+
|
|
67
|
+
Running `rspec` with no args discovers every theme's `spec/` directory:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bundle exec rspec
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Explicit paths
|
|
74
|
+
|
|
75
|
+
Passing an explicit directory expands it to include the matching theme's `spec/`:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bundle exec rspec themes/storefront
|
|
79
|
+
# Expands to: themes/storefront/spec
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Nested themes
|
|
83
|
+
|
|
84
|
+
If `storefront` and `storefront-admin` exist, `rspec themes/storefront` runs both. Disambiguate by going one level deeper:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bundle exec rspec themes/storefront-admin
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Minitest (`railties` test runner)
|
|
91
|
+
|
|
92
|
+
Mirrors the RSpec integration. Theme `test/` directories are auto-discovered:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
bin/rails test
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Explicit paths expand the same way:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
bin/rails test themes/storefront # runs themes/storefront/test
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## FactoryBot (`factory_bot_rails`)
|
|
105
|
+
|
|
106
|
+
Theme factories under `themes/<name>/spec/factories/` are added to FactoryBot's definition paths automatically. Build, create, and define as usual:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# themes/storefront/spec/factories/products.rb
|
|
110
|
+
FactoryBot.define do
|
|
111
|
+
factory :storefront_product, class: 'Themes::Storefront::Product' do
|
|
112
|
+
name { 'Widget' }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Writing a custom integration
|
|
118
|
+
|
|
119
|
+
Follow the pattern used by every built-in integration: a class under `Multitenancy::Integrations::<Name>` with a `.call(app)` method, guarded against optional-gem absence:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# lib/multitenancy/integrations/my_integration.rb
|
|
123
|
+
module Multitenancy::Integrations
|
|
124
|
+
class MyIntegration
|
|
125
|
+
def self.call(app)
|
|
126
|
+
return unless defined?(::SomeGem)
|
|
127
|
+
|
|
128
|
+
Multitenancy.themes.each do |theme|
|
|
129
|
+
# wire up per-theme stuff here
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Hook it into the Railtie in your application:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# config/initializers/multitenancy_custom.rb
|
|
140
|
+
ActiveSupport.on_load(:multitenancy) do
|
|
141
|
+
Multitenancy::Integrations::MyIntegration.call(Rails.application)
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `:multitenancy` load hook fires after every built-in integration has run, so you can safely inspect `Multitenancy.themes` at that point.
|
data/docs/rake-tasks.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Rake Tasks
|
|
2
|
+
|
|
3
|
+
The gem registers tasks under the `multitenancy:` namespace.
|
|
4
|
+
|
|
5
|
+
## `multitenancy:tailwindcss:build`
|
|
6
|
+
|
|
7
|
+
Compile Tailwind CSS for every theme.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bin/rails multitenancy:tailwindcss:build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Discovers `themes/<name>/app/assets/tailwind/<name>/application.css` inputs.
|
|
14
|
+
- Writes to `themes/<name>/app/assets/builds/<name>/application.css`.
|
|
15
|
+
- Skips themes without a Tailwind input file.
|
|
16
|
+
- Creates the builds directory if missing (so Propshaft picks up the output).
|
|
17
|
+
|
|
18
|
+
This task is wired into `assets:precompile`:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
Rake::Task['assets:precompile'].enhance(['multitenancy:tailwindcss:build'])
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
So production deploys compile theme CSS automatically — no additional config.
|
|
25
|
+
|
|
26
|
+
## `multitenancy:tailwindcss:watch`
|
|
27
|
+
|
|
28
|
+
Watch mode for development.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bin/rails multitenancy:tailwindcss:watch
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- Spawns one watcher process per theme.
|
|
35
|
+
- Recompiles on file changes under the theme's Tailwind directory.
|
|
36
|
+
- Traps `SIGINT` / `SIGTERM` to clean up all child PIDs on exit.
|
|
37
|
+
|
|
38
|
+
Runs in the foreground. Use it alongside `bin/dev` or a separate terminal.
|
|
39
|
+
|
|
40
|
+
## Prerequisites
|
|
41
|
+
|
|
42
|
+
Both tasks require `tailwindcss-rails ~> 4.4`. If the gem isn't in your bundle, the tasks are not registered (no error — they just don't exist).
|
|
43
|
+
|
|
44
|
+
## Troubleshooting
|
|
45
|
+
|
|
46
|
+
**"Task 'multitenancy:tailwindcss:build' not found"**
|
|
47
|
+
You're missing `tailwindcss-rails` in your Gemfile. Add it (`gem 'tailwindcss-rails', '~> 4.4'`), `bundle install`, restart.
|
|
48
|
+
|
|
49
|
+
**Build output not picked up by asset pipeline**
|
|
50
|
+
The integration excludes `themes/<name>/app/assets/tailwind/` from Propshaft (the raw inputs), but includes `themes/<name>/app/assets/builds/` (the outputs). If you're using Sprockets or Vite, you may need to adjust.
|
|
51
|
+
|
|
52
|
+
**Themes compile to the same path**
|
|
53
|
+
Each theme's output is under its own subdirectory (`builds/<theme-name>/application.css`), so collisions are impossible as long as theme directories have distinct names.
|
data/docs/themes.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Themes
|
|
2
|
+
|
|
3
|
+
A theme is a directory under `themes/` that gets bootstrapped into a `Rails::Engine` at boot.
|
|
4
|
+
|
|
5
|
+
## Anatomy
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
themes/storefront/
|
|
9
|
+
├── app/
|
|
10
|
+
│ ├── controllers/ # Themes::Storefront::XxxController
|
|
11
|
+
│ ├── models/ # Themes::Storefront::Xxx (optional; main app models are also reachable)
|
|
12
|
+
│ ├── views/ # Resolved first, ahead of main app views
|
|
13
|
+
│ ├── helpers/
|
|
14
|
+
│ ├── services/
|
|
15
|
+
│ ├── jobs/
|
|
16
|
+
│ ├── policies/
|
|
17
|
+
│ ├── assets/
|
|
18
|
+
│ ├── javascript/ # If importmap is enabled
|
|
19
|
+
│ └── ...
|
|
20
|
+
├── config/
|
|
21
|
+
│ ├── routes.rb # Engine-scoped routes
|
|
22
|
+
│ └── locales/
|
|
23
|
+
├── spec/ # Auto-discovered by RSpec (if present)
|
|
24
|
+
└── test/ # Auto-discovered by Minitest (if present)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Only directories that actually exist are registered. You don't need to create empty `app/mailers/` just for the gem to work.
|
|
28
|
+
|
|
29
|
+
The autoloaded paths by default are:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
app/controllers app/channels app/helpers app/services app/structs
|
|
33
|
+
app/models app/mailers app/presenters app/decorators app/queries
|
|
34
|
+
app/resources app/serializers app/transformers app/validators
|
|
35
|
+
app/workers app/jobs app/notifications app/policies lib
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Customize in an initializer:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# config/initializers/multitenancy.rb
|
|
42
|
+
Multitenancy.config.paths = %w[app/controllers app/models app/views lib]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Namespace rules
|
|
46
|
+
|
|
47
|
+
All Ruby code in a theme lives under `Themes::<Name>`:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# themes/storefront/app/controllers/home_controller.rb
|
|
51
|
+
module Themes::Storefront
|
|
52
|
+
class HomeController < ApplicationController
|
|
53
|
+
def index
|
|
54
|
+
@products = Product.all # main-app model — no namespacing needed
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The namespace module is created dynamically at boot. You can rely on it existing:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
Themes::Storefront # => Themes::Storefront
|
|
64
|
+
Themes::Storefront::Engine # => Themes::Storefront::Engine
|
|
65
|
+
Themes::Storefront::HomeController # => Themes::Storefront::HomeController
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Engine isolation
|
|
69
|
+
|
|
70
|
+
Each theme's engine runs through `isolate_namespace(Themes::<Name>)`. That means:
|
|
71
|
+
|
|
72
|
+
- Routes are scoped (`home_path` in the engine is NOT `Rails.application.routes.url_helpers.home_path`).
|
|
73
|
+
- Helpers don't leak between themes.
|
|
74
|
+
- Generators inside the theme generate into its namespace.
|
|
75
|
+
|
|
76
|
+
## Controllers
|
|
77
|
+
|
|
78
|
+
Theme `ApplicationController`s typically include the gem's `Controller` concern:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# themes/storefront/app/controllers/application_controller.rb
|
|
82
|
+
module Themes::Storefront
|
|
83
|
+
class ApplicationController < ::ApplicationController
|
|
84
|
+
include Multitenancy::Controller
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The generator does this for you. The concern sets:
|
|
90
|
+
|
|
91
|
+
- `prepend_view_path` — theme views win over main-app views with the same name.
|
|
92
|
+
- `layout "application"` — uses the theme's `layouts/application.html.erb`.
|
|
93
|
+
- A `before_action` that re-executes the theme's importmap reloader in development when JS files change.
|
|
94
|
+
|
|
95
|
+
### View resolution quirk
|
|
96
|
+
|
|
97
|
+
Without the concern, `Themes::Storefront::HomeController` would look up views under `themes/storefront/home/` — a path that doesn't exist, because views are at `home/`. The concern strips the theme namespace prefix from `_prefixes` so view lookup works naturally:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
theme module = Themes::Storefront
|
|
101
|
+
controller = Themes::Storefront::HomeController
|
|
102
|
+
lookup path = home/ (not themes/storefront/home/)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
That means `app/views/home/index.html.erb` inside the theme renders for the index action.
|
|
106
|
+
|
|
107
|
+
## Models
|
|
108
|
+
|
|
109
|
+
Shared across themes by default. You can add theme-specific models in `themes/<name>/app/models/`:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# themes/storefront/app/models/product.rb
|
|
113
|
+
module Themes::Storefront
|
|
114
|
+
class Product < ApplicationRecord
|
|
115
|
+
self.table_name = 'storefront_products'
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Inside the theme, `Product` means `Themes::Storefront::Product`. To reach the main app's model, use `::Product`.
|
|
121
|
+
|
|
122
|
+
## Database
|
|
123
|
+
|
|
124
|
+
All themes share the main app's database connection. If you want per-tenant data, handle it at the model level — a `tenant_id` column plus a default scope, or multiple DB configs in `database.yml` with `connects_to`. The gem doesn't touch this.
|
|
125
|
+
|
|
126
|
+
## Routes
|
|
127
|
+
|
|
128
|
+
Each theme's `config/routes.rb` is a standard engine routes file:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# themes/storefront/config/routes.rb
|
|
132
|
+
Themes::Storefront::Engine.routes.draw do
|
|
133
|
+
root to: 'home#index'
|
|
134
|
+
resources :products
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The main app's `config/routes/multitenancy.rb` decides how each engine gets mounted — see [getting-started.md](getting-started.md).
|
|
139
|
+
|
|
140
|
+
## Assets & JavaScript
|
|
141
|
+
|
|
142
|
+
- CSS: each theme has its own `app/assets/` paths registered with Propshaft.
|
|
143
|
+
- JS (importmap): each theme has its own `Importmap::Map` with its own pins, separate from the main app. The `javascript_importmap_tags` helper in the theme's layout draws the theme's importmap.
|
|
144
|
+
- Tailwind: each theme has its own input file and gets its own build output under `app/assets/builds/<theme>/`.
|
|
145
|
+
|
|
146
|
+
See [integrations.md](integrations.md) for the asset and JS story in detail.
|
|
147
|
+
|
|
148
|
+
## Locales
|
|
149
|
+
|
|
150
|
+
`themes/<name>/config/locales/*.yml` is added to `I18n.load_path`. Keys aren't automatically namespaced — if two themes define the same key, the last loaded wins. Scope keys manually if you want them isolated:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
# themes/storefront/config/locales/en.yml
|
|
154
|
+
en:
|
|
155
|
+
themes:
|
|
156
|
+
storefront:
|
|
157
|
+
home:
|
|
158
|
+
title: Welcome to Storefront
|
|
159
|
+
```
|