plutonium 0.37.0 โ†’ 0.38.0

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-controller/SKILL.md +25 -2
  3. data/.claude/skills/plutonium-definition-fields/SKILL.md +33 -0
  4. data/.claude/skills/plutonium-nested-resources/SKILL.md +79 -19
  5. data/.claude/skills/plutonium-policy/SKILL.md +93 -6
  6. data/CHANGELOG.md +36 -0
  7. data/CLAUDE.md +8 -10
  8. data/CONTRIBUTING.md +6 -8
  9. data/Rakefile +16 -1
  10. data/app/assets/plutonium.css +1 -1
  11. data/app/assets/plutonium.js +9371 -11492
  12. data/app/assets/plutonium.js.map +4 -4
  13. data/app/assets/plutonium.min.js +55 -55
  14. data/app/assets/plutonium.min.js.map +4 -4
  15. data/docs/guides/index.md +5 -0
  16. data/docs/guides/nested-resources.md +132 -29
  17. data/docs/guides/troubleshooting.md +82 -0
  18. data/docs/reference/controller/index.md +1 -1
  19. data/docs/reference/definition/fields.md +33 -0
  20. data/docs/reference/model/index.md +1 -1
  21. data/docs/reference/policy/index.md +77 -6
  22. data/gemfiles/rails_7.gemfile.lock +3 -3
  23. data/gemfiles/rails_8.0.gemfile.lock +3 -3
  24. data/gemfiles/rails_8.1.gemfile.lock +3 -3
  25. data/lib/plutonium/core/controller.rb +144 -19
  26. data/lib/plutonium/core/controllers/association_resolver.rb +86 -0
  27. data/lib/plutonium/helpers/display_helper.rb +12 -0
  28. data/lib/plutonium/query/filters/association.rb +25 -3
  29. data/lib/plutonium/resource/controller.rb +90 -9
  30. data/lib/plutonium/resource/controllers/authorizable.rb +17 -4
  31. data/lib/plutonium/resource/controllers/crud_actions.rb +7 -5
  32. data/lib/plutonium/resource/controllers/interactive_actions.rb +9 -0
  33. data/lib/plutonium/resource/controllers/presentable.rb +13 -11
  34. data/lib/plutonium/resource/policy.rb +85 -2
  35. data/lib/plutonium/resource/record/routes.rb +31 -1
  36. data/lib/plutonium/routing/mapper_extensions.rb +40 -4
  37. data/lib/plutonium/routing/route_set_extensions.rb +3 -0
  38. data/lib/plutonium/ui/breadcrumbs.rb +1 -1
  39. data/lib/plutonium/ui/display/resource.rb +5 -2
  40. data/lib/plutonium/ui/form/components/key_value_store.rb +17 -5
  41. data/lib/plutonium/ui/page/index.rb +1 -1
  42. data/lib/plutonium/version.rb +1 -1
  43. data/lib/tasks/release.rake +1 -1
  44. data/package.json +6 -5
  45. data/plutonium.gemspec +1 -1
  46. data/src/js/controllers/key_value_store_controller.js +6 -0
  47. data/src/js/controllers/resource_drop_down_controller.js +3 -3
  48. data/yarn.lock +1465 -693
  49. metadata +6 -5
  50. data/app/javascript/controllers/key_value_store_controller.js +0 -119
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 117694b8e7d907c2c28a78439aaf85d32c8a682fe220777a51152cef561b6a9d
4
- data.tar.gz: f44605f40590d45730bf734f3cd9868d233c60dd9a25269f33e56957f7c7898a
3
+ metadata.gz: 1a3ade68b80487615f8ad46432edb306cde755d712e7222b11705427f9a283ca
4
+ data.tar.gz: 10e95e8c085c8b7229929f50517fce9d964a1ff2ca31b0ee20662ca863c88f47
5
5
  SHA512:
