docs-kit 1.3.0 → 1.4.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 +53 -0
- data/README.md +27 -0
- data/app/components/docs_ui/callout.rb +3 -3
- data/app/components/docs_ui/code.rb +3 -1
- data/app/components/docs_ui/page.rb +8 -4
- data/lib/docs_kit/api_client.rb +22 -18
- data/lib/docs_kit/configuration.rb +80 -11
- data/lib/docs_kit/engine.rb +14 -1
- data/lib/docs_kit/og_generator.rb +3 -1
- data/lib/docs_kit/registry.rb +49 -8
- data/lib/docs_kit/shareable.rb +14 -0
- data/lib/docs_kit/snapshot.rb +31 -12
- data/lib/docs_kit/store.rb +57 -0
- data/lib/docs_kit/version.rb +1 -1
- data/lib/docs_kit.rb +25 -5
- data/lib/generators/docs_kit/install/migration.rb +7 -2
- data/lib/generators/docs_kit/install/migration_registry.rb +15 -10
- data/lib/generators/docs_kit/install/migrations/v1_2_0.rb +7 -5
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6340fb386df08272f63e525ca81d43baaa1f440a82287e4a34e9a392b220a530
|
|
4
|
+
data.tar.gz: 3740655d5194976ea25a3ed3cdd6a633bb593c03c288a1c3a069d16d7c522cfe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01e79925a8f67168a27e898fb13f6a9a5658b331b8a5b1ba854c9481c1afc5256f7ec89fb80a6e762f9ec0fd399425c38be82a1943a8fc9517e5cd0803e32fa4
|
|
7
|
+
data.tar.gz: a9a74e687605433c9a23069d6760a5a14093e898e0f64b133ede84f951f9f4deead0e98ad7735ca5a87e9f79ba86c9a340993004f1ece46731bda6016e6b9bab
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.4.0]
|
|
6
|
+
|
|
7
|
+
### Upgrading from 1.3
|
|
8
|
+
|
|
9
|
+
Nothing to change for a site whose initializer configures docs-kit in a
|
|
10
|
+
`to_prepare` block, as the generated one does. Three things now fail loudly
|
|
11
|
+
instead of later or never: a broken `brand_logo` raises at boot, in
|
|
12
|
+
development too; mutating a registry DSL or `DocsUI::Page` macro value in place
|
|
13
|
+
raises `FrozenError`; so does mutating one of docs-kit's constants.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **A cold archived version is built once, however many requests ask.**
|
|
18
|
+
`DocsKit::Snapshot.for` cached snapshots in an unguarded class ivar, so
|
|
19
|
+
threads that missed a version together each read and parsed its
|
|
20
|
+
`manifest.json` and raced to store it. The cache is now per Ractor and its
|
|
21
|
+
check-then-set runs under a lock; `Configuration#openapi_document`'s
|
|
22
|
+
mtime-checked memo likewise. (#91)
|
|
23
|
+
- **No per-render constant lookups once the app has booted.** The engine now
|
|
24
|
+
calls `DocsKit.finalize!` once the host has configured the kit, with no app
|
|
25
|
+
code: the Rouge code themes and every registered page's view class are
|
|
26
|
+
resolved once into frozen maps instead of `Object.const_get` /
|
|
27
|
+
`safe_constantize` on every render, and the brand logo, SEO, landing and
|
|
28
|
+
optional-mcp memos are filled at boot rather than by whichever request comes
|
|
29
|
+
first. `DocsKit.configure` re-opens it, and a theme assigned directly
|
|
30
|
+
afterwards still wins. Outside Rails, call `DocsKit.finalize!` yourself. (#91)
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Finalized after boot in every environment, and after every reload.** The
|
|
35
|
+
engine finalizes in `after_initialize` in development too. With code
|
|
36
|
+
reloading on, it finalizes again after each reload, once the host's
|
|
37
|
+
`to_prepare` configure has re-run and re-opened the configuration, so the
|
|
38
|
+
resolved page views are the reloaded classes. The host's callables (`brand`,
|
|
39
|
+
`nav`, `version_badge`, `importmap`, ...) are kept as given, never frozen:
|
|
40
|
+
they may close over app state, so `DocsKit.configuration` stays
|
|
41
|
+
main-Ractor-only and finalizing never fails on one. (#91)
|
|
42
|
+
- **Boot-time values are frozen.** The registry DSL (`page`, `path_prefix`,
|
|
43
|
+
`view_namespace`, `group_by_attribute`) and `DocsUI::Page`'s `title`,
|
|
44
|
+
`eyebrow`, `description` and `on_page` macros deep-freeze what they are
|
|
45
|
+
given (Ractor-shareable), as do the gem's own constants, and
|
|
46
|
+
`DocsKit.configuration` is built at load rather than on first read. Code
|
|
47
|
+
that mutated one of those values in place after declaring it now raises
|
|
48
|
+
`FrozenError`. (#91)
|
|
49
|
+
- **A broken `brand_logo` fails at boot.** `DocsKit.finalize!` builds the logo,
|
|
50
|
+
so a missing file or non-SVG markup raises when the app boots, in every
|
|
51
|
+
environment, instead of on the first render. (#91)
|
|
52
|
+
- docs-kit is held to [conformance-kit](https://github.com/zoolutions/conformance-kit)
|
|
53
|
+
0.4 in CI: the rubocop-thread_safety cops, thread and fiber contention specs,
|
|
54
|
+
a Ruby 4.0 Ractor canary (search index, llms.txt, the sidebar tree) and
|
|
55
|
+
`conformance-audit --strict`, whose allowlist names Rails' and Thor's class
|
|
56
|
+
state on the engine, controllers and generators by `ancestor:` rule. (#91)
|
|
57
|
+
|
|
5
58
|
## [1.3.0] — 2026-10-13
|
|
6
59
|
|
|
7
60
|
### Fixed
|
data/README.md
CHANGED
|
@@ -363,6 +363,33 @@ c.nav = lambda do
|
|
|
363
363
|
end
|
|
364
364
|
```
|
|
365
365
|
|
|
366
|
+
### After boot: finalized, threads and Ractors
|
|
367
|
+
|
|
368
|
+
The engine calls `DocsKit.finalize!` once the app has booted (`after_initialize`,
|
|
369
|
+
after your `to_prepare` configure) in every environment, and in development again
|
|
370
|
+
after every code reload. It resolves the code themes and every registered page's
|
|
371
|
+
view class once, into frozen maps, and builds the brand logo, SEO, landing and
|
|
372
|
+
optional-mcp memos up front, so no request races another to fill a memo, or
|
|
373
|
+
looks up a theme or a resolved page view. A page whose view class didn't exist
|
|
374
|
+
at boot is still looked up when it is read, so it turns up once written. That
|
|
375
|
+
is also why a broken `brand_logo` fails at boot.
|
|
376
|
+
|
|
377
|
+
- `DocsKit.configure` re-opens a finalized configuration, so re-running it (a
|
|
378
|
+
development reload, a spec) takes effect.
|
|
379
|
+
- Outside Rails (a script or Rack app that renders the chrome), call
|
|
380
|
+
`DocsKit.finalize!` yourself once the configuration is complete.
|
|
381
|
+
- Your callables (`brand`, `title_suffix`, `nav`, `version_badge`, `importmap`)
|
|
382
|
+
are kept exactly as given: they may close over app state (`Current`, a
|
|
383
|
+
tenant), so they are never frozen or copied, and finalizing never fails on
|
|
384
|
+
one. That makes `DocsKit.configuration` main-Ractor-only. Code that runs in a
|
|
385
|
+
non-main Ractor builds its own `DocsKit::Configuration` from plain values and
|
|
386
|
+
passes it in (`DocsKit::LlmsText.index(config, ...)`,
|
|
387
|
+
`DocsKit::Snapshot.for(version, config:)`).
|
|
388
|
+
- What the registry DSL (`page`, `path_prefix`, `view_namespace`) and
|
|
389
|
+
`DocsUI::Page`'s macros (`title`, `eyebrow`, `description`, `on_page`) are
|
|
390
|
+
given is deep-frozen when declared, as are docs-kit's own constants: mutating
|
|
391
|
+
one in place raises `FrozenError`.
|
|
392
|
+
|
|
366
393
|
## Render
|
|
367
394
|
|
|
368
395
|
```ruby
|
|
@@ -7,9 +7,9 @@ module DocsUI
|
|
|
7
7
|
# render DocsUI::Callout.new(:warning) { "Restart the server after…" }
|
|
8
8
|
class Callout < Phlex::HTML
|
|
9
9
|
LEVELS = {
|
|
10
|
-
note: { klass: "alert-info", icon: "info" },
|
|
11
|
-
tip: { klass: "alert-success", icon: "lightbulb" },
|
|
12
|
-
warning: { klass: "alert-warning", icon: "triangle-alert" }
|
|
10
|
+
note: { klass: "alert-info", icon: "info" }.freeze,
|
|
11
|
+
tip: { klass: "alert-success", icon: "lightbulb" }.freeze,
|
|
12
|
+
warning: { klass: "alert-warning", icon: "triangle-alert" }.freeze
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
15
|
def initialize(level = :note, title: nil)
|
|
@@ -24,7 +24,9 @@ module DocsUI
|
|
|
24
24
|
class Code < Phlex::HTML
|
|
25
25
|
include Phlex::Rails::Helpers::ContentSecurityPolicyNonce
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
# Ractor-shareable (deep-frozen): Rouge's HTML formatter keeps no per-call
|
|
28
|
+
# state, so one instance serves every render (and every Ractor).
|
|
29
|
+
FORMATTER = DocsKit::Shareable.make(Rouge::Formatters::HTML.new)
|
|
28
30
|
|
|
29
31
|
def initialize(source, lexer: nil, filename: nil)
|
|
30
32
|
@source = source.to_s.strip
|
|
@@ -35,15 +35,19 @@ module DocsUI
|
|
|
35
35
|
# friction-free path that never trips the parens-with-blocks gotcha.
|
|
36
36
|
include DocsUI::PageHelpers
|
|
37
37
|
|
|
38
|
+
# The page macros run in a page class's body, at load (boot, on the main
|
|
39
|
+
# Ractor), and make each value shareable on write: request threads only
|
|
40
|
+
# read them afterwards, and a non-main Ractor can too.
|
|
41
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable -- written once in the class body at load; values made shareable
|
|
38
42
|
class << self
|
|
39
43
|
def title(value = nil)
|
|
40
|
-
@title = value if value
|
|
44
|
+
@title = DocsKit::Shareable.make(value) if value
|
|
41
45
|
@title
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
# Inherited: a subclass that sets no eyebrow shows its parent page's.
|
|
45
49
|
def eyebrow(value = nil)
|
|
46
|
-
@eyebrow = value if value
|
|
50
|
+
@eyebrow = DocsKit::Shareable.make(value) if value
|
|
47
51
|
@eyebrow || (superclass.eyebrow if parent_page?)
|
|
48
52
|
end
|
|
49
53
|
|
|
@@ -53,7 +57,7 @@ module DocsUI
|
|
|
53
57
|
# description, no lead) falls back to config.seo.description in MetaTags.
|
|
54
58
|
# Inherited from a parent page when unset.
|
|
55
59
|
def description(value = nil)
|
|
56
|
-
@description = value if value
|
|
60
|
+
@description = DocsKit::Shareable.make(value) if value
|
|
57
61
|
@description || (superclass.description if parent_page?)
|
|
58
62
|
end
|
|
59
63
|
|
|
@@ -62,7 +66,7 @@ module DocsUI
|
|
|
62
66
|
# or :panel/:toggle/:sidebar to override per page. Inherited from a parent
|
|
63
67
|
# page when unset.
|
|
64
68
|
def on_page(value = :__unset__)
|
|
65
|
-
@on_page = value unless value == :__unset__
|
|
69
|
+
@on_page = DocsKit::Shareable.make(value) unless value == :__unset__
|
|
66
70
|
return @on_page if defined?(@on_page)
|
|
67
71
|
|
|
68
72
|
parent_page? ? superclass.on_page : DocsKit.configuration.on_page_default
|
data/lib/docs_kit/api_client.rb
CHANGED
|
@@ -31,22 +31,26 @@ module DocsKit
|
|
|
31
31
|
|
|
32
32
|
# The four generic-HTTP clients every site starts from. Order is stable
|
|
33
33
|
# (curl → javascript → ruby → python) and preserved when a site merges its own.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
# Shareable (lambdas, not Method objects, which never are), so a non-main
|
|
35
|
+
# Ractor can read the defaults.
|
|
36
|
+
ApiClient::DEFAULTS = DocsKit::Shareable.make(
|
|
37
|
+
{
|
|
38
|
+
curl: ApiClient.new(
|
|
39
|
+
label: "cURL", lexer: :curl, filename: "request.sh",
|
|
40
|
+
template: ->(request) { ApiTemplates.curl(request) }
|
|
41
|
+
),
|
|
42
|
+
javascript: ApiClient.new(
|
|
43
|
+
label: "JavaScript", lexer: :javascript, filename: "request.js",
|
|
44
|
+
template: ->(request) { ApiTemplates.javascript(request) }
|
|
45
|
+
),
|
|
46
|
+
ruby: ApiClient.new(
|
|
47
|
+
label: "Ruby", lexer: :ruby, filename: "request.rb",
|
|
48
|
+
template: ->(request) { ApiTemplates.ruby(request) }
|
|
49
|
+
),
|
|
50
|
+
python: ApiClient.new(
|
|
51
|
+
label: "Python", lexer: :python, filename: "request.py",
|
|
52
|
+
template: ->(request) { ApiTemplates.python(request) }
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
)
|
|
52
56
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "shareable"
|
|
3
4
|
require_relative "seo_config"
|
|
4
5
|
require_relative "landing_config"
|
|
5
6
|
|
|
@@ -276,7 +277,7 @@ module DocsKit
|
|
|
276
277
|
|
|
277
278
|
# The sentinel "no explicit nav" lambda. #nav_groups compares against this
|
|
278
279
|
# identity to decide whether to derive the sidebar from #nav_registries.
|
|
279
|
-
DEFAULT_NAV = -> { {} }
|
|
280
|
+
DEFAULT_NAV = DocsKit::Shareable.make(-> { {} })
|
|
280
281
|
|
|
281
282
|
# The search-palette shortcuts before this was configurable — "/" and the
|
|
282
283
|
# platform command chord — so a site that never sets #search_shortcuts keeps
|
|
@@ -347,6 +348,8 @@ module DocsKit
|
|
|
347
348
|
@versions = []
|
|
348
349
|
@repo_url = nil
|
|
349
350
|
@snapshots_path = nil
|
|
351
|
+
@openapi_memo = nil
|
|
352
|
+
@finalized = nil
|
|
350
353
|
end
|
|
351
354
|
|
|
352
355
|
# The normalized App Home link (a DocsKit::TopbarLink), or nil when unset —
|
|
@@ -453,16 +456,60 @@ module DocsKit
|
|
|
453
456
|
# spec in development is picked up without a server restart. Raises a
|
|
454
457
|
# DocsKit::Error naming the knob when read while #openapi is unset — a missing
|
|
455
458
|
# spec has nothing useful to degrade to.
|
|
459
|
+
#
|
|
460
|
+
# The memo lives on this configuration, so a replaced one takes its
|
|
461
|
+
# Document with it, and its mtime check-then-set runs under a per-Ractor
|
|
462
|
+
# lock (DocsKit::Store): threads that miss together load the spec once and
|
|
463
|
+
# read the same Document. A configuration is only ever read in the Ractor
|
|
464
|
+
# that built it, so that Ractor's lock is the one that matters.
|
|
456
465
|
def openapi_document
|
|
457
466
|
raise DocsKit::Error, "no OpenAPI spec configured — set c.openapi to a path or Hash" if @openapi.nil?
|
|
458
467
|
|
|
468
|
+
source = @openapi
|
|
459
469
|
mtime = openapi_source_mtime
|
|
460
|
-
|
|
470
|
+
DocsKit::Store.fetch(:docs_kit_openapi_lock) { Mutex.new }.synchronize do
|
|
471
|
+
memo = @openapi_memo
|
|
472
|
+
next memo.fetch(:document) if memo && memo.fetch(:source).equal?(source) && memo.fetch(:mtime) == mtime
|
|
473
|
+
|
|
474
|
+
DocsKit::OpenApi.load(source).tap do |document|
|
|
475
|
+
@openapi_memo = { source: source, mtime: mtime, document: document }
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
461
479
|
|
|
462
|
-
|
|
463
|
-
|
|
480
|
+
# Resolves, once at boot, what a render would otherwise look up per call:
|
|
481
|
+
# the Rouge theme classes and every registered page's view class (frozen,
|
|
482
|
+
# so a non-main Ractor can read them), and fills the lazy memos (brand logo,
|
|
483
|
+
# SEO, landing, the optional-mcp probe) so no request races to fill them.
|
|
484
|
+
# Called by DocsKit.finalize! (the engine's after_initialize, and after every
|
|
485
|
+
# code reload); DocsKit.configure re-opens it (#reopen!). The host's
|
|
486
|
+
# callables (brand, nav, version_badge, importmap, ...) are kept as they are:
|
|
487
|
+
# they can close over app state, so the configuration itself stays
|
|
488
|
+
# main-Ractor-only. A theme assigned directly afterwards still wins:
|
|
489
|
+
# the resolved class is only used while its source setting is unchanged.
|
|
490
|
+
def finalize!
|
|
491
|
+
brand_logo
|
|
492
|
+
seo
|
|
493
|
+
landing
|
|
494
|
+
mcp_gem_present?
|
|
495
|
+
each_registry { |registry| registry.finalize! if registry.respond_to?(:finalize!) }
|
|
496
|
+
@finalized = DocsKit::Shareable.make(
|
|
497
|
+
code_theme: @code_theme, code_theme_class: resolve_code_theme_class,
|
|
498
|
+
code_theme_dark: @code_theme_dark, code_theme_dark_class: resolve_code_theme_dark_class
|
|
499
|
+
)
|
|
500
|
+
self
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Drops what #finalize! resolved (and the registries' resolved view classes),
|
|
504
|
+
# so the next reads resolve lazily against the current settings.
|
|
505
|
+
def reopen!
|
|
506
|
+
@finalized = nil
|
|
507
|
+
each_registry { |registry| registry.reopen! if registry.respond_to?(:reopen!) }
|
|
508
|
+
self
|
|
464
509
|
end
|
|
465
510
|
|
|
511
|
+
def finalized? = !@finalized.nil?
|
|
512
|
+
|
|
466
513
|
# The effective client map for DocsUI::RequestExample: the four shipped
|
|
467
514
|
# defaults with site overrides/extensions merged over them. Hash#merge keeps
|
|
468
515
|
# a reused token in its original position and appends new tokens in
|
|
@@ -677,9 +724,7 @@ module DocsKit
|
|
|
677
724
|
# name that doesn't resolve degrades to the default theme rather than raising
|
|
678
725
|
# NameError on every DocsUI::Code render.
|
|
679
726
|
def code_theme_class
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
resolve_theme(@code_theme) || Object.const_get(DEFAULT_CODE_THEME)
|
|
727
|
+
resolved(:code_theme_class, :code_theme, @code_theme) { resolve_code_theme_class }
|
|
683
728
|
end
|
|
684
729
|
|
|
685
730
|
# The dark Rouge theme class resolved from #code_theme_dark (String or
|
|
@@ -687,10 +732,7 @@ module DocsKit
|
|
|
687
732
|
# dark code CSS only when this is non-nil, so an unresolvable name degrades to
|
|
688
733
|
# nil (no dark restyle) rather than raising.
|
|
689
734
|
def code_theme_dark_class
|
|
690
|
-
|
|
691
|
-
return @code_theme_dark if @code_theme_dark.is_a?(Class)
|
|
692
|
-
|
|
693
|
-
resolve_theme(@code_theme_dark)
|
|
735
|
+
resolved(:code_theme_dark_class, :code_theme_dark, @code_theme_dark) { resolve_code_theme_dark_class }
|
|
694
736
|
end
|
|
695
737
|
|
|
696
738
|
# The dark themes the site actually ships: #dark_themes intersected with
|
|
@@ -703,6 +745,33 @@ module DocsKit
|
|
|
703
745
|
|
|
704
746
|
private
|
|
705
747
|
|
|
748
|
+
def resolve_code_theme_class
|
|
749
|
+
return @code_theme if @code_theme.is_a?(Class)
|
|
750
|
+
|
|
751
|
+
resolve_theme(@code_theme) || Object.const_get(DEFAULT_CODE_THEME)
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def resolve_code_theme_dark_class
|
|
755
|
+
return if @code_theme_dark.nil?
|
|
756
|
+
return @code_theme_dark if @code_theme_dark.is_a?(Class)
|
|
757
|
+
|
|
758
|
+
resolve_theme(@code_theme_dark)
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# What #finalize! resolved for key, while the setting it came from is still
|
|
762
|
+
# the same object; otherwise the block resolves it now (never finalized,
|
|
763
|
+
# re-opened, or the setting was reassigned since).
|
|
764
|
+
def resolved(key, setting, current)
|
|
765
|
+
finalized = @finalized
|
|
766
|
+
return yield unless finalized && finalized.fetch(setting).equal?(current)
|
|
767
|
+
|
|
768
|
+
finalized.fetch(key)
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def each_registry(&)
|
|
772
|
+
(@nav_registries || {}).each_value(&)
|
|
773
|
+
end
|
|
774
|
+
|
|
706
775
|
# Resolve a Rouge theme constant from its String name, returning nil (not
|
|
707
776
|
# raising) when the name doesn't resolve — a typo'd or unloaded theme must
|
|
708
777
|
# not crash every code block on the page.
|
data/lib/docs_kit/engine.rb
CHANGED
|
@@ -15,7 +15,7 @@ module DocsKit
|
|
|
15
15
|
config.eager_load_paths = []
|
|
16
16
|
config.paths["app"].skip_eager_load!
|
|
17
17
|
|
|
18
|
-
JAVASCRIPT_PATH = root.join("app/javascript")
|
|
18
|
+
JAVASCRIPT_PATH = root.join("app/javascript").freeze
|
|
19
19
|
|
|
20
20
|
initializer "docs_kit.controller_helper" do
|
|
21
21
|
ActiveSupport.on_load(:action_controller_base) do
|
|
@@ -31,6 +31,19 @@ module DocsKit
|
|
|
31
31
|
Mime::Type.register("text/markdown", :md) unless Mime::Type.lookup_by_extension(:md)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Resolve the configuration's per-render lookups once (DocsKit.finalize!)
|
|
35
|
+
# after the host has configured the kit: its to_prepare DocsKit.configure
|
|
36
|
+
# runs before after_initialize at boot. With code reloading on, every
|
|
37
|
+
# reload re-runs that configure (which re-opens the configuration) against
|
|
38
|
+
# the reloaded classes, so finalize again after it: a to_prepare added
|
|
39
|
+
# here, after boot, runs after every to_prepare the host registered.
|
|
40
|
+
initializer "docs_kit.finalize" do |app|
|
|
41
|
+
app.config.after_initialize do
|
|
42
|
+
DocsKit.finalize!
|
|
43
|
+
app.reloader.to_prepare { DocsKit.finalize! } if app.config.reloading_enabled?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
34
47
|
# Serve the bundled Stimulus controller (docs_nav) as an asset.
|
|
35
48
|
initializer "docs_kit.assets" do |app|
|
|
36
49
|
app.config.assets.paths << JAVASCRIPT_PATH.to_s if app.config.respond_to?(:assets)
|
|
@@ -106,7 +106,9 @@ module DocsKit
|
|
|
106
106
|
|
|
107
107
|
port = free_port
|
|
108
108
|
app = Rails.application
|
|
109
|
-
|
|
109
|
+
# The docs_kit:og rake task's one-shot local server (a CLI, never a request
|
|
110
|
+
# path): one thread, killed in the ensure below.
|
|
111
|
+
server_thread = Thread.new do # rubocop:disable ThreadSafety/NewThread
|
|
110
112
|
Rackup::Handler.get("webrick").run(app, Port: port, Host: "127.0.0.1", Logger: null_logger, AccessLog: [])
|
|
111
113
|
rescue LoadError
|
|
112
114
|
Rack::Handler.get("webrick").run(app, Port: port, Host: "127.0.0.1", Logger: null_logger, AccessLog: [])
|
data/lib/docs_kit/registry.rb
CHANGED
|
@@ -62,34 +62,44 @@ module DocsKit
|
|
|
62
62
|
# given. Appends to the registry in declaration order (== sidebar order).
|
|
63
63
|
#
|
|
64
64
|
# page "Getting started", group: "Guide", icon: "rocket", slug: "start", view: "Start"
|
|
65
|
+
#
|
|
66
|
+
# The DSL runs in the registry's class body, at load. Each value is made
|
|
67
|
+
# Ractor-shareable on write (the page list is replaced, never appended to
|
|
68
|
+
# in place), so a non-main Ractor can read a registry and no thread can
|
|
69
|
+
# mutate it afterwards.
|
|
65
70
|
def page(title, group:, slug: nil, view: nil, icon: nil)
|
|
66
71
|
raise Error, "cannot mix `page` and `entries` in one registry" if defined?(@entries) && @entries
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
entry = {
|
|
69
74
|
title: title,
|
|
70
75
|
group: group,
|
|
71
76
|
slug: slug || title.parameterize,
|
|
72
77
|
view: view || title.parameterize(separator: "_").camelize,
|
|
73
78
|
icon: icon
|
|
74
|
-
}
|
|
79
|
+
}
|
|
80
|
+
@pages = DocsKit::Shareable.make([*(defined?(@pages) && @pages), entry])
|
|
75
81
|
end
|
|
76
82
|
|
|
77
83
|
# href = "#{path_prefix}/#{slug}". Defaults to "/docs".
|
|
78
84
|
def path_prefix(value = nil)
|
|
79
|
-
@path_prefix = value if value
|
|
85
|
+
@path_prefix = DocsKit::Shareable.make(value) if value
|
|
80
86
|
@path_prefix || "/docs"
|
|
81
87
|
end
|
|
82
88
|
|
|
83
89
|
# The namespace a page's view_class resolves under (v2 pages only), e.g.
|
|
84
90
|
# "Views::Docs::Pages". Required to resolve views via the default Entry.
|
|
91
|
+
# Setting it drops the views #finalize! resolved under the old namespace.
|
|
85
92
|
def view_namespace(value = nil)
|
|
86
|
-
|
|
93
|
+
if value
|
|
94
|
+
@view_namespace = DocsKit::Shareable.make(value)
|
|
95
|
+
@view_classes = nil
|
|
96
|
+
end
|
|
87
97
|
@view_namespace
|
|
88
98
|
end
|
|
89
99
|
|
|
90
100
|
# The attribute used by #grouped (default :group).
|
|
91
101
|
def group_by_attribute(attr = nil)
|
|
92
|
-
@group_by_attribute = attr if attr
|
|
102
|
+
@group_by_attribute = DocsKit::Shareable.make(attr) if attr
|
|
93
103
|
@group_by_attribute || :group
|
|
94
104
|
end
|
|
95
105
|
|
|
@@ -98,12 +108,32 @@ module DocsKit
|
|
|
98
108
|
# v2 pages are wrapped in the default Entry; hash entries in the site's class.
|
|
99
109
|
def all
|
|
100
110
|
if defined?(@pages) && @pages
|
|
101
|
-
@
|
|
111
|
+
views = @view_classes || Entry::NO_VIEWS
|
|
112
|
+
@pages.map { |attrs| Entry.new(attrs, path_prefix, view_namespace, views) }
|
|
102
113
|
else
|
|
103
114
|
(entries || []).map { |entry| new(entry) }
|
|
104
115
|
end
|
|
105
116
|
end
|
|
106
117
|
|
|
118
|
+
# Resolves every `page`'s view class once (DocsKit.finalize!, at boot) into
|
|
119
|
+
# a frozen { view name => class } map the entries read instead of
|
|
120
|
+
# constantizing on every call. Only views that resolve are kept, so a page
|
|
121
|
+
# written later still turns up. An `entries` registry resolves its own views.
|
|
122
|
+
def finalize!
|
|
123
|
+
resolved = Array(defined?(@pages) && @pages).each_with_object({}) do |attrs, map|
|
|
124
|
+
view_class = Entry.resolve(view_namespace, attrs[:view])
|
|
125
|
+
map[attrs[:view]] = view_class if view_class
|
|
126
|
+
end
|
|
127
|
+
@view_classes = DocsKit::Shareable.make(resolved)
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Drops the resolved view classes: entries constantize again (DocsKit.configure).
|
|
132
|
+
def reopen!
|
|
133
|
+
@view_classes = nil
|
|
134
|
+
self
|
|
135
|
+
end
|
|
136
|
+
|
|
107
137
|
# The instance whose slug matches, or nil.
|
|
108
138
|
def from_slug(slug)
|
|
109
139
|
all.find { |item| item.slug.to_s == slug.to_s }
|
|
@@ -129,15 +159,26 @@ module DocsKit
|
|
|
129
159
|
# resolved under the registry's view_namespace (nil until the class exists,
|
|
130
160
|
# preserving the no-dead-links behavior).
|
|
131
161
|
class Entry
|
|
162
|
+
NO_VIEWS = {}.freeze
|
|
163
|
+
|
|
164
|
+
# The page class `view_name` names under `view_namespace`, or nil.
|
|
165
|
+
def self.resolve(view_namespace, view_name)
|
|
166
|
+
return unless view_namespace
|
|
167
|
+
|
|
168
|
+
"#{view_namespace}::#{view_name}".safe_constantize
|
|
169
|
+
end
|
|
170
|
+
|
|
132
171
|
attr_reader :slug, :title, :group, :icon, :view_name, :href
|
|
133
172
|
|
|
134
|
-
|
|
173
|
+
# view_classes: the registry's finalized { view name => class } map.
|
|
174
|
+
def initialize(attrs, path_prefix, view_namespace, view_classes = NO_VIEWS)
|
|
135
175
|
@slug = attrs[:slug]
|
|
136
176
|
@title = attrs[:title]
|
|
137
177
|
@group = attrs[:group]
|
|
138
178
|
@icon = attrs[:icon]
|
|
139
179
|
@view_name = attrs[:view]
|
|
140
180
|
@view_namespace = view_namespace
|
|
181
|
+
@view_classes = view_classes
|
|
141
182
|
@href = "#{path_prefix}/#{@slug}"
|
|
142
183
|
end
|
|
143
184
|
|
|
@@ -145,7 +186,7 @@ module DocsKit
|
|
|
145
186
|
def view_class
|
|
146
187
|
return unless @view_namespace
|
|
147
188
|
|
|
148
|
-
|
|
189
|
+
@view_classes.fetch(@view_name) { self.class.resolve(@view_namespace, @view_name) }
|
|
149
190
|
end
|
|
150
191
|
|
|
151
192
|
# The renderable instance for this page (nil when unauthored) — the seam
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
# Makes a boot-time value Ractor-shareable: Ractor.make_shareable (a deep
|
|
5
|
+
# freeze; a capture-free Proc becomes an isolated one) where the engine has
|
|
6
|
+
# Ractors, a plain #freeze where it has none (TruffleRuby, JRuby). For
|
|
7
|
+
# constants and DSL values written once at load, so a non-main Ractor can read
|
|
8
|
+
# them and no thread can mutate them afterwards.
|
|
9
|
+
module Shareable
|
|
10
|
+
def self.make(value)
|
|
11
|
+
defined?(::Ractor) && ::Ractor.respond_to?(:make_shareable) ? ::Ractor.make_shareable(value) : value.freeze
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/docs_kit/snapshot.rb
CHANGED
|
@@ -28,27 +28,41 @@ module DocsKit
|
|
|
28
28
|
# invalidated when manifest.json's mtime changes — the same
|
|
29
29
|
# reload-on-change posture as Configuration#openapi_document, so editing
|
|
30
30
|
# a snapshot in development is picked up without a server restart.
|
|
31
|
+
#
|
|
32
|
+
# The cache is per Ractor (DocsKit::Store) and its check-then-set runs
|
|
33
|
+
# under that Ractor's lock, so threads that miss a cold key together
|
|
34
|
+
# build it once and all get the same instance.
|
|
31
35
|
def for(version, config: DocsKit.configuration)
|
|
32
36
|
version = DocVersion.from(version)
|
|
33
37
|
root = root_for(version, config)
|
|
34
38
|
mtime = manifest_mtime(root)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@cache ||= {}
|
|
38
|
-
cached = @cache[key]
|
|
39
|
-
return cached.fetch(:snapshot) if cached && cached.fetch(:mtime) == mtime
|
|
40
|
-
|
|
41
|
-
new(version: version, root: root).tap do |snapshot|
|
|
42
|
-
@cache[key] = { snapshot: snapshot, mtime: mtime }
|
|
43
|
-
end
|
|
39
|
+
memoize([version.id.to_s, root.to_s], mtime) { new(version: version, root: root) }
|
|
44
40
|
end
|
|
45
41
|
|
|
46
42
|
def reset_cache!
|
|
47
|
-
|
|
43
|
+
cache = cache_store
|
|
44
|
+
cache.fetch(:lock).synchronize { cache.fetch(:entries).clear }
|
|
48
45
|
end
|
|
49
46
|
|
|
50
47
|
private
|
|
51
48
|
|
|
49
|
+
def cache_store
|
|
50
|
+
DocsKit::Store.fetch(:docs_kit_snapshots) { { lock: Mutex.new, entries: {} } }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The cached snapshot for key while its manifest mtime is unchanged, else
|
|
54
|
+
# the block's fresh one (stored) — the whole check-then-set under the lock.
|
|
55
|
+
def memoize(key, mtime)
|
|
56
|
+
cache = cache_store
|
|
57
|
+
cache.fetch(:lock).synchronize do
|
|
58
|
+
entries = cache.fetch(:entries)
|
|
59
|
+
cached = entries[key]
|
|
60
|
+
next cached.fetch(:snapshot) if cached && cached.fetch(:mtime) == mtime
|
|
61
|
+
|
|
62
|
+
yield.tap { |snapshot| entries[key] = { snapshot: snapshot, mtime: mtime } }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
52
66
|
# <snapshots_path>/<id>, or nil when no snapshots path resolves (no
|
|
53
67
|
# config, no Rails) — which reads back as an empty snapshot.
|
|
54
68
|
def root_for(version, config)
|
|
@@ -70,10 +84,13 @@ module DocsKit
|
|
|
70
84
|
|
|
71
85
|
attr_reader :version, :root
|
|
72
86
|
|
|
87
|
+
# Built whole here, never lazily: a cached Snapshot is shared by every
|
|
88
|
+
# server thread, so it carries no memo a reader could race to fill.
|
|
73
89
|
def initialize(version:, root:)
|
|
74
90
|
@version = version
|
|
75
91
|
@root = root
|
|
76
92
|
@manifest = read_manifest
|
|
93
|
+
@registries = build_registries
|
|
77
94
|
end
|
|
78
95
|
|
|
79
96
|
# Every snapshot page across the manifest's registries, in manifest order —
|
|
@@ -116,8 +133,10 @@ module DocsKit
|
|
|
116
133
|
private
|
|
117
134
|
|
|
118
135
|
# The manifest's registries as { heading:, entries: [Snapshot::Entry] }.
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
attr_reader :registries
|
|
137
|
+
|
|
138
|
+
def build_registries
|
|
139
|
+
Array(@manifest && @manifest["registries"]).map do |registry|
|
|
121
140
|
prefix = registry["path_prefix"] || "/docs"
|
|
122
141
|
{
|
|
123
142
|
heading: registry["heading"],
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
# Per-Ractor lazy storage for docs-kit's caches: `Store.fetch(:docs_kit_x) { {} }`.
|
|
5
|
+
# A copy of conformance-kit's ConformanceKit::Store (MIT, same author): the kit
|
|
6
|
+
# is a development dependency only, so the gem carries its own.
|
|
7
|
+
#
|
|
8
|
+
# On Ruby 3.4+ this is `Ractor.store_if_absent`, atomic and per-Ractor. Below
|
|
9
|
+
# 3.4 (and on engines without Ractor) the main Ractor uses one Hash guarded by
|
|
10
|
+
# a Mutex, created at load (never lazily, so there is no check-then-set race);
|
|
11
|
+
# it is allowlisted main-only in .conformance.yml. A non-main Ractor on 3.3
|
|
12
|
+
# cannot read it, so it keeps its own values in Ractor-local storage instead.
|
|
13
|
+
# That path has no lock (none is shareable): threads of one Ractor that miss
|
|
14
|
+
# together may each build a value, but only the first one stored is kept and
|
|
15
|
+
# returned to all of them.
|
|
16
|
+
module Store
|
|
17
|
+
# rubocop:disable ThreadSafety/MutableClassInstanceVariable -- main-only fallback, guarded by @fallback_lock
|
|
18
|
+
@fallback_lock = Mutex.new
|
|
19
|
+
@fallback_values = {}
|
|
20
|
+
# rubocop:enable ThreadSafety/MutableClassInstanceVariable
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def fetch(key, &init)
|
|
24
|
+
raise ArgumentError, "DocsKit::Store.fetch(#{key.inspect}) needs an initializer block" unless init
|
|
25
|
+
return ::Ractor.store_if_absent(key, &init) if native?
|
|
26
|
+
return ractor_local(key, &init) unless main_ractor?
|
|
27
|
+
|
|
28
|
+
@fallback_lock.synchronize do
|
|
29
|
+
@fallback_values.fetch(key) { @fallback_values[key] = yield }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def main_ractor? = !defined?(::Ractor) || ::Ractor.current == ::Ractor.main
|
|
36
|
+
|
|
37
|
+
# Boxed, so a nil or false value is cached rather than rebuilt on every fetch.
|
|
38
|
+
# The value is built before the check-then-store, which runs no Ruby code
|
|
39
|
+
# between the read and the write, so a thread that built a value after
|
|
40
|
+
# another stored one gets the stored one.
|
|
41
|
+
def ractor_local(key)
|
|
42
|
+
slot = :"docs_kit_store/#{key}"
|
|
43
|
+
stored = ::Ractor.current[slot]
|
|
44
|
+
return stored.first if stored
|
|
45
|
+
|
|
46
|
+
built = [yield]
|
|
47
|
+
(::Ractor.current[slot] ||= built).first
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
public
|
|
51
|
+
|
|
52
|
+
def native?
|
|
53
|
+
defined?(::Ractor) ? ::Ractor.respond_to?(:store_if_absent) : false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/docs_kit/version.rb
CHANGED
data/lib/docs_kit.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "rouge"
|
|
|
5
5
|
require "zeitwerk"
|
|
6
6
|
|
|
7
7
|
require_relative "docs_kit/version"
|
|
8
|
+
require_relative "docs_kit/shareable"
|
|
8
9
|
require_relative "docs_kit/configuration"
|
|
9
10
|
|
|
10
11
|
# DocsKit — a reusable docs-site component library: the shared Phlex chrome
|
|
@@ -17,19 +18,36 @@ require_relative "docs_kit/configuration"
|
|
|
17
18
|
module DocsKit
|
|
18
19
|
class Error < StandardError; end
|
|
19
20
|
|
|
21
|
+
# Built at load, never lazily: no first-request race to build two.
|
|
22
|
+
@configuration = Configuration.new
|
|
23
|
+
|
|
20
24
|
class << self
|
|
21
|
-
# The current configuration
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
# The current configuration. Reset with DocsKit.reset_configuration!.
|
|
26
|
+
# Assigned at load and by reset_configuration! (boot, specs) on the main
|
|
27
|
+
# Ractor; request threads only read it. Main-only (.conformance.yml): it
|
|
28
|
+
# holds the host's per-request callables (brand, nav, version_badge, ...).
|
|
29
|
+
attr_reader :configuration
|
|
25
30
|
|
|
31
|
+
# Re-opens a finalized configuration first (a development reload re-runs
|
|
32
|
+
# the host's to_prepare configure), so every setting takes effect.
|
|
26
33
|
def configure
|
|
34
|
+
configuration.reopen!
|
|
27
35
|
yield(configuration)
|
|
28
36
|
configuration.validate!
|
|
29
37
|
end
|
|
30
38
|
|
|
39
|
+
# Resolves the configuration's per-render lookups once (see
|
|
40
|
+
# Configuration#finalize!). In a Rails app the engine calls it after the
|
|
41
|
+
# host has configured the kit, and again after every code reload; outside
|
|
42
|
+
# Rails, call it once the configuration is complete.
|
|
43
|
+
def finalize!
|
|
44
|
+
configuration.finalize!
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A fresh default configuration (specs; a host that re-boots its config).
|
|
48
|
+
# Runs at boot or between examples, never on a request.
|
|
31
49
|
def reset_configuration!
|
|
32
|
-
@configuration = Configuration.new
|
|
50
|
+
@configuration = Configuration.new # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
33
51
|
end
|
|
34
52
|
end
|
|
35
53
|
end
|
|
@@ -54,6 +72,8 @@ loader.push_dir(File.expand_path("docs_kit", __dir__), namespace: DocsKit)
|
|
|
54
72
|
loader.push_dir(File.expand_path("../app/components/docs_ui", __dir__), namespace: DocsUI)
|
|
55
73
|
loader.ignore(File.expand_path("docs_kit/version.rb", __dir__))
|
|
56
74
|
loader.ignore(File.expand_path("docs_kit/configuration.rb", __dir__))
|
|
75
|
+
# Required eagerly above (configuration.rb's constants use it at load).
|
|
76
|
+
loader.ignore(File.expand_path("docs_kit/shareable.rb", __dir__))
|
|
57
77
|
# Required eagerly by configuration.rb (a plain-Ruby value object, no Rails), so
|
|
58
78
|
# ignore it here too or zeitwerk double-manages the constant.
|
|
59
79
|
loader.ignore(File.expand_path("docs_kit/seo_config.rb", __dir__))
|
|
@@ -14,14 +14,19 @@ module DocsKit
|
|
|
14
14
|
# and hands back strings for whatever needs a human. A `nil` return means
|
|
15
15
|
# "nothing to warn about".
|
|
16
16
|
class Migration
|
|
17
|
-
attr_reader :
|
|
17
|
+
attr_reader :description
|
|
18
18
|
|
|
19
|
+
# Keeps `to` as a frozen String, so a migration list can be made
|
|
20
|
+
# Ractor-shareable without freezing Gem::Version's process-wide cached
|
|
21
|
+
# instances; #to hands back the Gem::Version.
|
|
19
22
|
def initialize(to:, description:, &block)
|
|
20
|
-
@to =
|
|
23
|
+
@to = -to.to_s
|
|
21
24
|
@description = description
|
|
22
25
|
@block = block
|
|
23
26
|
end
|
|
24
27
|
|
|
28
|
+
def to = Gem::Version.new(@to)
|
|
29
|
+
|
|
25
30
|
# Run the transform against the site. Returns the (possibly empty) list of
|
|
26
31
|
# manual-cleanup warnings — never nil, so callers can flat-map safely.
|
|
27
32
|
def call(root, generator)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../../docs_kit/shareable"
|
|
3
4
|
require_relative "migration"
|
|
4
5
|
require_relative "migrations/v1_2_0"
|
|
5
6
|
require_relative "../../../docs_kit/version"
|
|
@@ -24,21 +25,25 @@ module DocsKit
|
|
|
24
25
|
# its transform in migrations/vX_Y_Z.rb.
|
|
25
26
|
class MigrationRegistry
|
|
26
27
|
def initialize(migrations = [])
|
|
27
|
-
@migrations = migrations.sort_by(&:to)
|
|
28
|
+
@migrations = migrations.sort_by(&:to).freeze
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
# The registry the install generator runs during `--sync
|
|
31
|
-
def self.default
|
|
32
|
-
@default ||= new(MIGRATIONS)
|
|
33
|
-
end
|
|
31
|
+
# The registry the install generator runs during `--sync` (DEFAULT, below).
|
|
32
|
+
def self.default = DEFAULT
|
|
34
33
|
|
|
35
34
|
# Ordered `Migration.new(to: "1.x.0", description: "...") { ... }` entries, one
|
|
36
35
|
# per release that changes config knobs, routes, templates, or toolchain.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
# Shareable: each block is capture-free (its self is this class).
|
|
37
|
+
MIGRATIONS = DocsKit::Shareable.make(
|
|
38
|
+
[
|
|
39
|
+
Migration.new(to: "1.2.0", description: Migrations::V1_2_0::DESCRIPTION) do |root, _generator|
|
|
40
|
+
Migrations::V1_2_0.call(root)
|
|
41
|
+
end
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Built at load from MIGRATIONS, never lazily (no first-call race).
|
|
46
|
+
DEFAULT = DocsKit::Shareable.make(new(MIGRATIONS))
|
|
42
47
|
|
|
43
48
|
# The migrations a site last synced at `from_version` still needs, ascending
|
|
44
49
|
# by version — those in `(from_version, upto]`. `upto` defaults to the
|
|
@@ -12,12 +12,14 @@ module DocsKit
|
|
|
12
12
|
# handed back as a warning, and a newer value is left alone.
|
|
13
13
|
module V1_2_0 # rubocop:disable Naming/ClassAndModuleCamelCase -- named after the release
|
|
14
14
|
DESCRIPTION = "docs-kit 1.2: Ruby >= 3.3, Bun 1.4.2, Tailwind 4.3 / daisyUI 5.7 floors, Node 24 deploy"
|
|
15
|
-
|
|
15
|
+
# A String: Gem::Version.new hands out process-wide cached instances,
|
|
16
|
+
# which a constant must not freeze. Compared via Gem::Version below.
|
|
17
|
+
MIN_RUBY = "3.3"
|
|
16
18
|
BUN = { from: "1.3.2", to: "1.4.2" }.freeze
|
|
17
19
|
PACKAGE_FLOORS = {
|
|
18
|
-
"tailwindcss" => { from: "^4.1.18", to: "^4.3.3" },
|
|
19
|
-
"@tailwindcss/cli" => { from: "^4.1.18", to: "^4.3.3" },
|
|
20
|
-
"daisyui" => { from: "^5.6.0", to: "^5.7.47" }
|
|
20
|
+
"tailwindcss" => { from: "^4.1.18", to: "^4.3.3" }.freeze,
|
|
21
|
+
"@tailwindcss/cli" => { from: "^4.1.18", to: "^4.3.3" }.freeze,
|
|
22
|
+
"daisyui" => { from: "^5.6.0", to: "^5.7.47" }.freeze
|
|
21
23
|
}.freeze
|
|
22
24
|
DEPLOY_CALL = %r{zoolutions/docs-kit/\.github/workflows/deploy\.yml@(\S+)}
|
|
23
25
|
|
|
@@ -112,7 +114,7 @@ module DocsKit
|
|
|
112
114
|
|
|
113
115
|
def below_min_ruby?(value)
|
|
114
116
|
version = version_of(value)
|
|
115
|
-
version ? version < MIN_RUBY : false
|
|
117
|
+
version ? version < Gem::Version.new(MIN_RUBY) : false
|
|
116
118
|
end
|
|
117
119
|
end
|
|
118
120
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: docs-kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -235,9 +235,11 @@ files:
|
|
|
235
235
|
- lib/docs_kit/search_index.rb
|
|
236
236
|
- lib/docs_kit/search_index/snippet.rb
|
|
237
237
|
- lib/docs_kit/seo_config.rb
|
|
238
|
+
- lib/docs_kit/shareable.rb
|
|
238
239
|
- lib/docs_kit/shortcut.rb
|
|
239
240
|
- lib/docs_kit/snapshot.rb
|
|
240
241
|
- lib/docs_kit/snapshot/entry.rb
|
|
242
|
+
- lib/docs_kit/store.rb
|
|
241
243
|
- lib/docs_kit/templates/new_site.rb
|
|
242
244
|
- lib/docs_kit/topbar_link.rb
|
|
243
245
|
- lib/docs_kit/version.rb
|