daisyui 2.0.2 → 2.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -1
- data/README.md +39 -0
- data/lib/daisy_ui/base.rb +45 -9
- data/lib/daisy_ui/class_merge/configuration.rb +18 -3
- data/lib/daisy_ui/class_merge/daisy_groups.rb +8 -5
- data/lib/daisy_ui/class_merge/tailwind_config.rb +8 -18
- data/lib/daisy_ui/class_merge/validators.rb +4 -0
- data/lib/daisy_ui/class_merge.rb +23 -12
- data/lib/daisy_ui/configurable/report_log.rb +36 -0
- data/lib/daisy_ui/configurable.rb +81 -16
- data/lib/daisy_ui/engine.rb +7 -0
- data/lib/daisy_ui/mcp_server.rb +1 -0
- data/lib/daisy_ui/shareable.rb +56 -0
- data/lib/daisy_ui/store.rb +71 -0
- data/lib/daisy_ui/updated_at.rb +1 -1
- data/lib/daisy_ui/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41fa98f82c0377c4615fa5e4a1091ae8383688a951acb726122866cefa273665
|
|
4
|
+
data.tar.gz: 651713886837f46040942fc397089d1194dc35787ab1ba18308db97f12d94a06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be24df2be52d01adbda5f9e257650d788352a0dfd286bee5c734dfce66aee0377dd0ad601269363ffd734d908cd0c2cde612062017dc59fde61086f4f53e716c
|
|
7
|
+
data.tar.gz: 2320af028c2539b849de1feb7c1d2690cc6ca0e75da24bd7d838415171c4d922f59b26f5833e1cc1a1e035c330ce532eaae75e21107bd89119f24653f7ab3eb4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [2.1.0]
|
|
2
|
+
|
|
3
|
+
### DaisyUI finalizes itself at boot: class merging in any Ractor (#58)
|
|
4
|
+
|
|
5
|
+
At boot DaisyUI now freezes its configuration and every component's
|
|
6
|
+
registries, so a non-main Ractor can read them. `DaisyUI::ClassMerge.merge`
|
|
7
|
+
then works in any Ractor; each Ractor builds its own merger and LRU.
|
|
8
|
+
|
|
9
|
+
- Automatic: `DaisyUI.configure` finalizes at its end, and the Rails engine
|
|
10
|
+
finalizes again in `after_initialize`, with no app code. A Rails-less app
|
|
11
|
+
that never calls `configure` calls `DaisyUI.finalize!` at the end of boot.
|
|
12
|
+
- **The configuration is frozen after `DaisyUI.configure` (and, in a Rails
|
|
13
|
+
app, after boot).** Writing to `DaisyUI.configuration` directly now raises
|
|
14
|
+
`FrozenError`, e.g. `DaisyUI.configuration.unknown_modifiers = :ignore` in a
|
|
15
|
+
spec: use `DaisyUI.configure { |config| config.unknown_modifiers = :ignore }`.
|
|
16
|
+
`DaisyUI.configure` can be called again at any time: it yields a writable
|
|
17
|
+
copy and publishes it once that is finalized, so no reader ever meets a
|
|
18
|
+
half-changed configuration, and a block that raises changes nothing. A
|
|
19
|
+
Ractor that already built its merger rebuilds it from the new
|
|
20
|
+
configuration.
|
|
21
|
+
- Finalizing never raises. A configured value that cannot be made shareable
|
|
22
|
+
is kept as it is, unfrozen, and the configuration is then readable on the
|
|
23
|
+
main Ractor only: a callable `unknown_modifiers` or a raw `class_merge`
|
|
24
|
+
validator that closes over app state (such as
|
|
25
|
+
`->(error) { Appsignal.report_error(error) }` in an initializer), or an
|
|
26
|
+
object that holds a logger.
|
|
27
|
+
- Component registries (`register_modifiers`, `register_flags`,
|
|
28
|
+
`self.component_class =`) are frozen as they are written, so a component
|
|
29
|
+
that loads after boot (lazy loading in development) is readable from any
|
|
30
|
+
Ractor too. Writers now replace them instead of mutating them, and
|
|
31
|
+
mutating one in place raises `FrozenError`.
|
|
32
|
+
- Thread safety: the Tailwind class groups behind `ClassMerge` were built
|
|
33
|
+
with an unguarded `||=` (8 threads built them 8 times); they are now built
|
|
34
|
+
once per Ractor.
|
|
35
|
+
- Rendering components inside a Ractor waits on Phlex
|
|
36
|
+
([yippee-fun/phlex#1055](https://github.com/yippee-fun/phlex/pull/1055)).
|
|
37
|
+
|
|
38
|
+
## [2.0.2] and earlier
|
|
2
39
|
|
|
3
40
|
### Unknown modifiers are reported (#54)
|
|
4
41
|
|
data/README.md
CHANGED
|
@@ -209,6 +209,45 @@ end
|
|
|
209
209
|
Keyword arguments are not validated: `Toggle(primray: true)` renders a
|
|
210
210
|
`primray` attribute, because unknown keywords are HTML attributes.
|
|
211
211
|
|
|
212
|
+
## Threads, fibers and Ractors
|
|
213
|
+
|
|
214
|
+
Components and `DaisyUI::ClassMerge` are safe to share between threads (Puma)
|
|
215
|
+
and fibers (Falcon). For Ractors, DaisyUI finalizes itself at boot: it freezes
|
|
216
|
+
the configuration and every component's registries, so
|
|
217
|
+
`DaisyUI::ClassMerge.merge` runs in any Ractor (as long as the configuration
|
|
218
|
+
is shareable, see below), each Ractor with a merger of its own.
|
|
219
|
+
`DaisyUI.configure` finalizes at its end and the Rails engine
|
|
220
|
+
finalizes again once the app has booted, so there is nothing to call. A
|
|
221
|
+
Rails-less app that never calls `configure` calls `DaisyUI.finalize!` at the
|
|
222
|
+
end of boot.
|
|
223
|
+
|
|
224
|
+
After that the configuration is frozen: writing to `DaisyUI.configuration`
|
|
225
|
+
directly raises `FrozenError`. `DaisyUI.configure` keeps working at any time:
|
|
226
|
+
it yields a writable copy and publishes it once that is finalized, and a
|
|
227
|
+
Ractor that already built its merger rebuilds it from the new configuration.
|
|
228
|
+
A test that changes a setting goes through it too:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
around do |example|
|
|
232
|
+
DaisyUI.configure { |config| config.unknown_modifiers = :ignore }
|
|
233
|
+
example.run
|
|
234
|
+
ensure
|
|
235
|
+
DaisyUI.configure { |config| config.unknown_modifiers = :raise }
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Finalizing never raises. A configured value that cannot be made shareable is
|
|
240
|
+
kept as it is, and the configuration is then readable on the main Ractor only:
|
|
241
|
+
a callable `unknown_modifiers` or a raw `class_merge` validator that closes
|
|
242
|
+
over app state (`->(error) { Appsignal.report_error(error) }` in an
|
|
243
|
+
initializer), or an object that holds a logger. A lambda is shareable when it
|
|
244
|
+
captures no local variables and is defined inside a module, or made with
|
|
245
|
+
`Ractor.shareable_lambda` on Ruby 4.0. A component that loads after boot (lazy
|
|
246
|
+
loading in development) freezes its registries as it writes them, so any
|
|
247
|
+
Ractor can read it; a Ractor that already built its merger picks up its groups
|
|
248
|
+
after the next `configure`. Rendering components inside a Ractor waits on Phlex
|
|
249
|
+
([yippee-fun/phlex#1055](https://github.com/yippee-fun/phlex/pull/1055)).
|
|
250
|
+
|
|
212
251
|
# Compatibility Notes
|
|
213
252
|
|
|
214
253
|
## @tailwindcss/forms plugin
|
data/lib/daisy_ui/base.rb
CHANGED
|
@@ -118,14 +118,37 @@ module DaisyUI
|
|
|
118
118
|
error: "bg-error text-error-content"
|
|
119
119
|
}.freeze
|
|
120
120
|
|
|
121
|
+
# Component registries: written by the class body (register_modifiers,
|
|
122
|
+
# register_flags, `self.component_class =`) and by `inherited`, on the main
|
|
123
|
+
# Ractor while the class loads; rendering only reads them. Every writer
|
|
124
|
+
# assigns a new value instead of mutating the old one and freezes it as it
|
|
125
|
+
# writes (Shareable.share), so a non-main Ractor can read the registries of
|
|
126
|
+
# a component that loaded after boot too, and a later register_* still
|
|
127
|
+
# works.
|
|
121
128
|
class << self
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable -- see above
|
|
130
|
+
attr_reader :modifiers
|
|
131
|
+
|
|
132
|
+
def modifiers=(modifiers)
|
|
133
|
+
@modifiers = Shareable.share(modifiers)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def modifier_groups=(groups)
|
|
137
|
+
@modifier_groups = Shareable.share(groups)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def flags=(flags)
|
|
141
|
+
@flags = Shareable.share(flags)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def component_class=(component_class)
|
|
145
|
+
@component_class = Shareable.share(component_class)
|
|
146
|
+
end
|
|
124
147
|
|
|
125
148
|
# Positional symbols a component handles itself (e.g. Dropdown's
|
|
126
149
|
# :popover): accepted as modifiers, but they emit no class.
|
|
127
150
|
def flags
|
|
128
|
-
@flags
|
|
151
|
+
@flags || []
|
|
129
152
|
end
|
|
130
153
|
|
|
131
154
|
def register_flags(*names)
|
|
@@ -135,20 +158,24 @@ module DaisyUI
|
|
|
135
158
|
# { group => [modifier, ...] }: modifiers in one group are alternatives
|
|
136
159
|
# (sizes, colors, ...), so ClassMerge keeps only the last one.
|
|
137
160
|
def modifier_groups
|
|
138
|
-
@modifier_groups
|
|
161
|
+
@modifier_groups || {}
|
|
139
162
|
end
|
|
140
163
|
|
|
164
|
+
# Derived from the class name on first use, the same value every time.
|
|
165
|
+
# Remembered on the main Ractor only (DaisyUI.finalize! derives it at
|
|
166
|
+
# boot); a non-main Ractor cannot set a class ivar, so it derives it.
|
|
141
167
|
def component_class
|
|
142
168
|
return @component_class if instance_variable_defined?(:@component_class)
|
|
143
169
|
|
|
144
|
-
|
|
170
|
+
derived = begin
|
|
145
171
|
# Convert "DaisyUI::Button" -> "button"
|
|
146
172
|
class_name = name.split("::").last # demodulize
|
|
147
173
|
class_name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
148
174
|
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
149
175
|
.tr("_", "-")
|
|
150
|
-
.downcase
|
|
176
|
+
.downcase.freeze
|
|
151
177
|
end
|
|
178
|
+
Store.main_ractor? ? (@component_class = derived) : derived
|
|
152
179
|
end
|
|
153
180
|
|
|
154
181
|
def inherited(subclass)
|
|
@@ -161,6 +188,7 @@ module DaisyUI
|
|
|
161
188
|
|
|
162
189
|
subclass.component_class = @component_class
|
|
163
190
|
end
|
|
191
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
164
192
|
|
|
165
193
|
# Registers modifiers. A Hash value declares a group of alternatives:
|
|
166
194
|
#
|
|
@@ -178,15 +206,23 @@ module DaisyUI
|
|
|
178
206
|
end
|
|
179
207
|
end
|
|
180
208
|
|
|
181
|
-
modifier_groups.
|
|
209
|
+
groups = modifier_groups.transform_values { |members| members.reject { |member| flat.key?(member) } }
|
|
182
210
|
mods.each do |group, value|
|
|
183
|
-
(
|
|
211
|
+
groups[group] = (groups[group] || []) + value.keys if value.is_a?(Hash)
|
|
184
212
|
end
|
|
185
|
-
modifier_groups.delete_if { |_group, members| members.empty? }
|
|
186
213
|
|
|
214
|
+
self.modifier_groups = groups.reject { |_group, members| members.empty? }
|
|
187
215
|
self.modifiers = (modifiers || {}).merge(flat)
|
|
188
216
|
ClassMerge.reset!
|
|
189
217
|
end
|
|
218
|
+
|
|
219
|
+
# @api private Called by DaisyUI.finalize! for every component: derives
|
|
220
|
+
# component_class on the main Ractor and makes sure every registry is
|
|
221
|
+
# frozen for non-main Ractors.
|
|
222
|
+
def finalize!
|
|
223
|
+
[component_class, modifiers, modifier_groups, flags].each { |registry| Shareable.share(registry) }
|
|
224
|
+
self
|
|
225
|
+
end
|
|
190
226
|
end
|
|
191
227
|
|
|
192
228
|
def initialize(*modifiers, as: :div, id: nil, **options)
|
|
@@ -24,6 +24,20 @@ module DaisyUI
|
|
|
24
24
|
@conflicts = {}
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def initialize_copy(source)
|
|
28
|
+
super
|
|
29
|
+
@theme = @theme.transform_values(&:dup)
|
|
30
|
+
@class_groups = @class_groups.transform_values(&:dup)
|
|
31
|
+
@conflicts = @conflicts.transform_values(&:dup)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @api private Called by DaisyUI.finalize!. A raw class group's
|
|
35
|
+
# validator that closes over app state is kept as it is.
|
|
36
|
+
def finalize!
|
|
37
|
+
[@theme, @class_groups, @conflicts].each { |settings| Shareable.share(settings) }
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
27
41
|
# Registers utilities that behave like an existing one:
|
|
28
42
|
# `utility("text-display", like: "text-lg")` makes `text-display` a font
|
|
29
43
|
# size, so it replaces `text-sm` and sits next to `text-error`.
|
|
@@ -57,11 +71,12 @@ module DaisyUI
|
|
|
57
71
|
append(@conflicts, conflicts)
|
|
58
72
|
end
|
|
59
73
|
|
|
60
|
-
# @api private The config passed to Merger.new, including the daisyUI
|
|
61
|
-
|
|
74
|
+
# @api private The config passed to Merger.new, including the daisyUI
|
|
75
|
+
# groups of +configuration+ (the DaisyUI configuration holding this one).
|
|
76
|
+
def merger_config(configuration = DaisyUI.configuration)
|
|
62
77
|
{
|
|
63
78
|
theme: @theme,
|
|
64
|
-
class_groups: DaisyGroups.build.merge(@class_groups) { |_id, existing, added| existing + added },
|
|
79
|
+
class_groups: DaisyGroups.build(configuration).merge(@class_groups) { |_id, existing, added| existing + added },
|
|
65
80
|
conflicting_class_groups: @conflicts
|
|
66
81
|
}
|
|
67
82
|
end
|
|
@@ -18,8 +18,9 @@ module DaisyUI
|
|
|
18
18
|
# @api private
|
|
19
19
|
module DaisyGroups
|
|
20
20
|
class << self
|
|
21
|
-
# Class groups for the merger: `{ "daisy:group:btn-size" => ["btn-xs", ...], ... }
|
|
22
|
-
|
|
21
|
+
# Class groups for the merger: `{ "daisy:group:btn-size" => ["btn-xs", ...], ... }`,
|
|
22
|
+
# from one configuration's prefix and modifiers.
|
|
23
|
+
def build(configuration = DaisyUI.configuration, prefix: configuration.prefix)
|
|
23
24
|
components = self.components
|
|
24
25
|
groups = {}
|
|
25
26
|
|
|
@@ -37,7 +38,7 @@ module DaisyUI
|
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
# Configured modifiers join the component's own group of that name.
|
|
40
|
-
|
|
41
|
+
configuration.modifiers.groups.each do |(component, group), classes|
|
|
41
42
|
owner = component ? component.component_class || component.name : "global"
|
|
42
43
|
add(groups, "daisy:group:#{owner}-#{group}", classes.flat_map(&:split), prefix)
|
|
43
44
|
end
|
|
@@ -51,9 +52,11 @@ module DaisyUI
|
|
|
51
52
|
|
|
52
53
|
# Every named DaisyUI::Base subclass: the gem's components (eager
|
|
53
54
|
# loaded) and any app component loaded so far. An app component that
|
|
54
|
-
# loads later resets the merger through `register_modifiers`.
|
|
55
|
+
# loads later resets the merger through `register_modifiers`. Only the
|
|
56
|
+
# main Ractor can eager-load; DaisyUI.finalize! did it there before
|
|
57
|
+
# any other Ractor builds a merger.
|
|
55
58
|
def components
|
|
56
|
-
Zeitwerk::Loader.eager_load_namespace(DaisyUI)
|
|
59
|
+
Zeitwerk::Loader.eager_load_namespace(DaisyUI) if Store.main_ractor?
|
|
57
60
|
descendants(DaisyUI::Base).select(&:name)
|
|
58
61
|
end
|
|
59
62
|
|
|
@@ -8,21 +8,6 @@ module DaisyUI
|
|
|
8
8
|
module TailwindConfig
|
|
9
9
|
include Validators
|
|
10
10
|
|
|
11
|
-
class << self
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
# Recursively freezes nested Hash/Array structures so shared config
|
|
15
|
-
# constants can't be mutated in place. Keys are strings/symbols and are
|
|
16
|
-
# already immutable, so only values need walking.
|
|
17
|
-
def deep_freeze(object)
|
|
18
|
-
case object
|
|
19
|
-
when Hash then object.each_value { |value| deep_freeze(value) }
|
|
20
|
-
when Array then object.each { |value| deep_freeze(value) }
|
|
21
|
-
end
|
|
22
|
-
object.freeze
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
11
|
THEME_KEYS = %w[
|
|
27
12
|
color
|
|
28
13
|
font
|
|
@@ -46,8 +31,8 @@ module DaisyUI
|
|
|
46
31
|
].freeze
|
|
47
32
|
THEME_KEYS.each do |key|
|
|
48
33
|
const_set("THEME_#{key.upcase.tr('-', '_')}", lambda { |config|
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
config[:theme].fetch(key, nil)
|
|
35
|
+
}
|
|
51
36
|
)
|
|
52
37
|
end
|
|
53
38
|
|
|
@@ -2465,7 +2450,12 @@ module DaisyUI
|
|
|
2465
2450
|
"selection"
|
|
2466
2451
|
]
|
|
2467
2452
|
}.freeze
|
|
2468
|
-
|
|
2453
|
+
|
|
2454
|
+
# Every constant above, DEFAULTS deeply, is made Ractor-shareable so a
|
|
2455
|
+
# non-main Ractor can build its own merger from them: the scale and theme
|
|
2456
|
+
# lambdas are capture-free (THEME_* only reads its frozen key) and their
|
|
2457
|
+
# self is this module.
|
|
2458
|
+
constants(false).each { |name| Shareable.make(const_get(name)) }
|
|
2469
2459
|
|
|
2470
2460
|
# Extends DEFAULTS with incoming_config without mutating either.
|
|
2471
2461
|
# Theme scales are appended. Unlike upstream, `class_groups` and
|
|
@@ -197,6 +197,10 @@ module DaisyUI
|
|
|
197
197
|
IS_LABEL_SHADOW = lambda { |label|
|
|
198
198
|
label == "shadow"
|
|
199
199
|
}
|
|
200
|
+
|
|
201
|
+
# Ractor-shareable, so a non-main Ractor can read them: every validator
|
|
202
|
+
# is a capture-free lambda whose self is this module.
|
|
203
|
+
constants(false).each { |name| Shareable.make(const_get(name)) }
|
|
200
204
|
end
|
|
201
205
|
end
|
|
202
206
|
end
|
data/lib/daisy_ui/class_merge.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "monitor"
|
|
4
|
-
|
|
5
3
|
module DaisyUI
|
|
6
4
|
# Merges CSS class lists so that later classes win over earlier conflicting
|
|
7
5
|
# ones, for Tailwind utilities and daisyUI modifiers alike:
|
|
@@ -16,10 +14,10 @@ module DaisyUI
|
|
|
16
14
|
module ClassMerge
|
|
17
15
|
TAILWIND_MERGE_VERSION = "1.5.6"
|
|
18
16
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
# The merger (with its LRU) and the Tailwind class groups live in
|
|
18
|
+
# DaisyUI::Store: one per Ractor, built on first use under the store's
|
|
19
|
+
# reentrant lock, so a reset! triggered while the merger is being built
|
|
20
|
+
# (e.g. a component file configuring DaisyUI as it loads) cannot deadlock.
|
|
23
21
|
class << self
|
|
24
22
|
# Accepts strings, nested arrays, nil and false (dropped, as Phlex does
|
|
25
23
|
# for `class:`). Returns a frozen String, "" when empty.
|
|
@@ -27,20 +25,33 @@ module DaisyUI
|
|
|
27
25
|
merger.merge(parts.flatten.select(&:itself).join(" "))
|
|
28
26
|
end
|
|
29
27
|
|
|
28
|
+
# Each Ractor's merger remembers the configuration it was built from
|
|
29
|
+
# and is rebuilt once DaisyUI.configure publishes another, also in a
|
|
30
|
+
# Ractor where reset! never ran.
|
|
30
31
|
def merger
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
configuration = DaisyUI.configuration
|
|
33
|
+
built_for, merger = Store.fetch(:daisy_ui_class_merger) { build_merger(configuration) }
|
|
34
|
+
return merger if built_for.equal?(configuration)
|
|
35
|
+
|
|
36
|
+
Store.delete(:daisy_ui_class_merger)
|
|
37
|
+
Store.fetch(:daisy_ui_class_merger) { build_merger(configuration) }.last
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
# @api private The stock Tailwind class groups, for classifying tokens.
|
|
37
41
|
def tailwind_utils
|
|
38
|
-
|
|
42
|
+
Store.fetch(:daisy_ui_tailwind_utils) { ClassGroupUtils.new(TailwindConfig::DEFAULTS) }
|
|
39
43
|
end
|
|
40
44
|
|
|
41
|
-
# Drops the
|
|
45
|
+
# Drops the current Ractor's merger so its next merge picks up
|
|
46
|
+
# configuration changes.
|
|
42
47
|
def reset!
|
|
43
|
-
|
|
48
|
+
Store.delete(:daisy_ui_class_merger)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def build_merger(configuration)
|
|
54
|
+
[configuration, Merger.new(config: configuration.class_merge.merger_config(configuration))]
|
|
44
55
|
end
|
|
45
56
|
end
|
|
46
57
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Configurable
|
|
5
|
+
# @api private The unknown modifiers already reported, so :warn and a
|
|
6
|
+
# callable fire once per key. Remembers up to `limit` keys, then starts
|
|
7
|
+
# over, so modifiers built from user input cannot grow it without bound.
|
|
8
|
+
# One per Ractor (DaisyUI::Store), so its Mutex is never shared.
|
|
9
|
+
class ReportLog
|
|
10
|
+
def initialize(limit)
|
|
11
|
+
@limit = limit
|
|
12
|
+
@keys = Set.new
|
|
13
|
+
@lock = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# True the first time a key is seen.
|
|
17
|
+
def first?(key)
|
|
18
|
+
@lock.synchronize do
|
|
19
|
+
return false if @keys.include?(key)
|
|
20
|
+
|
|
21
|
+
@keys.clear if @keys.size >= @limit
|
|
22
|
+
@keys.add(key)
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear
|
|
28
|
+
@lock.synchronize { @keys.clear }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def size
|
|
32
|
+
@lock.synchronize { @keys.size }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -2,15 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
module DaisyUI
|
|
4
4
|
module Configurable
|
|
5
|
+
# Created when DaisyUI extends this module, at load, so `configuration` is
|
|
6
|
+
# a plain read with no check-then-set race.
|
|
7
|
+
def self.extended(base)
|
|
8
|
+
base.instance_variable_set(:@configuration, Configuration.new)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Yields a writable copy of the configuration, then finalizes the copy
|
|
12
|
+
# (see finalize!) and publishes it. Calling configure again keeps
|
|
13
|
+
# working, no reader (another thread, a non-main Ractor) meets a
|
|
14
|
+
# half-changed or writable configuration, and a block that raises
|
|
15
|
+
# publishes nothing. Calls are serialized, so each starts from what the
|
|
16
|
+
# previous one published.
|
|
5
17
|
def configure
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
configure_lock.synchronize do
|
|
19
|
+
draft = configuration.dup
|
|
20
|
+
yield(draft) if block_given?
|
|
21
|
+
@configuration = draft.finalize!
|
|
22
|
+
ClassMerge.reset!
|
|
23
|
+
finalize!
|
|
24
|
+
configuration
|
|
25
|
+
end
|
|
10
26
|
end
|
|
11
27
|
|
|
12
28
|
def configuration
|
|
13
|
-
@configuration
|
|
29
|
+
@configuration
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Freezes what a non-main Ractor reads (this configuration and every
|
|
33
|
+
# component's registries) so DaisyUI::ClassMerge.merge runs in any Ractor.
|
|
34
|
+
# configure calls it, and the Rails engine calls it once the app has
|
|
35
|
+
# booted; an app with neither calls it at the end of boot. Afterwards
|
|
36
|
+
# change settings only through configure: writing to the frozen
|
|
37
|
+
# configuration raises. A configured value that cannot be made shareable
|
|
38
|
+
# is kept as it is (see Shareable.share), never raised on.
|
|
39
|
+
def finalize!
|
|
40
|
+
ClassMerge::DaisyGroups.components.each(&:finalize!)
|
|
41
|
+
configuration.finalize!
|
|
42
|
+
ClassMerge.tailwind_utils
|
|
43
|
+
self
|
|
14
44
|
end
|
|
15
45
|
|
|
16
46
|
class Modifiers
|
|
@@ -19,6 +49,19 @@ module DaisyUI
|
|
|
19
49
|
@groups = {}
|
|
20
50
|
end
|
|
21
51
|
|
|
52
|
+
def initialize_copy(source)
|
|
53
|
+
super
|
|
54
|
+
@modifiers = @modifiers.transform_values(&:dup)
|
|
55
|
+
@groups = @groups.dup
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @api private Called by DaisyUI.finalize!.
|
|
59
|
+
def finalize!
|
|
60
|
+
Shareable.share(@modifiers)
|
|
61
|
+
Shareable.share(@groups)
|
|
62
|
+
freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
22
65
|
# `group:` makes the modifier an alternative to the component's other
|
|
23
66
|
# modifiers in that group, e.g. `add(:huge, classes: "btn-xl",
|
|
24
67
|
# component: DaisyUI::Button, group: :size)`.
|
|
@@ -59,8 +102,24 @@ module DaisyUI
|
|
|
59
102
|
|
|
60
103
|
def initialize
|
|
61
104
|
@prefix = nil
|
|
62
|
-
|
|
63
|
-
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def initialize_copy(source)
|
|
108
|
+
super
|
|
109
|
+
@class_merge = @class_merge&.dup
|
|
110
|
+
@modifiers = @modifiers&.dup
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @api private Called by DaisyUI.configure and DaisyUI.finalize!. A
|
|
114
|
+
# callable unknown_modifiers that cannot be made shareable (a lambda
|
|
115
|
+
# that closes over app state, an object holding a logger) is kept: the
|
|
116
|
+
# configuration is then frozen but readable on the main Ractor only.
|
|
117
|
+
def finalize!
|
|
118
|
+
class_merge.finalize!
|
|
119
|
+
modifiers.finalize!
|
|
120
|
+
Shareable.share(@prefix)
|
|
121
|
+
Shareable.share(@unknown_modifiers)
|
|
122
|
+
freeze
|
|
64
123
|
end
|
|
65
124
|
|
|
66
125
|
# What a component does with a positional modifier it does not know:
|
|
@@ -94,7 +153,7 @@ module DaisyUI
|
|
|
94
153
|
|
|
95
154
|
# @api private
|
|
96
155
|
def reset_reported_unknown_modifiers!
|
|
97
|
-
|
|
156
|
+
reported_unknown_modifiers.clear
|
|
98
157
|
end
|
|
99
158
|
|
|
100
159
|
def prefix=(prefix)
|
|
@@ -112,16 +171,14 @@ module DaisyUI
|
|
|
112
171
|
|
|
113
172
|
private
|
|
114
173
|
|
|
115
|
-
# Remembers up to REPORTED_UNKNOWN_MODIFIERS_LIMIT keys, then starts over,
|
|
116
|
-
# so modifiers built from user input cannot grow it without bound.
|
|
117
174
|
def first_report?(key)
|
|
118
|
-
|
|
119
|
-
|
|
175
|
+
reported_unknown_modifiers.first?(key)
|
|
176
|
+
end
|
|
120
177
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
178
|
+
# What was already reported lives per Ractor, outside the configuration,
|
|
179
|
+
# which stays shareable.
|
|
180
|
+
def reported_unknown_modifiers
|
|
181
|
+
Store.fetch(:daisy_ui_reported_unknown_modifiers) { ReportLog.new(REPORTED_UNKNOWN_MODIFIERS_LIMIT) }
|
|
125
182
|
end
|
|
126
183
|
|
|
127
184
|
def default_unknown_modifiers
|
|
@@ -130,5 +187,13 @@ module DaisyUI
|
|
|
130
187
|
%w[development test].include?(::Rails.env.to_s) ? :raise : :warn
|
|
131
188
|
end
|
|
132
189
|
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
# Reentrant, and one per Ractor (DaisyUI::Store) so it is never shared;
|
|
194
|
+
# configure writes DaisyUI's ivar, so only the main Ractor gets that far.
|
|
195
|
+
def configure_lock
|
|
196
|
+
Store.fetch(:daisy_ui_configure_lock) { Monitor.new }
|
|
197
|
+
end
|
|
133
198
|
end
|
|
134
199
|
end
|
data/lib/daisy_ui/engine.rb
CHANGED
|
@@ -34,6 +34,13 @@ module DaisyUI
|
|
|
34
34
|
precompile.concat(MINIFIED_ASSETS) if precompile.is_a?(Array)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Once the app has booted, freeze the configuration and component
|
|
38
|
+
# registries so a non-main Ractor can merge classes. DaisyUI.configure in
|
|
39
|
+
# an initializer already finalized; this also covers an app that never
|
|
40
|
+
# calls it and the components loaded during boot (eager loading).
|
|
41
|
+
# DaisyUI.configure keeps working afterwards, on a copy.
|
|
42
|
+
config.after_initialize { DaisyUI.finalize! }
|
|
43
|
+
|
|
37
44
|
initializer "daisy_ui.importmap", before: "importmap" do |app|
|
|
38
45
|
next unless app.config.respond_to?(:importmap)
|
|
39
46
|
|
data/lib/daisy_ui/mcp_server.rb
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
# @api private Deep-freezes a value so a non-main Ractor can read it from a
|
|
5
|
+
# constant or a class ivar: `Ractor.make_shareable`, or, on engines without
|
|
6
|
+
# Ractor (JRuby, TruffleRuby), a walk freezing Hash values and Array elements.
|
|
7
|
+
module Shareable
|
|
8
|
+
class << self
|
|
9
|
+
# For the gem's own literals, which must all be shareable: raises if one
|
|
10
|
+
# is not.
|
|
11
|
+
def make(value)
|
|
12
|
+
ractor? ? ::Ractor.make_shareable(value) : deep_freeze(value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# For what an app configures. Freezes Hashes, Arrays and Strings all the
|
|
16
|
+
# way down, makes a capture-free Proc shareable, and keeps anything else
|
|
17
|
+
# (a Proc that closes over app state, a logger) as it is, so whatever
|
|
18
|
+
# holds it is readable on the main Ractor only. Never raises, and never
|
|
19
|
+
# hands an object it does not know to Ractor.make_shareable, which
|
|
20
|
+
# freezes as it goes and would leave a logger frozen before failing.
|
|
21
|
+
def share(value)
|
|
22
|
+
case value
|
|
23
|
+
when Hash
|
|
24
|
+
value.each do |key, member|
|
|
25
|
+
share(key)
|
|
26
|
+
share(member)
|
|
27
|
+
end.freeze
|
|
28
|
+
when Array then value.each { |member| share(member) }.freeze
|
|
29
|
+
when String then value.freeze
|
|
30
|
+
when Proc then share_proc(value)
|
|
31
|
+
else value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ractor?
|
|
36
|
+
defined?(::Ractor) ? ::Ractor.respond_to?(:make_shareable) : false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def share_proc(callable)
|
|
42
|
+
ractor? ? ::Ractor.make_shareable(callable) : callable
|
|
43
|
+
rescue ::Ractor::Error, ArgumentError
|
|
44
|
+
callable
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def deep_freeze(value)
|
|
48
|
+
case value
|
|
49
|
+
when Hash then value.each_value { |member| deep_freeze(member) }
|
|
50
|
+
when Array then value.each { |member| deep_freeze(member) }
|
|
51
|
+
end
|
|
52
|
+
value.freeze
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module DaisyUI
|
|
6
|
+
# @api private Per-Ractor lazy storage for caches that must not live in a
|
|
7
|
+
# class ivar or a constant (the class-merge merger, the Tailwind class
|
|
8
|
+
# groups, the unknown-modifier report log):
|
|
9
|
+
#
|
|
10
|
+
# DaisyUI::Store.fetch(:daisy_ui_merger) { Merger.new(...) }
|
|
11
|
+
# DaisyUI::Store.delete(:daisy_ui_merger)
|
|
12
|
+
#
|
|
13
|
+
# Adapted from conformance-kit's ConformanceKit::Store (MIT, same author),
|
|
14
|
+
# which daisyui cannot depend on at runtime, plus `delete`. Every Ractor has
|
|
15
|
+
# its own Monitor-guarded table: reentrant, so an initializer may fetch or
|
|
16
|
+
# delete other keys. On Ruby 3.4+ the table comes from
|
|
17
|
+
# `Ractor.store_if_absent`. Below 3.4 the main Ractor's table is created at
|
|
18
|
+
# load (never lazily, so there is no check-then-set race), and a non-main
|
|
19
|
+
# Ractor keeps its own in Ractor-local storage.
|
|
20
|
+
module Store
|
|
21
|
+
# One Ractor's values. Reads skip the lock once a key is stored.
|
|
22
|
+
class Table
|
|
23
|
+
def initialize
|
|
24
|
+
@monitor = Monitor.new
|
|
25
|
+
@values = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fetch(key)
|
|
29
|
+
@values.fetch(key) do
|
|
30
|
+
@monitor.synchronize { @values.fetch(key) { @values[key] = yield } }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def delete(key)
|
|
35
|
+
@monitor.synchronize { @values.delete(key) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
def fetch(key, &init)
|
|
41
|
+
raise ArgumentError, "DaisyUI::Store.fetch(#{key.inspect}) needs an initializer block" unless init
|
|
42
|
+
|
|
43
|
+
table.fetch(key, &init)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def delete(key)
|
|
47
|
+
table.delete(key)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def main_ractor?
|
|
51
|
+
!defined?(::Ractor) || ::Ractor.current == ::Ractor.main
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def native?
|
|
55
|
+
defined?(::Ractor) ? ::Ractor.respond_to?(:store_if_absent) : false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def table
|
|
61
|
+
return ::Ractor.store_if_absent(:daisy_ui_store) { Table.new } if native?
|
|
62
|
+
return ::Ractor.current[:daisy_ui_store] ||= Table.new unless main_ractor?
|
|
63
|
+
|
|
64
|
+
# Set once below, at load, below Ruby 3.4; only the main Ractor reads it.
|
|
65
|
+
@main_table # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@main_table = Table.new unless native?
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/daisy_ui/updated_at.rb
CHANGED
data/lib/daisy_ui/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: daisyui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -98,6 +98,7 @@ files:
|
|
|
98
98
|
- lib/daisy_ui/collapse.rb
|
|
99
99
|
- lib/daisy_ui/collapsible_sub_menu.rb
|
|
100
100
|
- lib/daisy_ui/configurable.rb
|
|
101
|
+
- lib/daisy_ui/configurable/report_log.rb
|
|
101
102
|
- lib/daisy_ui/countdown.rb
|
|
102
103
|
- lib/daisy_ui/diff.rb
|
|
103
104
|
- lib/daisy_ui/divider.rb
|
|
@@ -141,11 +142,13 @@ files:
|
|
|
141
142
|
- lib/daisy_ui/range.rb
|
|
142
143
|
- lib/daisy_ui/rating.rb
|
|
143
144
|
- lib/daisy_ui/select.rb
|
|
145
|
+
- lib/daisy_ui/shareable.rb
|
|
144
146
|
- lib/daisy_ui/skeleton.rb
|
|
145
147
|
- lib/daisy_ui/stack.rb
|
|
146
148
|
- lib/daisy_ui/stat.rb
|
|
147
149
|
- lib/daisy_ui/status.rb
|
|
148
150
|
- lib/daisy_ui/steps.rb
|
|
151
|
+
- lib/daisy_ui/store.rb
|
|
149
152
|
- lib/daisy_ui/sub_menu.rb
|
|
150
153
|
- lib/daisy_ui/swap.rb
|
|
151
154
|
- lib/daisy_ui/tab.rb
|