6
- metadata.gz: bf00b14c0db29e36d7c85ef6069ff1a7eea4fa2a1db6c4acd3d1cca841852ead636f81dcd0ae147b4dd985958001d7ee906c60abd0e02428758e315e9383726d
7
- data.tar.gz: e8d74bf2b18faf507d3b4f9b46c586a4bcc6fe02048b9bbc5abbdf173fee3017dbe8bde1ab6176f65347fea98caef0f662fde838f05cdf4fe244e8ad37efd8aa
6
+ metadata.gz: e4a5ac3024c168686c6c315d2d7fa280c708a7e807580a422cb20223b6ccf2f95e96876e6a56d9ca88ee8fcbb63cc906dba33ad02af73a62f167bd91431a1a08
7
+ data.tar.gz: a0644bf92081d308e0ef306d38168d63138864c54534fea161c20b9f2d79e8556914239dd3122854686e0fee140758f9aa49b2b9c72248a48868045405fffbd5
@@ -194,22 +194,45 @@ build_collection # Build table component
194
194
  resource_url_for(@post) # URL for record
195
195
  resource_url_for(@post, action: :edit) # Edit URL
196
196
  resource_url_for(Post) # Index URL
197
+
198
+ # With parent (nested resources)
199
+ resource_url_for(@comment, parent: @post) # Nested URL
200
+ resource_url_for(Comment, action: :new, parent: @post)
201
+
202
+ # Cross-package URLs
203
+ resource_url_for(@post, package: AdminPortal)
197
204
  ```
198
205
 
199
206
  ## Nested Resources
200
207
 
201
- Parent records are automatically resolved:
208
+ Parent records are automatically resolved from routes with the `nested_` prefix:
202
209
 
203
210
  ```ruby
204
- # Route: /users/:user_id/posts/:id
211
+ # Route: /users/:user_id/nested_posts/:id
205
212
  class PostsController < ::ResourceController
206
213
  # current_parent returns the User
214
+ # current_nested_association returns :posts
207
215
  # resource_record! returns the Post scoped to that User
208
216
  end
209
217
  ```
210
218
 
219
+ ### Key Methods for Nested Resources
220
+
221
+ ```ruby
222
+ current_parent # Parent record (e.g., User instance)
223
+ current_nested_association # Association name (e.g., :posts)
224
+ parent_route_param # URL param (e.g., :user_id)
225
+ parent_input_param # Form param (e.g., :user)
226
+ ```
227
+
211
228
  Parent fields are automatically excluded from forms/displays. Override with presentation hooks (see above).
212
229
 
230
+ ### has_one Support
231
+
232
+ For `has_one` associations, routes are singular:
233
+ - `/users/:user_id/nested_profile` (no `:id` param)
234
+ - Index redirects to show (or new if no record exists)
235
+
213
236
  ## Entity Scoping (Multi-tenancy)
214
237
 
215
238
  When a portal is scoped to an entity:
@@ -321,6 +321,39 @@ column :status, align: :center # Center
321
321
  column :amount, align: :end # Right
322
322
  ```
323
323
 
324
+ ### Value Formatting
325
+
326
+ Use `formatter` for simple value transformations without a full block:
327
+
328
+ ```ruby
329
+ # Truncate long text
330
+ column :description, formatter: ->(value) { value&.truncate(30) }
331
+
332
+ # Format numbers
333
+ column :price, formatter: ->(value) { "$%.2f" % value if value }
334
+
335
+ # Transform values
336
+ column :status, formatter: ->(value) { value&.humanize&.upcase }
337
+ ```
338
+
339
+ The `formatter` option:
340
+ - Receives the field value as its argument
341
+ - Returns the transformed value for display
342
+ - Works with `column` and `display` declarations
343
+ - Is simpler than block syntax when you only need to transform the value
344
+
345
+ **formatter vs block:** Use `formatter` when you only need the value. Use a block when you need access to the full record:
346
+
347
+ ```ruby
348
+ # formatter - receives just the value
349
+ column :name, formatter: ->(value) { value&.titleize }
350
+
351
+ # block - receives the full record
352
+ column :full_name do |record|
353
+ "#{record.first_name} #{record.last_name}"
354
+ end
355
+ ```
356
+
324
357
  ### Custom Column Rendering
325
358
 
326
359
  Use a block to customize how a column value is displayed. The block receives the raw record:
@@ -5,7 +5,7 @@ description: Plutonium nested resources - parent/child routes, scoping, and URL
5
5
 
6
6
  # Nested Resources
7
7
 
8
- Plutonium automatically creates nested routes for `has_many` associations, scopes queries to the parent, and handles URL generation.
8
+ Plutonium automatically creates nested routes for `has_many` and `has_one` associations, scopes queries to the parent, and handles URL generation.
9
9
 
