docs-kit 1.3.0 → 1.4.1
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 +214 -16
- data/README.md +98 -12
- 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/config/importmap.rb +12 -10
- data/lib/docs_kit/api_client.rb +22 -18
- data/lib/docs_kit/configuration.rb +80 -11
- data/lib/docs_kit/engine.rb +28 -4
- 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: 29ec90ecec4c794dcfd2f0dfa7254a02844ccafa76019303b2b2bdbaf87c62c3
|
|
4
|
+
data.tar.gz: 3b8e8d345e6f795d00f44cea26529c9433eeb9ebb60e36cd07ce07541eec0fd5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb1ef7f9d6c3673f48d965b3bdd71c85b38b0f7c053d492078046f8fd37b094a98cb92ace7b6b777e986e00fd1cd2be187f455f591ab23437a9da8dee4ac31a6
|
|
7
|
+
data.tar.gz: 573e0e3db675e201e7457c8adee993d69e239789d08a3019b89c2409dd1cd6ec154e37fc6977c1a40e95369118ea78fa815e0e1da48ba4d551c6375a81692063
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,100 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Upgrading from 1.4
|
|
6
|
+
|
|
7
|
+
Nothing to change for a site that registers the controller with
|
|
8
|
+
`eagerLoadControllersFrom("docs_kit/controllers", application)`, as the
|
|
9
|
+
generated `index.js` does: `docs-nav` still registers on every page. It is no
|
|
10
|
+
longer preloaded, so on docs pages the eager loader's `import()` fetches it
|
|
11
|
+
one request later instead of from the preload cache. To keep the preload, set
|
|
12
|
+
`config.docs_kit.preload = true` in `config/application.rb`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`config.docs_kit.controllers_under`** (default `"docs_kit/controllers"`)
|
|
17
|
+
sets the auto-pin's module namespace. `"controllers"` pins the controller as
|
|
18
|
+
`controllers/docs_nav_controller` (same asset), so the host's own loader for
|
|
19
|
+
`controllers` finds it, with no docs-kit line in `index.js`; with
|
|
20
|
+
`lazyLoadControllersFrom("controllers", application)` a page fetches it only
|
|
21
|
+
if it renders `data-controller="docs-nav"`. Set it in
|
|
22
|
+
`config/application.rb`. (#107)
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **`docs-nav` is no longer preloaded on every page.** The auto-pin took
|
|
27
|
+
importmap-rails' default `preload: true`, so in a host whose docs share the
|
|
28
|
+
application's import map every page, docs or not, carried a
|
|
29
|
+
`<link rel="modulepreload">` for the 24 KB controller, and a host `pin` with
|
|
30
|
+
`preload: false` could not override it (a `pin_all_from` directory expands
|
|
31
|
+
over plain pins). `config.docs_kit.preload` (default `false`) is now the
|
|
32
|
+
pin's `preload:`: `true` restores 1.4's behaviour, an entry-point name
|
|
33
|
+
(importmap-rails 2.x) preloads it only where that entry point is rendered.
|
|
34
|
+
(#107)
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **One documented registration.** The README and the pin file's header now
|
|
39
|
+
point at one "Registering the controller" section. The README no longer
|
|
40
|
+
recommends `lazyLoadControllersFrom("docs_kit/controllers", application)`,
|
|
41
|
+
which in a host with its own controllers requests
|
|
42
|
+
`docs_kit/controllers/<identifier>_controller` for every host
|
|
43
|
+
`data-controller` and logs a "Failed to autoload controller" error for each,
|
|
44
|
+
on every page. (#107)
|
|
45
|
+
|
|
46
|
+
## [1.4.0] — 2026-10-15
|
|
47
|
+
|
|
48
|
+
### Upgrading from 1.3
|
|
49
|
+
|
|
50
|
+
Nothing to change for a site whose initializer configures docs-kit in a
|
|
51
|
+
`to_prepare` block, as the generated one does. Three things now fail loudly
|
|
52
|
+
instead of later or never: a broken `brand_logo` raises at boot, in
|
|
53
|
+
development too; mutating a registry DSL or `DocsUI::Page` macro value in place
|
|
54
|
+
raises `FrozenError`; so does mutating one of docs-kit's constants.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- **A cold archived version is built once, however many requests ask.**
|
|
59
|
+
`DocsKit::Snapshot.for` cached snapshots in an unguarded class ivar, so
|
|
60
|
+
threads that missed a version together each read and parsed its
|
|
61
|
+
`manifest.json` and raced to store it. The cache is now per Ractor and its
|
|
62
|
+
check-then-set runs under a lock; `Configuration#openapi_document`'s
|
|
63
|
+
mtime-checked memo likewise. (#91)
|
|
64
|
+
- **No per-render constant lookups once the app has booted.** The engine now
|
|
65
|
+
calls `DocsKit.finalize!` once the host has configured the kit, with no app
|
|
66
|
+
code: the Rouge code themes and every registered page's view class are
|
|
67
|
+
resolved once into frozen maps instead of `Object.const_get` /
|
|
68
|
+
`safe_constantize` on every render, and the brand logo, SEO, landing and
|
|
69
|
+
optional-mcp memos are filled at boot rather than by whichever request comes
|
|
70
|
+
first. `DocsKit.configure` re-opens it, and a theme assigned directly
|
|
71
|
+
afterwards still wins. Outside Rails, call `DocsKit.finalize!` yourself. (#91)
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- **Finalized after boot in every environment, and after every reload.** The
|
|
76
|
+
engine finalizes in `after_initialize` in development too. With code
|
|
77
|
+
reloading on, it finalizes again after each reload, once the host's
|
|
78
|
+
`to_prepare` configure has re-run and re-opened the configuration, so the
|
|
79
|
+
resolved page views are the reloaded classes. The host's callables (`brand`,
|
|
80
|
+
`nav`, `version_badge`, `importmap`, ...) are kept as given, never frozen:
|
|
81
|
+
they may close over app state, so `DocsKit.configuration` stays
|
|
82
|
+
main-Ractor-only and finalizing never fails on one. (#91)
|
|
83
|
+
- **Boot-time values are frozen.** The registry DSL (`page`, `path_prefix`,
|
|
84
|
+
`view_namespace`, `group_by_attribute`) and `DocsUI::Page`'s `title`,
|
|
85
|
+
`eyebrow`, `description` and `on_page` macros deep-freeze what they are
|
|
86
|
+
given (Ractor-shareable), as do the gem's own constants, and
|
|
87
|
+
`DocsKit.configuration` is built at load rather than on first read. Code
|
|
88
|
+
that mutated one of those values in place after declaring it now raises
|
|
89
|
+
`FrozenError`. (#91)
|
|
90
|
+
- **A broken `brand_logo` fails at boot.** `DocsKit.finalize!` builds the logo,
|
|
91
|
+
so a missing file or non-SVG markup raises when the app boots, in every
|
|
92
|
+
environment, instead of on the first render. (#91)
|
|
93
|
+
- docs-kit is held to [conformance-kit](https://github.com/zoolutions/conformance-kit)
|
|
94
|
+
0.4 in CI: the rubocop-thread_safety cops, thread and fiber contention specs,
|
|
95
|
+
a Ruby 4.0 Ractor canary (search index, llms.txt, the sidebar tree) and
|
|
96
|
+
`conformance-audit --strict`, whose allowlist names Rails' and Thor's class
|
|
97
|
+
state on the engine, controllers and generators by `ancestor:` rule. (#91)
|
|
98
|
+
|
|
5
99
|
## [1.3.0] — 2026-10-13
|
|
6
100
|
|
|
7
101
|
### Fixed
|
|
@@ -42,7 +136,15 @@
|
|
|
42
136
|
`on_page` macros inherit from a parent page when unset. Pages using only the
|
|
43
137
|
class macros render byte-identically. (#95)
|
|
44
138
|
|
|
45
|
-
## [1.2.1] — 2026-09-30
|
|
139
|
+
## [1.2.1] — 2026-09-30
|
|
140
|
+
|
|
141
|
+
### Changed
|
|
142
|
+
|
|
143
|
+
- **daisyui 2.x is allowed.** The dependency is now `>= 1.2, < 3`. 1.2.0 was
|
|
144
|
+
tagged but never reached RubyGems (its release run failed a stale spec), so
|
|
145
|
+
1.2.1 is the first published 1.2 release.
|
|
146
|
+
|
|
147
|
+
## [1.2.0] — 2026-09-30 (tagged; never published to RubyGems)
|
|
46
148
|
|
|
47
149
|
### Upgrading from 1.1
|
|
48
150
|
|
|
@@ -77,9 +179,6 @@ bundle exec rspec
|
|
|
77
179
|
|
|
78
180
|
### Changed
|
|
79
181
|
|
|
80
|
-
- **daisyui 2.x is allowed.** The dependency is now `>= 1.2, < 3`. 1.2.0 was
|
|
81
|
-
tagged but never reached RubyGems (its release run failed a stale spec), so
|
|
82
|
-
1.2.1 is the first published 1.2 release.
|
|
83
182
|
- **The reusable deploy workflow runs on Node 24 actions.** `deploy.yml` now
|
|
84
183
|
uses `actions/checkout@v7`, `docker/setup-buildx-action@v4`,
|
|
85
184
|
`docker/login-action@v4`, `docker/build-push-action@v7` and
|
|
@@ -116,21 +215,68 @@ bundle exec rspec
|
|
|
116
215
|
`lexer:` still wins, and a block with neither (or an unguessable filename)
|
|
117
216
|
stays Ruby, so existing output is unchanged.
|
|
118
217
|
|
|
119
|
-
## [1.1.1]
|
|
218
|
+
## [1.1.1] — 2026-08-27
|
|
120
219
|
|
|
121
220
|
### Fixed
|
|
122
221
|
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
222
|
+
- **`/llms.txt` sends a public max-age** (`expires_in 300, public: true`), so
|
|
223
|
+
shared caches such as dash-proxy can store it; ETag revalidation is
|
|
224
|
+
unchanged. (#75)
|
|
225
|
+
|
|
226
|
+
## [1.1.0] — 2026-08-27
|
|
227
|
+
|
|
228
|
+
### Added
|
|
229
|
+
|
|
230
|
+
- **Docs versions foundation.** `c.versions` (`DocsKit::DocVersion`),
|
|
231
|
+
`c.repo_url` and `c.snapshots_path`, a request-scoped version
|
|
232
|
+
(`DocsKit::Scope`) and a snapshot content source. A site that configures no
|
|
233
|
+
versions renders unchanged. (#67)
|
|
234
|
+
|
|
235
|
+
### Changed
|
|
236
|
+
|
|
237
|
+
- **The reusable deploy workflow uses dash 4.** `deploy.yml` installs
|
|
238
|
+
`dash ~> 4.0` and runs `dash doctor` before deploying; the registry secret is
|
|
239
|
+
now `DASH_REGISTRY_PASSWORD` (`KAMAL_REGISTRY_PASSWORD` is still exported),
|
|
240
|
+
and the `docs-kit new` deploy template turns on dash-proxy compression,
|
|
241
|
+
caching and security headers. (#74)
|
|
242
|
+
|
|
243
|
+
### Fixed
|
|
244
|
+
|
|
245
|
+
- **Repointed at the `zoolutions` org.** The reusable deploy workflow, the
|
|
246
|
+
`docs-kit new` and install templates, the README and the gemspec metadata
|
|
247
|
+
now reference `zoolutions/docs-kit` (reusable-workflow `uses:` refs don't
|
|
248
|
+
follow the repo move's redirect). (#70)
|
|
249
|
+
|
|
250
|
+
## [1.0.8] — 2026-08-04
|
|
251
|
+
|
|
252
|
+
### Added
|
|
253
|
+
|
|
254
|
+
- **`c.brand_logo`.** A config-driven brand mark (`svg:`, `paths:`, `markup:`,
|
|
255
|
+
`file:` or `src:`) rendered by the new `DocsUI::Logo` in the topbar and
|
|
256
|
+
sidebar brand links, falling back to `c.brand`; `c.landing.logo` accepts the
|
|
257
|
+
same forms. `c.topbar_brand = :mobile_only` hides the topbar copy where the
|
|
258
|
+
sidebar brand is already visible. (#68)
|
|
259
|
+
|
|
260
|
+
### Changed
|
|
261
|
+
|
|
262
|
+
- **The sidebar's top-level heading is flattened.** With one nav heading it
|
|
263
|
+
is no longer rendered; with several, each is a static `menu-title` label and
|
|
264
|
+
only subgroups collapse. The landing doc index follows the same rule. (#66)
|
|
265
|
+
|
|
266
|
+
## [1.0.7] — 2026-08-03
|
|
267
|
+
|
|
268
|
+
### Added
|
|
269
|
+
|
|
270
|
+
- **Version-aware `--sync` migrations.** Install and `--sync` stamp the
|
|
271
|
+
initializer with `# docs-kit synced: vX.Y.Z`, and `--sync` runs the ordered
|
|
272
|
+
migrations between that version and the gem's, printing what it can't
|
|
273
|
+
automate. The migration registry ships empty in 1.0.x. (#56)
|
|
274
|
+
- **Two homes: `c.app_link`.** An opt-in `c.app_link = { href:, label: }`
|
|
275
|
+
renders a topbar link back to the hosting app, and the page masthead's link
|
|
276
|
+
now follows `c.brand_href` instead of the host's `root_path` and reads
|
|
277
|
+
"← Docs home". (#63)
|
|
278
|
+
|
|
279
|
+
## [1.0.6] — 2026-07-04
|
|
134
280
|
|
|
135
281
|
### Added
|
|
136
282
|
|
|
@@ -141,6 +287,11 @@ bundle exec rspec
|
|
|
141
287
|
site's asset pipeline). nil (the default) omits it, so it's backwards-compatible.
|
|
142
288
|
This closes the gap a mounted docs app hit — a hero with no way to show the
|
|
143
289
|
product's own logo — the first consumer to try `DocsUI::Landing` on a real site.
|
|
290
|
+
|
|
291
|
+
## [1.0.5] — 2026-07-04
|
|
292
|
+
|
|
293
|
+
### Added
|
|
294
|
+
|
|
144
295
|
- **`DocsUI::Landing` — a config-driven marketing landing page.** Every consuming
|
|
145
296
|
site (and this dogfood site) was hand-rolling a home page; now render
|
|
146
297
|
`DocsUI::Landing` and drive it from a new `c.landing` config block
|
|
@@ -154,6 +305,48 @@ bundle exec rspec
|
|
|
154
305
|
now renders it and the initializer documents `c.landing`. This is the first
|
|
155
306
|
landing pattern proven on a **mounted** docs app (a docs section inside a larger
|
|
156
307
|
Rails app whose `/` is already taken), contributed back from that use case.
|
|
308
|
+
|
|
309
|
+
## [1.0.4] — 2026-07-04
|
|
310
|
+
|
|
311
|
+
### Changed
|
|
312
|
+
|
|
313
|
+
- **rouge 5 is allowed.** The dependency is now `>= 4.0, < 6`; the `< 5` cap
|
|
314
|
+
was downgrading sites already on rouge 5. (#54)
|
|
315
|
+
|
|
316
|
+
### Fixed
|
|
317
|
+
|
|
318
|
+
- **`docs_kit:install --sync` no longer re-injects the commented `/mcp` route
|
|
319
|
+
scaffold** into a site that already draws live MCP routes. (#53)
|
|
320
|
+
|
|
321
|
+
## [1.0.3] — 2026-07-04
|
|
322
|
+
|
|
323
|
+
### Added
|
|
324
|
+
|
|
325
|
+
- **`docs_kit:install` templates a lean multi-stage `Dockerfile` and a
|
|
326
|
+
`.dockerignore`.** The `Dockerfile` is site-owned (skipped if present) and
|
|
327
|
+
carries a `# docs-kit Dockerfile vX.Y.Z` marker that `--sync` warns about
|
|
328
|
+
when it is older than the gem; `docs-kit new` uses the same template. (#52)
|
|
329
|
+
|
|
330
|
+
## [1.0.2] — 2026-07-03
|
|
331
|
+
|
|
332
|
+
### Fixed
|
|
333
|
+
|
|
334
|
+
- **SEO `og:image` 404.** The og:image tag pointed at the raw config path
|
|
335
|
+
(`https://site/og/og.png`), which isn't a served URL — Propshaft serves the
|
|
336
|
+
digested asset under `/assets`. A relative `og_image` is now resolved through
|
|
337
|
+
the site's asset pipeline (`image_url`) to the digested `/assets/og/og-<digest>.png`
|
|
338
|
+
URL, and the image is treated as **site content**: the gem ships none,
|
|
339
|
+
`c.seo.og_image` defaults to **nil** (unset → no og:image tag, never a 404), and
|
|
340
|
+
the `docs_kit:og` task writes into the *site's* `app/assets/images/`. Added a
|
|
341
|
+
booted-app integration test in the dogfood site
|
|
342
|
+
(`docs/test/integration/seo_meta_tags_test.rb`) that asserts og:image resolves
|
|
343
|
+
to a `/assets` URL that actually returns 200 — the coverage isolated component
|
|
344
|
+
specs can't provide.
|
|
345
|
+
|
|
346
|
+
## [1.0.1] — 2026-07-03
|
|
347
|
+
|
|
348
|
+
### Added
|
|
349
|
+
|
|
157
350
|
- **SEO + social sharing.** Every page now emits a complete SEO `<head>` —
|
|
158
351
|
meta description, Open Graph, Twitter Card, canonical, favicon, robots, and
|
|
159
352
|
theme-color — via the new `DocsUI::MetaTags` component, driven entirely by a
|
|
@@ -165,6 +358,11 @@ bundle exec rspec
|
|
|
165
358
|
Backwards-compatible: a site that sets no `c.seo` renders a valid minimal card
|
|
166
359
|
and its `<head>` is a strict superset of before. The install generator documents
|
|
167
360
|
`c.seo` and installs the task; `docs-kit new` reminds the owner to run it. (#48)
|
|
361
|
+
|
|
362
|
+
## [1.0.0] — 2026-07-03
|
|
363
|
+
|
|
364
|
+
### Added
|
|
365
|
+
|
|
168
366
|
- Release tooling matching the sibling gems (daisyui/phlex-reactive/pgbus): a
|
|
169
367
|
`rake release[X.Y.Z]` task (version bump → lockfile update → build-verify →
|
|
170
368
|
commit → push → GitHub Release; `pre`/`force` supported, `main`-only, clean-tree
|
data/README.md
CHANGED
|
@@ -124,7 +124,7 @@ generators:
|
|
|
124
124
|
|-------|---------------|-----|
|
|
125
125
|
| `ApplicationController#render_page` defined by hand | `DocsKit::Controller#render_page` is included (the generator injects `include DocsKit::Controller`) | Delete the method — keep the `include`. |
|
|
126
126
|
| `app/helpers/icon_helper.rb` | docs-kit renders icons via rails_icons (`DocsUI::Icon`) | Delete the file. |
|
|
127
|
-
| Hand-pinned docs-kit lines in `config/importmap.rb` | the engine auto-pins the `docs-nav` controller and its assets | Delete the manual `pin`/`pin_all_from` lines for docs-kit. |
|
|
127
|
+
| Hand-pinned docs-kit lines in `config/importmap.rb` | the engine auto-pins the `docs-nav` controller and its assets | Delete the manual `pin`/`pin_all_from` lines for docs-kit. A line kept only to change `preload:` or `under:` becomes `config.docs_kit.preload` or `config.docs_kit.controllers_under` in `config/application.rb` (see [Registering the controller](#registering-the-controller)). |
|
|
128
128
|
| `Dockerfile` stamped by an older docs-kit (`# docs-kit Dockerfile vX.Y.Z`) | docs-kit ships an optimized, multi-stage Dockerfile; a stale copy misses image-size wins | Diff yours against the current template (`lib/generators/docs_kit/install/templates/Dockerfile.tt` in the gem), adopt the changes or replace it. See [Upgrading your Dockerfile](#upgrading-your-dockerfile). |
|
|
129
129
|
| `app/assets/stylesheets/tailwind.sources.css` committed to git | `bin/build-css` regenerates it on every build with machine-specific absolute gem paths — the committed copy churns per machine/Ruby and no build consumes it. The generator adds the `.gitignore` entry, but gitignoring doesn't untrack an already-committed copy. | `git rm --cached app/assets/stylesheets/tailwind.sources.css` and commit. |
|
|
130
130
|
|
|
@@ -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
|
|
@@ -1096,12 +1123,74 @@ server round-trip:
|
|
|
1096
1123
|
of three placements, auto-hiding on short pages. No server-side knowledge of
|
|
1097
1124
|
the headings, no per-page wiring.
|
|
1098
1125
|
|
|
1126
|
+
### Registering the controller
|
|
1127
|
+
|
|
1128
|
+
The engine pins `docs-nav` into the host's import map as
|
|
1129
|
+
`docs_kit/controllers/docs_nav_controller`, with no `<link rel="modulepreload">`
|
|
1130
|
+
on any page. Register it in `app/javascript/controllers/index.js` one of two
|
|
1131
|
+
ways.
|
|
1132
|
+
|
|
1133
|
+
**Eager** — what `docs_kit:install` writes, next to the stock eager loader:
|
|
1134
|
+
|
|
1135
|
+
```js
|
|
1136
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
1137
|
+
eagerLoadControllersFrom("docs_kit/controllers", application)
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
It imports the controller on every page that loads the entry point, whether
|
|
1141
|
+
the page renders `docs-nav` or not. Right for a docs-only site, where every page
|
|
1142
|
+
renders the sidebar.
|
|
1143
|
+
|
|
1144
|
+
**Through the host's own loader** — pin `docs-nav` under the host's
|
|
1145
|
+
`controllers` namespace instead, and add no docs-kit line to `index.js`:
|
|
1146
|
+
|
|
1147
|
+
```ruby
|
|
1148
|
+
# config/application.rb
|
|
1149
|
+
config.docs_kit.controllers_under = "controllers"
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
The module becomes `controllers/docs_nav_controller` (same asset), so the
|
|
1153
|
+
host's existing `eagerLoadControllersFrom("controllers", application)` or
|
|
1154
|
+
`lazyLoadControllersFrom("controllers", application)` finds it by convention.
|
|
1155
|
+
With the lazy loader, a page fetches `docs-nav` only if it renders
|
|
1156
|
+
`data-controller="docs-nav"`: the setup for a host whose docs share an app with
|
|
1157
|
+
other pages. Drop an old `eagerLoadControllersFrom("docs_kit/controllers", ...)`
|
|
1158
|
+
line, which no longer matches anything. Don't keep a host file named
|
|
1159
|
+
`app/javascript/controllers/docs_nav_controller.js`: it has the same module
|
|
1160
|
+
name, and the host's `config/importmap.rb` is drawn after the engine's, so it
|
|
1161
|
+
replaces docs-kit's controller.
|
|
1162
|
+
|
|
1163
|
+
> **Don't** call `lazyLoadControllersFrom("docs_kit/controllers", application)`
|
|
1164
|
+
> in a host with its own controllers. A lazy loader imports
|
|
1165
|
+
> `<under>/<identifier>_controller` for *every* `data-controller` on the page
|
|
1166
|
+
> that isn't registered yet, so `data-controller="dropdown"` makes it request
|
|
1167
|
+
> `docs_kit/controllers/dropdown_controller`, which isn't in the map: one
|
|
1168
|
+
> "Failed to autoload controller" console error per host identifier, on every
|
|
1169
|
+
> page. A second lazy loader is only safe on a namespace that owns every
|
|
1170
|
+
> identifier on the page.
|
|
1171
|
+
|
|
1172
|
+
**Preloading.** `config.docs_kit.preload` (default `false`) is the pin's
|
|
1173
|
+
`preload:`. `true` puts a modulepreload for `docs-nav` on every page that renders
|
|
1174
|
+
the import map (docs-kit 1.4's behaviour). An entry-point name, such as
|
|
1175
|
+
`"docs"`, preloads it only on pages that render that entry point; it needs
|
|
1176
|
+
importmap-rails 2.x (1.x takes only `true`/`false`, and docs-kit doesn't check).
|
|
1177
|
+
|
|
1178
|
+
```ruby
|
|
1179
|
+
# config/application.rb — defaults shown
|
|
1180
|
+
config.docs_kit.preload = false # true | "docs" | %w[docs admin]
|
|
1181
|
+
config.docs_kit.controllers_under = "docs_kit/controllers"
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
importmap-rails draws the pin in its own initializer, before
|
|
1185
|
+
`config/initializers/` run, so set both in `config/application.rb`. With
|
|
1186
|
+
`config.docs_kit.auto_pin = false` neither applies: the host owns the pin.
|
|
1187
|
+
|
|
1099
1188
|
### A dedicated docs import map
|
|
1100
1189
|
|
|
1101
1190
|
By default the Shell loads the host's `application` entry point from
|
|
1102
1191
|
`Rails.application.importmap`, and the engine adds the `docs-nav` pin to that
|
|
1103
1192
|
map. A host with several import maps can give its docs their own entry point
|
|
1104
|
-
and map
|
|
1193
|
+
and map:
|
|
1105
1194
|
|
|
1106
1195
|
```ruby
|
|
1107
1196
|
# config/application.rb — skip the engine's auto-pin into the shared map
|
|
@@ -1123,14 +1212,17 @@ pin "docs"
|
|
|
1123
1212
|
pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
|
1124
1213
|
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
|
1125
1214
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
|
1215
|
+
# This map only loads on docs pages, so preloading docs-nav here is the point.
|
|
1126
1216
|
pin_all_from DocsKit::Engine.root.join("app/javascript/docs_kit/controllers"),
|
|
1127
|
-
under: "docs_kit/controllers", to: "docs_kit/controllers"
|
|
1217
|
+
under: "docs_kit/controllers", to: "docs_kit/controllers", preload: true
|
|
1128
1218
|
```
|
|
1129
1219
|
|
|
1130
1220
|
`c.importmap` takes an `Importmap::Map` or a callable returning one; a callable
|
|
1131
1221
|
is resolved on every render, so a map rebuilt on code reload is picked up. The
|
|
1132
|
-
`docs` entry point registers the
|
|
1133
|
-
|
|
1222
|
+
`docs` entry point registers the controller eagerly, as in
|
|
1223
|
+
[Registering the controller](#registering-the-controller). With one shared map,
|
|
1224
|
+
a `docs` entry point and `config.docs_kit.preload = "docs"` preload `docs-nav`
|
|
1225
|
+
on docs pages only, without a second map.
|
|
1134
1226
|
|
|
1135
1227
|
### On this page (auto-TOC)
|
|
1136
1228
|
|
|
@@ -1156,13 +1248,7 @@ headings, so they need zero per-page data. A page with fewer than 2 sections
|
|
|
1156
1248
|
hides the TOC. Scroll-spy highlights the section you're reading.
|
|
1157
1249
|
|
|
1158
1250
|
`DocsUI::Sidebar` already carries `data-controller="docs-nav"`. Register the
|
|
1159
|
-
controller in the
|
|
1160
|
-
|
|
1161
|
-
```js
|
|
1162
|
-
// app/javascript/controllers/index.js
|
|
1163
|
-
import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
1164
|
-
lazyLoadControllersFrom("docs_kit/controllers", application)
|
|
1165
|
-
```
|
|
1251
|
+
controller as in [Registering the controller](#registering-the-controller).
|
|
1166
1252
|
|
|
1167
1253
|
The **active nav item** needs no JS — it's server-rendered from the request path.
|
|
1168
1254
|
|
|
@@ -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/config/importmap.rb
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Auto-pins the gem's bundled Stimulus
|
|
4
|
-
#
|
|
3
|
+
# Auto-pins the gem's bundled Stimulus controller (docs-nav) for importmap-rails
|
|
4
|
+
# consumers. With the defaults it is `docs_kit/controllers/docs_nav_controller`,
|
|
5
|
+
# preloaded nowhere. Two engine options, set in the host's config/application.rb,
|
|
6
|
+
# shape the pin:
|
|
5
7
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# eagerLoadControllersFrom("docs_kit/controllers", application)
|
|
8
|
+
# config.docs_kit.preload = false # true, or an entry point ("docs")
|
|
9
|
+
# config.docs_kit.controllers_under = "docs_kit/controllers" # "controllers" rides the host's lazy loader
|
|
9
10
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
11
|
+
# How to register the controller in the host's controllers/index.js, and which
|
|
12
|
+
# loader to avoid: README → "Registering the controller".
|
|
13
|
+
docs_kit = DocsKit::Engine.config.docs_kit
|
|
13
14
|
pin_all_from DocsKit::Engine.root.join("app/javascript/docs_kit/controllers"),
|
|
14
|
-
under:
|
|
15
|
-
to: "docs_kit/controllers"
|
|
15
|
+
under: docs_kit.controllers_under,
|
|
16
|
+
to: "docs_kit/controllers",
|
|
17
|
+
preload: docs_kit.preload
|
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,16 +31,40 @@ 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)
|
|
37
50
|
end
|
|
38
51
|
|
|
39
|
-
# Host-facing engine options
|
|
40
|
-
#
|
|
41
|
-
#
|
|
52
|
+
# Host-facing engine options, set in config/application.rb: importmap-rails
|
|
53
|
+
# draws the auto-pin in its own initializer, before config/initializers run.
|
|
54
|
+
# `config.docs_kit.auto_pin = false` keeps the gem's pin out of the host's
|
|
55
|
+
# shared import map, for a host that pins `docs_kit/controllers` in a
|
|
56
|
+
# dedicated docs map.
|
|
42
57
|
config.docs_kit = ActiveSupport::OrderedOptions.new
|
|
43
58
|
config.docs_kit.auto_pin = true
|
|
59
|
+
# The auto-pin's `preload:`. false (the default) keeps docs-nav out of every
|
|
60
|
+
# page's modulepreloads; true preloads it everywhere; an entry-point name (or
|
|
61
|
+
# an array, importmap-rails 2.x) preloads it only where that entry point loads.
|
|
62
|
+
config.docs_kit.preload = false
|
|
63
|
+
# The auto-pin's module namespace. "controllers" pins docs-nav as
|
|
64
|
+
# `controllers/docs_nav_controller`, so the host's own
|
|
65
|
+
# `lazyLoadControllersFrom("controllers", application)` loads it by convention
|
|
66
|
+
# on the pages that render it. The asset path never changes.
|
|
67
|
+
config.docs_kit.controllers_under = "docs_kit/controllers"
|
|
44
68
|
|
|
45
69
|
# Auto-pin the controller for importmap-rails consumers, so a host app gets
|
|
46
70
|
# `docs_kit/controllers/docs_nav_controller` with no manual pin.
|
|
@@ -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.1
|
|
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
|