10
10
  ## How It Works
11
11
 
@@ -15,12 +15,17 @@ When you register resources with parent-child relationships:
15
15
  # In portal routes
16
16
  register_resource ::Company
17
17
  register_resource ::Property # has belongs_to :company
18
+ register_resource ::CompanyProfile # has belongs_to :company (has_one on Company)
18
19
  ```
19
20
 
20
- Plutonium automatically creates nested routes:
21
- - `/companies/:company_id/properties` - Properties scoped to company
22
- - `/companies/:company_id/properties/new` - New property for company
23
- - `/companies/:company_id/properties/:id` - Property in company context
21
+ Plutonium automatically creates nested routes with a `nested_` prefix:
22
+ - `/companies/:company_id/nested_properties` - Properties scoped to company (has_many)
23
+ - `/companies/:company_id/nested_properties/new` - New property for company
24
+ - `/companies/:company_id/nested_properties/:id` - Property in company context
25
+ - `/companies/:company_id/nested_company_profile` - Singular profile (has_one)
26
+ - `/companies/:company_id/nested_company_profile/new` - New profile for company
27
+
28
+ The `nested_` prefix prevents route conflicts when the same resource is registered both as a top-level and nested resource.
24
29
 
25
30
  ## Automatic Behavior
26
31
 
@@ -81,26 +86,45 @@ end
81
86
 
82
87
  ## Query Scoping
83
88
 
84
- Collections are automatically scoped to the parent via policies:
89
+ Collections are automatically scoped to the parent via policies. The policy receives `parent` and `parent_association` context:
85
90
 
86
91
  ```ruby
87
92
  class PropertyPolicy < ResourcePolicy
93
+ # parent: the parent record (e.g., Company instance)
94
+ # parent_association: the association name (e.g., :properties)
95
+
88
96
  relation_scope do |relation|
89
- relation = super(relation) # Applies associated_with(entity_scope)
90
- # entity_scope is the current_parent
97
+ relation = super(relation) # Applies parent scoping automatically
91
98
  relation
92
99
  end
93
100
  end
94
101
  ```
95
102
 
96
- The `associated_with` scope finds records belonging to the parent:
103
+ ### How Parent Scoping Works
97
104
 
105
+ For **has_many** associations, scoping uses the association directly:
98
106
  ```ruby
99
- # Automatic detection via belongs_to
100
- Property.associated_with(company)
101
- # => Property.where(company: company)
107
+ # parent.properties => Company#properties
108
+ parent.send(parent_association)
102
109
  ```
103
110
 
111
+ For **has_one** associations, scoping uses a where clause:
112
+ ```ruby
113
+ # Property.where(company_id: company.id) with limit
114
+ relation.where(foreign_key => parent.id)
115
+ ```
116
+
117
+ ### Parent vs Entity Scope
118
+
119
+ When a parent is present, parent scoping takes precedence over entity scoping:
120
+
121
+ ```ruby
122
+ # With parent: scopes via parent association
123
+ # Without parent: falls back to entity_scope (multi-tenancy)
124
+ ```
125
+
126
+ This prevents double-scoping - the parent was already authorized and entity-scoped during its own authorization.
127
+
104
128
  ### Custom Association Scope
105
129
 
106
130
  For complex relationships, define a custom scope:
@@ -118,21 +142,37 @@ end
118
142
  Use `resource_url_for` with the `parent:` option:
119
143
 
120
144
  ```ruby
121
- # Child collection
145
+ # Child collection (has_many)
122
146
  resource_url_for(Property, parent: company)
123
- # => /companies/123/properties
147
+ # => /companies/123/nested_properties
124
148
 
125
149
  # Child record
126
150
  resource_url_for(property, parent: company)
127
- # => /companies/123/properties/456
151
+ # => /companies/123/nested_properties/456
128
152
 
129
153
  # New child form
130
154
  resource_url_for(Property, action: :new, parent: company)
131
- # => /companies/123/properties/new
155
+ # => /companies/123/nested_properties/new
132
156
 
133
157
  # Edit child
134
158
  resource_url_for(property, action: :edit, parent: company)
135
- # => /companies/123/properties/456/edit
159
+ # => /companies/123/nested_properties/456/edit
160
+
161
+ # Singular resource (has_one)
162
+ resource_url_for(company_profile, parent: company)
163
+ # => /companies/123/nested_company_profile
164
+
165
+ resource_url_for(CompanyProfile, action: :new, parent: company)
166
+ # => /companies/123/nested_company_profile/new
167
+ ```
168
+
169
+ ### Cross-Package URL Generation
170
+
171
+ Generate URLs for resources in a different package:
172
+
173
+ ```ruby
174
+ # From AdminPortal, generate URL to CustomerPortal resource
175
+ resource_url_for(property, parent: company, package: CustomerPortal)
136
176
  ```
137
177
 
138
178
  ## Association Panels
@@ -192,12 +232,32 @@ resource_params
192
232
 
193
233
  You don't need to include hidden fields for the parent in forms.
194
234
 
235
+ ## has_one Associations
236
+
237
+ Plutonium supports both `has_many` and `has_one` associations:
238
+
239
+ ```ruby
240
+ class Company < ResourceRecord
241
+ has_many :properties # Plural routes
242
+ has_one :company_profile # Singular routes
243
+ end
244
+ ```
245
+
246
+ Routes generated:
247
+ - `has_many`: `/companies/:id/nested_properties` (plural, with `:id` param)
248
+ - `has_one`: `/companies/:id/nested_company_profile` (singular, no `:id` param)
249
+
250
+ For has_one associations:
251
+ - Index redirects to show (or new if no record exists)
252
+ - Only one record can exist per parent
253
+ - Forms don't show parent field (determined by URL)
254
+
195
255
  ## Nesting Limitations
196
256
 
197
257
  Plutonium supports **one level of nesting**:
198
258
 
199
- - โœ… `/companies/:company_id/properties` (parent โ†’ child)
200
- - โŒ `/companies/:company_id/properties/:property_id/units` (grandparent โ†’ parent โ†’ child)
259
+ - โœ… `/companies/:company_id/nested_properties` (parent โ†’ child)
260
+ - โŒ `/companies/:company_id/nested_properties/:property_id/nested_units` (grandparent โ†’ parent โ†’ child)
201
261
 
202
262
  ## Common Patterns
203
263
 
@@ -182,13 +182,13 @@ relation_scope do |relation|
182
182
  end
183
183
  ```
184
184
 
185
- ### With Entity Scoping
185
+ ### With Parent Scoping (Nested Resources)
186
186
 
187
- Call `super` to preserve automatic entity scoping:
187
+ For nested resources, call `super` to apply automatic parent scoping:
188
188
 
189
189
  ```ruby
190
190
  relation_scope do |relation|
191
- relation = super(relation) # Apply entity scope first
191
+ relation = super(relation) # Applies parent scoping automatically
192
192
 
193
193
  if user.admin?
194
194
  relation
@@ -198,6 +198,71 @@ relation_scope do |relation|
198
198
  end
199
199
  ```
200
200
 
201
+ **Parent scoping takes precedence over entity scoping.** When a parent is present:
202
+ - For `has_many`: scopes via `parent.association_name`
203
+ - For `has_one`: scopes via `where(foreign_key: parent.id)`
204
+
205
+ ### With Entity Scoping (Multi-tenancy)
206
+
207
+ When no parent is present, `super` applies entity scoping:
208
+
209
+ ```ruby
210
+ relation_scope do |relation|
211
+ relation = super(relation) # Apply entity scope if no parent
212
+
213
+ if user.admin?
214
+ relation
215
+ else
216
+ relation.where(published: true)
217
+ end
218
+ end
219
+ ```
220
+
221
+ ### default_relation_scope is Required
222
+
223
+ Plutonium verifies that `default_relation_scope` is called in every `relation_scope`. This prevents accidental multi-tenancy leaks when overriding scopes.
224
+
225
+ ```ruby
226
+ # โŒ This will raise an error
227
+ relation_scope do |relation|
228
+ relation.where(published: true) # Missing default_relation_scope!
229
+ end
230
+
231
+ # โœ… Correct - call default_relation_scope
232
+ relation_scope do |relation|
233
+ default_relation_scope(relation).where(published: true)
234
+ end
235
+
236
+ # โœ… Also correct - super calls default_relation_scope
237
+ relation_scope do |relation|
238
+ super(relation).where(published: true)
239
+ end
240
+ ```
241
+
242
+ When overriding an inherited scope:
243
+
244
+ ```ruby
245
+ class AdminPostPolicy < PostPolicy
246
+ relation_scope do |relation|
247
+ # Replace inherited scope but keep Plutonium's parent/entity scoping
248
+ default_relation_scope(relation)
249
+ end
250
+ end
251
+ ```
252
+
253
+ ### Skipping Default Scoping (Rare)
254
+
255
+ If you intentionally need to skip scoping, call `skip_default_relation_scope!`:
256
+
257
+ ```ruby
258
+ relation_scope do |relation|
259
+ skip_default_relation_scope!
260
+ relation # No parent/entity scoping applied
261
+ end
262
+ ```
263
+
264
+ Consider using a separate portal instead of skipping scoping.
265
+
201
266
  ## Portal-Specific Policies
202
267
 
203
268
  Override policies per portal:
@@ -306,9 +371,31 @@ end
306
371
  Policies have access to:
307
372
 
308
373
  ```ruby
309
- user # Current user (required)
310
- record # The resource being authorized
311
- entity_scope # Current scoped entity (for multi-tenancy)
374
+ user # Current user (required)
375
+ record # The resource being authorized
376
+ entity_scope # Current scoped entity (for multi-tenancy)
377
+ parent # Parent record for nested resources (nil if not nested)
378
+ parent_association # Association name on parent (e.g., :comments)
379
+ ```
380
+
381
+ ### Nested Resource Context
382
+
383
+ For nested resources (e.g., `/posts/123/nested_comments`), the policy receives:
384
+
385
+ ```ruby
386
+ class CommentPolicy < ResourcePolicy
387
+ def create?
388
+ # parent is the Post instance
389
+ # parent_association is :comments
390
+ parent.present? && user.can_comment_on?(parent)
391
+ end
392
+
393
+ relation_scope do |relation|
394
+ # super() uses parent and parent_association for scoping
395
+ relation = super(relation)
396
+ relation
397
+ end
398
+ end
312
399
  ```
313
400
 
314
401
  ### Custom Context
data/CHANGELOG.md CHANGED
@@ -1,3 +1,39 @@
1
+ ## [0.38.0] - 2026-01-25
2
+
3
+ ### ๐Ÿš€ Features
4
+
5
+ - *(core)* Handle ActionPolicy::Unauthorized for non-HTML formats
6
+ - *(interactive_actions)* Add non-HTML response handlers for successful actions
7
+ - *(routing)* Add has_one nested resource support
8
+ - *(routing)* [**breaking**] Refactor nested resource URL generation with named route helpers
9
+ - *(policy)* Add default_relation_scope method with verification
10
+ - *(nested)* Use association names for nested resource titles and breadcrumbs
11
+
12
+ ### ๐Ÿ› Bug Fixes
13
+
14
+ - Remove duplicate kv store controller and move improvements into original
15
+ - *(filters)* Improve association filter class resolution
16
+ - *(ui)* Improve dropdown positioning with viewport boundary
17
+ - *(crud)* Use correct action attributes for form re-rendering on errors
18
+ - *(form)* Distinguish empty vs not-submitted key-value store fields
19
+ - *(controller)* Use existing record context for form param extraction
20
+
21
+ ### ๐Ÿ“š Documentation
22
+
23
+ - *(definition)* Document formatter option for columns and displays
24
+ - Cleanup review definition
25
+ - Add troubleshooting guide for inflection issue
26
+ - Update nested resource routes to use nested_ prefix
27
+
28
+ ### ๐Ÿงช Testing
29
+
30
+ - Refactor tests to use real module implementations instead of mocks
31
+
32
+ ### โš™๏ธ Miscellaneous Tasks
33
+
34
+ - Use chokidar to fix dev build cyclic dependency issues
35
+ - Warn when running tests without Appraisal
36
+ - Switch to yarn
1
37
  ## [0.37.0] - 2026-01-21
2
38
 
3
39
  ### ๐Ÿš€ Features
data/CLAUDE.md CHANGED
@@ -77,16 +77,16 @@ When working on JavaScript or CSS in `src/`:
77
77
 
78
78
  ```bash
79
79
  # Watch mode (rebuilds on changes to src/build/)
80
- npm run dev
80
+ yarn dev
81
81
 
82
82
  # Production build (to app/assets/)
83
- npm run build
83
+ yarn build
84
84
  ```
85
85
 
86
- - `npm run dev` - watches and rebuilds to `src/build/` for development
87
- - `npm run build` - compiles to `app/assets/` for release
86
+ - `yarn dev` - watches and rebuilds to `src/build/` for development
87
+ - `yarn build` - compiles to `app/assets/` for release
88
88
 
89
- **Always run `npm run dev`** in a terminal when working on frontend code.
89
+ **Always run `yarn dev`** in a terminal when working on frontend code.
90
90
 
91
91
  ### Running Tests
92
92
 
@@ -114,10 +114,8 @@ Generators are in `lib/generators/pu/`. Test by:
114
114
  ### Documentation
115
115
 
116
116
  ```bash
117
- cd docs
118
- pnpm install
119
- pnpm dev # Local preview at localhost:5173
120
- pnpm build # Build for production
117
+ yarn docs:dev # Local preview at localhost:5173
118
+ yarn docs:build # Build for production
121
119
  ```
122
120
 
123
121
  ## Code Conventions
@@ -212,4 +210,4 @@ This:
212
210
  ### Update documentation
213
211
  1. Edit files in `docs/`
214
212
  2. If user-facing behavior, update relevant skill in `.claude/skills/`
215
- 3. Run `pnpm build` to verify no broken links
213
+ 3. Run `yarn docs:build` to verify no broken links
data/CONTRIBUTING.md CHANGED
@@ -104,7 +104,7 @@ git push origin main --tags
104
104
 
105
105
  ```bash
106
106
  bundle install
107
- npm install
107
+ yarn install
108
108
  ```
109
109
 
110
110
  ### Environment
@@ -123,10 +123,10 @@ Frontend source is in `src/`. When making JS or CSS changes:
123
123
 
124
124
  ```bash
125
125
  # Watch mode - keeps rebuilding as you edit
126
- npm run dev
126
+ yarn dev
127
127
 
128
128
  # Production build - run before committing
129
- npm run build
129
+ yarn build
130
130
  ```
131
131
 
132
132
  ### Running Tests
@@ -160,10 +160,8 @@ bin/dev
160
160
  ### Documentation
161
161
 
162
162
  ```bash
163
- cd docs
164
- pnpm install
165
- pnpm dev # Preview at localhost:5173
166
- pnpm build # Check for errors
163
+ yarn docs:dev # Preview at localhost:5173
164
+ yarn docs:build # Check for errors
167
165
  ```
168
166
 
169
167
  ### Changelog Generation (optional)
@@ -180,7 +178,7 @@ cargo install git-cliff # via Rust
180
178
  2. Create a feature branch: `git checkout -b feat/my-feature`
181
179
  3. Make your changes with conventional commits
182
180
  4. Run tests: `bundle exec appraisal rake test`
183
- 5. Build assets: `npm run build`
181
+ 5. Build assets: `yarn build`
184
182
  6. Push and create a pull request
185
183
 
186
184
  ## Questions?
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
8
8
  task default: %i[test standard]
9
9
 
10
10
  task :assets do
11
- `npm run build`
11
+ `yarn build`
12
12
  end
13
13
 
14
14
  # https://stackoverflow.com/questions/15707940/rake-before-task-hook
@@ -20,3 +20,18 @@ Rake::TestTask.new do |t|
20
20
  t.test_files = FileList["test/**/*_test.rb"]
21
21
  t.verbose = true
22
22
  end
23
+
24
+ # Warn users to run tests through Appraisal
25
+ Rake::Task["test"].enhance do
26
+ # This runs after test completes successfully - no action needed
27
+ end
28
+
29
+ task :check_appraisal do
30
+ unless ENV["BUNDLE_GEMFILE"]&.include?("gemfiles/")
31
+ warn "\nโš ๏ธ Tests should be run through Appraisal for the correct gem environment:"
32
+ warn " bundle exec appraisal rails-8.1 rake test"
33
+ warn " bundle exec appraisal rake test # runs all Rails versions\n\n"
34
+ end
35
+ end
36
+
37
+ Rake::Task["test"].enhance [:check_appraisal]