docs-kit 1.0.5 → 1.0.7
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 +7 -0
- data/README.md +35 -4
- data/app/components/docs_ui/landing.rb +25 -4
- data/app/components/docs_ui/page.rb +8 -1
- data/app/components/docs_ui/shell.rb +18 -0
- data/lib/docs_kit/configuration.rb +18 -0
- data/lib/docs_kit/landing_config.rb +39 -0
- data/lib/docs_kit/version.rb +1 -1
- data/lib/generators/docs_kit/install/install_generator.rb +61 -0
- data/lib/generators/docs_kit/install/migration.rb +32 -0
- data/lib/generators/docs_kit/install/migration_registry.rb +57 -0
- data/lib/generators/docs_kit/install/templates/docs_kit.rb.erb +10 -0
- 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: fa41c41824768d1db2a5ad5a7ba9b983b0f81032c8b5a3f80277e2611e203449
|
|
4
|
+
data.tar.gz: 60cd31b54d83c9c6920ff48d27ec177f81b7a240fdf513e65f9617837cc6c738
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e23f649705ac6216e4961c389eab64965961023123937364e4915b608264bf994ec0885c1958929884449d4eb4f6d9202ed6a5fa7fb9dd373c6a2202e8850ea1
|
|
7
|
+
data.tar.gz: 0a047f0b8b45fdce21b9121b13461a5c003ec307586ef4b49bbb3ead0c4d79f44db00a243eda7792fc5be173f3b135195efad4212360527cb2ef5e143eafd5e5
|
data/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
|
|
19
19
|
### Added
|
|
20
20
|
|
|
21
|
+
- **`DocsUI::Landing` hero logo (`c.landing.logo`).** The landing hero now takes
|
|
22
|
+
an optional brand mark above the eyebrow, in two forms: an inline single-path
|
|
23
|
+
SVG (`{ svg: "<path d>", viewbox:, label: }`, rendered with `fill: currentColor`
|
|
24
|
+
so it adapts to the theme) or an image (`{ src:, alt: }`, resolved through the
|
|
25
|
+
site's asset pipeline). nil (the default) omits it, so it's backwards-compatible.
|
|
26
|
+
This closes the gap a mounted docs app hit — a hero with no way to show the
|
|
27
|
+
product's own logo — the first consumer to try `DocsUI::Landing` on a real site.
|
|
21
28
|
- **`DocsUI::Landing` — a config-driven marketing landing page.** Every consuming
|
|
22
29
|
site (and this dogfood site) was hand-rolling a home page; now render
|
|
23
30
|
`DocsUI::Landing` and drive it from a new `c.landing` config block
|
data/README.md
CHANGED
|
@@ -81,6 +81,31 @@ site-owned content (your `Doc` registry, your pages, your themed
|
|
|
81
81
|
`application.tailwind.css`). Drop `--sync` to also (re)scaffold missing content
|
|
82
82
|
files — Thor prompts before overwriting anything that exists.
|
|
83
83
|
|
|
84
|
+
### Version-aware migrations
|
|
85
|
+
|
|
86
|
+
Every install and `--sync` stamps the docs-kit version it ran into your config
|
|
87
|
+
initializer, as an inert comment on the first line:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# docs-kit synced: v1.0.5
|
|
91
|
+
# frozen_string_literal: true
|
|
92
|
+
Rails.application.config.to_prepare do
|
|
93
|
+
DocsKit.configure do |c|
|
|
94
|
+
# ...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
On the next `--sync` after a `bundle update docs-kit`, the generator reads that
|
|
98
|
+
stamp, works out the gap to the newly-installed version, and runs the **ordered
|
|
99
|
+
release-to-release migrations** in between — renamed config knobs, changed route
|
|
100
|
+
shapes, restructured templates — in sequence, then restamps to the new version.
|
|
101
|
+
A site created before the stamp existed (no comment) is treated as the earliest
|
|
102
|
+
version, so it gets every migration. Migrations are **warn-only-safe** exactly
|
|
103
|
+
like the drift report: what a step can't safely automate it prints as a manual
|
|
104
|
+
checklist (`migration steps to apply by hand:`), never a destructive rewrite of
|
|
105
|
+
a line you've edited. There are no migrations to apply yet — the mechanism ships
|
|
106
|
+
ahead of the first release that needs one, so the upgrade path is already in
|
|
107
|
+
place.
|
|
108
|
+
|
|
84
109
|
### One-time cleanup for sites created before these landed
|
|
85
110
|
|
|
86
111
|
`--sync` detects drift it can't safely automate and prints a checklist — it
|
|
@@ -132,11 +157,15 @@ diff Dockerfile "$(bundle show docs-kit)/lib/generators/docs_kit/install/templat
|
|
|
132
157
|
# config/initializers/docs_kit.rb
|
|
133
158
|
DocsKit.configure do |c|
|
|
134
159
|
c.brand = "phlex-reactive"
|
|
135
|
-
c.brand_href = "/docs" # brand link
|
|
160
|
+
c.brand_href = "/docs" # the DOCS home: brand, sidebar, and "← Docs home" masthead link (default "/")
|
|
136
161
|
c.title_suffix = "phlex-reactive"
|
|
137
162
|
c.themes = %w[dark light synthwave retro cyberpunk dracula night nord sunset]
|
|
138
163
|
c.version_badge = -> { "v#{Phlex::Reactive::VERSION}" } # optional
|
|
139
164
|
|
|
165
|
+
# Docs embedded in a bigger app? The way BACK to it — a labeled link rendered
|
|
166
|
+
# once in the topbar, right after the brand. Unset (default) renders nothing.
|
|
167
|
+
c.app_link = { href: "/", label: "Back to the app" }
|
|
168
|
+
|
|
140
169
|
# Repo/social links in the topbar (next to the theme switcher).
|
|
141
170
|
c.topbar_links = [
|
|
142
171
|
{ href: "https://github.com/you/phlex-reactive", label: "GitHub", icon: :github },
|
|
@@ -156,13 +185,15 @@ registry maps a heading to its authored pages (`Doc.nav_items`); a page that
|
|
|
156
185
|
isn't written yet is skipped, so there are no dead links. Register a page with
|
|
157
186
|
one line (see [Add a page](#add-a-page)) and it appears in the sidebar.
|
|
158
187
|
|
|
159
|
-
###
|
|
188
|
+
### The two homes, the brand link, and dark code themes
|
|
160
189
|
|
|
161
|
-
|
|
190
|
+
These knobs cover what sites used to shim by subclassing `DocsUI::Shell` or
|
|
191
|
+
overriding route helpers:
|
|
162
192
|
|
|
163
193
|
| Knob | Default | What it does |
|
|
164
194
|
|------|---------|--------------|
|
|
165
|
-
| `c.brand_href` | `"/"` | The href of the topbar brand link. Set it (e.g. `"/docs"`) instead of subclassing `Shell`
|
|
195
|
+
| `c.brand_href` | `"/"` | The **docs home** — the href of the topbar brand, the sidebar brand, and each page's "← Docs home" masthead link. Set it (e.g. `"/docs"`) when the docs live under a subpath, instead of subclassing `Shell` or overriding `root_path`. |
|
|
196
|
+
| `c.app_link` | `nil` | The **app home** — an opt-in `{ href:, label: }` link back to the application hosting the docs, rendered once in the topbar right after the brand (e.g. `{ href: "/", label: "Back to the app" }`). Unset renders nothing, so a standalone docs site is unchanged. External hrefs open in a new tab with `rel=noopener`. |
|
|
166
197
|
| `c.code_theme_dark` | `nil` | A second Rouge theme for **dark** daisyUI themes. `nil` keeps the single-theme behavior (fully backwards compatible). When set, `DocsUI::Code` also emits this theme's CSS scoped under `[data-theme=X] .code-highlight` for each shipped dark theme, so code blocks stay readable when the switcher flips to a dark theme. |
|
|
167
198
|
| `c.dark_themes` | daisyUI's built-in dark theme names | Which theme names count as dark for `code_theme_dark`. Intersected with `c.themes` at render time, so only shipped themes emit CSS. Override to name custom dark themes (e.g. `%w[zazu-dark]`). |
|
|
168
199
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module DocsUI
|
|
4
|
-
# The marketing landing page — a hero (
|
|
5
|
-
# snippet + CTA buttons), a feature-card grid, and a
|
|
6
|
-
# documentation index — rendered inside DocsUI::Shell. Every
|
|
7
|
-
# hand-rolling this; drive it from config instead:
|
|
4
|
+
# The marketing landing page — a hero (an optional brand logo + eyebrow + title +
|
|
5
|
+
# lead + optional install snippet + CTA buttons), a feature-card grid, and a
|
|
6
|
+
# registry-grouped documentation index — rendered inside DocsUI::Shell. Every
|
|
7
|
+
# consuming site was hand-rolling this; drive it from config instead:
|
|
8
8
|
#
|
|
9
9
|
# # config/initializers/docs_kit.rb
|
|
10
10
|
# DocsKit.configure do |c|
|
|
11
|
+
# c.landing.logo = { svg: "M4 2h9l5 5…Z", viewbox: "0 0 22 24", label: "Acme" }
|
|
11
12
|
# c.landing.eyebrow = "Developer Docs"
|
|
12
13
|
# c.landing.title = "Jobs & events on **Postgres**" # ** ** → primary color
|
|
13
14
|
# c.landing.lead = "PostgreSQL-native jobs + event bus for Rails."
|
|
@@ -30,6 +31,9 @@ module DocsUI
|
|
|
30
31
|
# walks the same #docs-content region Shell stamps.
|
|
31
32
|
class Landing < Phlex::HTML
|
|
32
33
|
include Phlex::Rails::Helpers::Request
|
|
34
|
+
# For the image-form hero logo (c.landing.logo = { src: … }): resolve the asset
|
|
35
|
+
# path through the site's pipeline to its digested /assets URL.
|
|
36
|
+
include Phlex::Rails::Helpers::ImageURL
|
|
33
37
|
|
|
34
38
|
def view_template
|
|
35
39
|
render DocsUI::Shell.new(title: landing.eyebrow || config.brand) do
|
|
@@ -50,6 +54,7 @@ module DocsUI
|
|
|
50
54
|
|
|
51
55
|
def hero
|
|
52
56
|
div(class: "flex flex-col gap-6") do
|
|
57
|
+
logo
|
|
53
58
|
eyebrow
|
|
54
59
|
heading
|
|
55
60
|
lead
|
|
@@ -58,6 +63,22 @@ module DocsUI
|
|
|
58
63
|
end
|
|
59
64
|
end
|
|
60
65
|
|
|
66
|
+
# The brand mark — an inline single-path SVG (currentColor, theme-adaptive) or
|
|
67
|
+
# an <img>. Rendered above the eyebrow, like a product wordmark.
|
|
68
|
+
def logo
|
|
69
|
+
return unless (mark = landing.hero_logo)
|
|
70
|
+
|
|
71
|
+
if mark.inline?
|
|
72
|
+
svg(viewbox: mark.viewbox, class: "h-9 w-auto text-primary", fill: "currentColor",
|
|
73
|
+
role: "img", aria_label: mark.label) do |s|
|
|
74
|
+
s.title { mark.label } if mark.label
|
|
75
|
+
s.path(d: mark.svg)
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
img(src: image_url(mark.src), alt: mark.alt.to_s, class: "h-9 w-auto")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
61
82
|
def eyebrow
|
|
62
83
|
return unless (text = landing.eyebrow)
|
|
63
84
|
|
|
@@ -65,7 +65,7 @@ module DocsUI
|
|
|
65
65
|
# "Markdown" action sits opposite "← Home"; it's chrome too, so it lives
|
|
66
66
|
# inside the skipped nav and never appears in the .md twin.
|
|
67
67
|
nav(class: "mb-6 flex items-center justify-between gap-4", data: { md_skip: true }) do
|
|
68
|
-
a(href:
|
|
68
|
+
a(href: home_href, class: "link link-hover text-sm opacity-70") { "← Docs home" }
|
|
69
69
|
render DocsUI::MarkdownAction.new(request.path) if markdown_action?
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -92,5 +92,12 @@ module DocsUI
|
|
|
92
92
|
def content
|
|
93
93
|
raise NotImplementedError, "#{self.class} must implement #content"
|
|
94
94
|
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# The masthead "← Docs home" target: config.brand_href (the DOCS home, like
|
|
99
|
+
# the topbar/sidebar brand links) — never the host's root_path helper, which
|
|
100
|
+
# on an app-embedded site is the application root, not the docs landing.
|
|
101
|
+
def home_href = DocsKit.configuration.brand_href
|
|
95
102
|
end
|
|
96
103
|
end
|
|
@@ -157,6 +157,7 @@ module DocsUI
|
|
|
157
157
|
label(for: DRAWER_ID, class: "btn btn-square btn-ghost btn-sm lg:hidden",
|
|
158
158
|
aria_label: "Open menu") { render DocsUI::Icon.new("menu", class: "size-5") }
|
|
159
159
|
a(href: config.brand_href, class: "btn btn-ghost text-lg font-bold") { config.brand }
|
|
160
|
+
app_home_link
|
|
160
161
|
end
|
|
161
162
|
render DocsUI::SearchBox.new if config.search_enabled?
|
|
162
163
|
div(class: "flex-none items-center") do
|
|
@@ -167,5 +168,22 @@ module DocsUI
|
|
|
167
168
|
end
|
|
168
169
|
end
|
|
169
170
|
end
|
|
171
|
+
|
|
172
|
+
# The opt-in App Home link (config.app_link) — the way back to the hosting
|
|
173
|
+
# app, rendered once, right after the brand. Nothing renders when unset, so
|
|
174
|
+
# the topbar stays byte-identical for a site that never configures it.
|
|
175
|
+
# External hrefs open in a new tab with rel=noopener (same posture as
|
|
176
|
+
# DocsUI::TopbarLinks); a site-relative href opens in place.
|
|
177
|
+
def app_home_link
|
|
178
|
+
link = config.app_link
|
|
179
|
+
return unless link
|
|
180
|
+
|
|
181
|
+
a(
|
|
182
|
+
href: link.href,
|
|
183
|
+
class: "link link-hover text-sm opacity-70",
|
|
184
|
+
target: (link.external? ? "_blank" : nil),
|
|
185
|
+
rel: (link.external? ? "noopener noreferrer" : nil)
|
|
186
|
+
) { link.label }
|
|
187
|
+
end
|
|
170
188
|
end
|
|
171
189
|
end
|
|
@@ -184,6 +184,15 @@ module DocsKit
|
|
|
184
184
|
# Read the effective map via #api_clients (which merges), never @api_clients.
|
|
185
185
|
attr_writer :api_clients
|
|
186
186
|
|
|
187
|
+
# The opt-in "App Home" link — the way back to the application that hosts
|
|
188
|
+
# the docs, rendered ONCE in the topbar right after the brand (e.g.
|
|
189
|
+
# "Back to the app" on a docs site embedded in a bigger app). A Hash
|
|
190
|
+
# ({ href:, label: }) or a DocsKit::TopbarLink; #app_link normalizes it.
|
|
191
|
+
# Defaults to nil → no link renders and the topbar is byte-identical to
|
|
192
|
+
# before. Distinct from #brand_href, which is the DOCS home (the brand,
|
|
193
|
+
# sidebar, and page-masthead links). Read via #app_link, never @app_link.
|
|
194
|
+
attr_writer :app_link
|
|
195
|
+
|
|
187
196
|
# External links rendered in the topbar next to the theme switcher — a repo
|
|
188
197
|
# link, a chat invite, a social profile. Each entry is a Hash
|
|
189
198
|
# ({ href:, label:, icon: }) or a DocsKit::TopbarLink; #topbar_links
|
|
@@ -262,10 +271,19 @@ module DocsKit
|
|
|
262
271
|
@api_base_url = "https://api.example.com"
|
|
263
272
|
@api_auth_header = nil
|
|
264
273
|
@api_clients = {}
|
|
274
|
+
@app_link = nil
|
|
265
275
|
@topbar_links = []
|
|
266
276
|
@openapi = nil
|
|
267
277
|
end
|
|
268
278
|
|
|
279
|
+
# The normalized App Home link (a DocsKit::TopbarLink), or nil when unset —
|
|
280
|
+
# absent config, absent link, exactly like every other opt-in knob.
|
|
281
|
+
def app_link
|
|
282
|
+
return if @app_link.nil?
|
|
283
|
+
|
|
284
|
+
DocsKit::TopbarLink.from(@app_link)
|
|
285
|
+
end
|
|
286
|
+
|
|
269
287
|
# The normalized topbar links (DocsKit::TopbarLink list), in declaration
|
|
270
288
|
# order. Each configured Hash/TopbarLink is coerced via TopbarLink.from, so
|
|
271
289
|
# the Shell only ever sees value objects. Blank/nil config yields [].
|
|
@@ -26,6 +26,15 @@ module DocsKit
|
|
|
26
26
|
# individually assignable in the `c.landing.x = ...` block, mirroring
|
|
27
27
|
# DocsKit::SeoConfig.
|
|
28
28
|
class LandingConfig
|
|
29
|
+
# An optional brand logo/mark rendered at the top of the hero (above the
|
|
30
|
+
# eyebrow), like a product wordmark. A Hash in one of two forms:
|
|
31
|
+
# { svg: "<path d …>", viewbox: "0 0 81 45", label: "Brand" } # inline mark
|
|
32
|
+
# { src: "logo.svg", alt: "Brand" } # image asset/URL
|
|
33
|
+
# The inline `svg` form renders with `fill: currentColor` so it adapts to the
|
|
34
|
+
# theme (light/dark) — best for a single-color mark. nil omits the logo.
|
|
35
|
+
# See #hero_logo (the normalized Logo value object) and DocsUI::Landing.
|
|
36
|
+
attr_writer :logo
|
|
37
|
+
|
|
29
38
|
# A small uppercase kicker above the title (e.g. "Developer Docs"). nil omits it.
|
|
30
39
|
attr_accessor :eyebrow
|
|
31
40
|
|
|
@@ -81,6 +90,13 @@ module DocsKit
|
|
|
81
90
|
{ code: attrs[:code].to_s, filename: attrs[:filename], lexer: (attrs[:lexer] || :shell).to_sym }
|
|
82
91
|
end
|
|
83
92
|
|
|
93
|
+
# The hero logo as a normalized Logo value object, or nil when unset.
|
|
94
|
+
def hero_logo
|
|
95
|
+
return if @logo.nil?
|
|
96
|
+
|
|
97
|
+
Logo.from(@logo)
|
|
98
|
+
end
|
|
99
|
+
|
|
84
100
|
# One hero call-to-action button. `style` maps to a daisyUI btn variant
|
|
85
101
|
# (:primary → btn-primary, anything else → btn-ghost). `icon` is an optional
|
|
86
102
|
# brand/lucide token rendered before the label (DocsUI::BrandMark resolves it).
|
|
@@ -117,5 +133,28 @@ module DocsKit
|
|
|
117
133
|
new(icon: attrs[:icon], title: attrs[:title], body: attrs[:body])
|
|
118
134
|
end
|
|
119
135
|
end
|
|
136
|
+
|
|
137
|
+
# The hero brand logo — either an inline single-path SVG mark (`svg` = the
|
|
138
|
+
# `<path d>` data, `viewbox` = its viewBox) rendered with fill: currentColor so
|
|
139
|
+
# it adapts to the theme, OR an image (`src` = an asset path/URL, `alt` = its
|
|
140
|
+
# accessible name). `label` is the accessible name for the inline mark.
|
|
141
|
+
Logo = Data.define(:svg, :viewbox, :src, :alt, :label) do
|
|
142
|
+
def initialize(svg: nil, viewbox: "0 0 24 24", src: nil, alt: nil, label: nil)
|
|
143
|
+
super
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.from(logo)
|
|
147
|
+
return logo if logo.is_a?(self)
|
|
148
|
+
|
|
149
|
+
attrs = logo.to_h.transform_keys(&:to_sym)
|
|
150
|
+
new(
|
|
151
|
+
svg: attrs[:svg], viewbox: attrs[:viewbox] || "0 0 24 24",
|
|
152
|
+
src: attrs[:src], alt: attrs[:alt], label: attrs[:label]
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# An inline SVG mark (vs. an <img>). True when `svg` path data is present.
|
|
157
|
+
def inline? = !svg.to_s.empty?
|
|
158
|
+
end
|
|
120
159
|
end
|
|
121
160
|
end
|
data/lib/docs_kit/version.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "erb"
|
|
|
4
4
|
require "yaml"
|
|
5
5
|
require "rails/generators/base"
|
|
6
6
|
require_relative "sync_report"
|
|
7
|
+
require_relative "migration_registry"
|
|
7
8
|
require_relative "../../../docs_kit/version"
|
|
8
9
|
|
|
9
10
|
module DocsKit
|
|
@@ -44,6 +45,18 @@ module DocsKit
|
|
|
44
45
|
AGENTS_END = "<!-- END docs-kit -->"
|
|
45
46
|
AGENTS_BLOCK_RE = /#{Regexp.escape(AGENTS_BEGIN)}.*#{Regexp.escape(AGENTS_END)}/m
|
|
46
47
|
|
|
48
|
+
# The config initializer — the one file every site has, so it carries the
|
|
49
|
+
# last-synced version stamp. See stamp_synced_version / run_migrations.
|
|
50
|
+
INITIALIZER = "config/initializers/docs_kit.rb"
|
|
51
|
+
|
|
52
|
+
# The inert comment recording which docs-kit version last synced this site.
|
|
53
|
+
# A future `--sync` reads it to run only the ordered migrations between that
|
|
54
|
+
# version and the gem's current one. Absent → the site predates the stamp
|
|
55
|
+
# (treated as the earliest version, so every migration applies).
|
|
56
|
+
SYNCED_STAMP_RE = /^#\s*docs-kit synced:\s*v(\d+\.\d+\.\d+)\s*$/
|
|
57
|
+
|
|
58
|
+
def self.synced_stamp(version = DocsKit::VERSION) = "# docs-kit synced: v#{version}"
|
|
59
|
+
|
|
47
60
|
# The RuboCop wiring docs-kit injects. REQUIRE loads the cops;
|
|
48
61
|
# INHERIT_GEM/INHERIT_PATH enable + scope them (see config/rubocop/docs_kit.yml).
|
|
49
62
|
RUBOCOP_REQUIRE = "docs_kit/rubocop"
|
|
@@ -308,6 +321,43 @@ module DocsKit
|
|
|
308
321
|
report.items.each { |item| say " • #{item}" }
|
|
309
322
|
end
|
|
310
323
|
|
|
324
|
+
# Run the ordered release-to-release migrations between the site's
|
|
325
|
+
# last-synced version and the gem's current one — the payoff of the version
|
|
326
|
+
# stamp. `--sync` only (a full install is a fresh scaffold, not an upgrade),
|
|
327
|
+
# and BEFORE stamp_synced_version restamps, so it reads the OLD version. An
|
|
328
|
+
# un-stamped site is treated as the earliest version (every migration runs).
|
|
329
|
+
# Migrations are warn-only-safe: what they can't automate they hand back as
|
|
330
|
+
# a checklist, printed like the drift report. The registry ships EMPTY at
|
|
331
|
+
# 1.0.x, so today this is a no-op that establishes the upgrade path.
|
|
332
|
+
def run_migrations
|
|
333
|
+
return unless options[:sync]
|
|
334
|
+
|
|
335
|
+
warnings = MigrationRegistry.default.migrate!(synced_version, destination_root, self)
|
|
336
|
+
return if warnings.empty?
|
|
337
|
+
|
|
338
|
+
say_status :warn, "migration steps to apply by hand:", :yellow
|
|
339
|
+
warnings.each { |item| say " • #{item}" }
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Record which docs-kit version this site is now synced at, so the NEXT
|
|
343
|
+
# `--sync` can run only the migrations after it. Injected as an inert
|
|
344
|
+
# comment at the top of the initializer (the one file every site has) —
|
|
345
|
+
# which create_initializer never rewrites, so stamping is its own step.
|
|
346
|
+
# Idempotent: updates a stale stamp in place, adds one when absent, and
|
|
347
|
+
# is a no-op when already current.
|
|
348
|
+
def stamp_synced_version
|
|
349
|
+
path = File.join(destination_root, INITIALIZER)
|
|
350
|
+
return unless File.exist?(path)
|
|
351
|
+
|
|
352
|
+
current = self.class.synced_stamp
|
|
353
|
+
source = File.read(path)
|
|
354
|
+
updated = source.match?(SYNCED_STAMP_RE) ? source.sub(SYNCED_STAMP_RE, current) : "#{current}\n#{source}"
|
|
355
|
+
return if updated == source
|
|
356
|
+
|
|
357
|
+
File.write(path, updated)
|
|
358
|
+
say_status :update, "#{INITIALIZER} (synced v#{DocsKit::VERSION})", :green
|
|
359
|
+
end
|
|
360
|
+
|
|
311
361
|
def show_post_install
|
|
312
362
|
return show_sync_summary if options[:sync]
|
|
313
363
|
|
|
@@ -328,6 +378,17 @@ module DocsKit
|
|
|
328
378
|
|
|
329
379
|
private
|
|
330
380
|
|
|
381
|
+
# The docs-kit version this site was last synced at, read from the
|
|
382
|
+
# initializer's stamp. Un-stamped (a site created before the stamp landed,
|
|
383
|
+
# or a fresh skeleton) → "0.0.0", the earliest version, so every migration
|
|
384
|
+
# applies. Read BEFORE stamp_synced_version overwrites it.
|
|
385
|
+
def synced_version
|
|
386
|
+
path = File.join(destination_root, INITIALIZER)
|
|
387
|
+
return "0.0.0" unless File.exist?(path)
|
|
388
|
+
|
|
389
|
+
File.read(path)[SYNCED_STAMP_RE, 1] || "0.0.0"
|
|
390
|
+
end
|
|
391
|
+
|
|
331
392
|
def show_sync_summary
|
|
332
393
|
say_status :info, "docs-kit synced.", :green
|
|
333
394
|
say <<~MSG
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
module Generators
|
|
5
|
+
# One ordered, versioned upgrade step between two docs-kit releases. `to` is
|
|
6
|
+
# the version the migration belongs to (the release that introduced the
|
|
7
|
+
# change); the registry runs it when a site's last-synced version is BELOW
|
|
8
|
+
# `to`. The block receives the site's `(destination_root, generator)` so it
|
|
9
|
+
# can read/rewrite files via the generator's helpers, and returns the list
|
|
10
|
+
# of manual-cleanup warnings it could NOT safely automate.
|
|
11
|
+
#
|
|
12
|
+
# Warn-only-safe by contract (the #24 drift pattern): a migration does what
|
|
13
|
+
# it can idempotently — never a destructive rewrite of a hand-edited line —
|
|
14
|
+
# and hands back strings for whatever needs a human. A `nil` return means
|
|
15
|
+
# "nothing to warn about".
|
|
16
|
+
class Migration
|
|
17
|
+
attr_reader :to, :description
|
|
18
|
+
|
|
19
|
+
def initialize(to:, description:, &block)
|
|
20
|
+
@to = Gem::Version.new(to.to_s)
|
|
21
|
+
@description = description
|
|
22
|
+
@block = block
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Run the transform against the site. Returns the (possibly empty) list of
|
|
26
|
+
# manual-cleanup warnings — never nil, so callers can flat-map safely.
|
|
27
|
+
def call(root, generator)
|
|
28
|
+
Array(@block.call(root, generator))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "migration"
|
|
4
|
+
require_relative "../../../docs_kit/version"
|
|
5
|
+
|
|
6
|
+
module DocsKit
|
|
7
|
+
module Generators
|
|
8
|
+
# The ordered set of release-to-release migrations `--sync` applies. Given a
|
|
9
|
+
# site's last-synced version, it selects the migrations the site hasn't run
|
|
10
|
+
# yet — those in the half-open range `(from_version, upto]`: ABOVE the site
|
|
11
|
+
# version (a migration AT the site version is already applied) and no newer
|
|
12
|
+
# than the installed gem — and runs them in ascending order, collecting the
|
|
13
|
+
# warn-only messages each couldn't safely automate.
|
|
14
|
+
#
|
|
15
|
+
# The `upto` ceiling (default: the installed gem version) matters because
|
|
16
|
+
# `--sync` restamps the site to DocsKit::VERSION afterward. A migration whose
|
|
17
|
+
# `to` sat ABOVE the gem version would then still exceed the new stamp and
|
|
18
|
+
# re-run on EVERY sync forever. It can't legitimately exist anyway (a site
|
|
19
|
+
# can't have "arrived" at a release it doesn't have), so it's filtered out.
|
|
20
|
+
#
|
|
21
|
+
# `.default` is the registry the generator uses. It SHIPS EMPTY at 1.0.x —
|
|
22
|
+
# the mechanism (stamp the synced version, detect the gap, run ordered
|
|
23
|
+
# transforms) is the deliverable; the first concrete `1.x → 1.y` transform is
|
|
24
|
+
# a one-line `Migration.new(...)` addition here once a release needs one.
|
|
25
|
+
class MigrationRegistry
|
|
26
|
+
def initialize(migrations = [])
|
|
27
|
+
@migrations = migrations.sort_by(&:to)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The registry the install generator runs during `--sync`. Empty today.
|
|
31
|
+
def self.default
|
|
32
|
+
@default ||= new(MIGRATIONS)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# No migrations to register yet — the mechanism is the 1.0 deliverable.
|
|
36
|
+
# Add ordered `Migration.new(to: "1.x.0", description: "...") { ... }`
|
|
37
|
+
# entries here as future releases change config knobs, routes, or templates.
|
|
38
|
+
MIGRATIONS = [].freeze
|
|
39
|
+
|
|
40
|
+
# The migrations a site last synced at `from_version` still needs, ascending
|
|
41
|
+
# by version — those in `(from_version, upto]`. `upto` defaults to the
|
|
42
|
+
# installed gem version so a migration targeting an unreleased version never
|
|
43
|
+
# applies (and never re-runs against the post-sync stamp).
|
|
44
|
+
def applicable(from_version, upto: DocsKit::VERSION)
|
|
45
|
+
from = Gem::Version.new(from_version.to_s)
|
|
46
|
+
ceiling = Gem::Version.new(upto.to_s)
|
|
47
|
+
@migrations.select { |migration| migration.to > from && migration.to <= ceiling }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Run every applicable migration in order against the site, returning the
|
|
51
|
+
# flattened list of manual-cleanup warnings they couldn't safely automate.
|
|
52
|
+
def migrate!(from_version, root, generator, upto: DocsKit::VERSION)
|
|
53
|
+
applicable(from_version, upto: upto).flat_map { |migration| migration.call(root, generator) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= self.class.synced_stamp %>
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
# docs-kit configuration — everything that makes this site look like YOUR docs.
|
|
@@ -20,6 +21,12 @@ Rails.application.config.to_prepare do
|
|
|
20
21
|
# docs live under a subpath:
|
|
21
22
|
# c.brand_href = "/docs"
|
|
22
23
|
|
|
24
|
+
# Docs embedded in a bigger app? Add the way BACK to that app — a labeled
|
|
25
|
+
# link rendered once in the topbar, right after the brand. brand_href is the
|
|
26
|
+
# DOCS home (brand, sidebar, and the page masthead's "← Docs home" all
|
|
27
|
+
# follow it); app_link is the APP home. Unset (the default) renders nothing.
|
|
28
|
+
# c.app_link = { href: "/", label: "Back to the app" }
|
|
29
|
+
|
|
23
30
|
# Repo/social links in the topbar, next to the theme switcher. Each renders
|
|
24
31
|
# as an icon-only button; `icon` is a shipped brand mark (:github, :gitlab,
|
|
25
32
|
# :discord, :x, :rubygems, :bluesky, :mastodon, :slack, :whatsapp, :telegram,
|
|
@@ -109,6 +116,9 @@ Rails.application.config.to_prepare do
|
|
|
109
116
|
# knobs. Every field is optional; with none set it still renders a minimal hero
|
|
110
117
|
# (the brand + the doc index). Wrap a run in **double asterisks** to accent it
|
|
111
118
|
# in the primary color.
|
|
119
|
+
# c.landing.logo = { svg: "M4 2h9l5 5…Z", viewbox: "0 0 22 24", label: "<%= app_brand %>" }
|
|
120
|
+
# # ^ an inline single-path SVG mark (fill: currentColor, theme-adaptive), OR
|
|
121
|
+
# # an image: { src: "logo.svg", alt: "<%= app_brand %>" }
|
|
112
122
|
# c.landing.eyebrow = "Developer Docs"
|
|
113
123
|
# c.landing.title = "The <%= app_brand %> **API**"
|
|
114
124
|
# c.landing.lead = "One sentence on what your product does."
|
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.0.
|
|
4
|
+
version: 1.0.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -234,6 +234,8 @@ files:
|
|
|
234
234
|
- lib/docs_kit/version.rb
|
|
235
235
|
- lib/generators/docs_kit/install/USAGE
|
|
236
236
|
- lib/generators/docs_kit/install/install_generator.rb
|
|
237
|
+
- lib/generators/docs_kit/install/migration.rb
|
|
238
|
+
- lib/generators/docs_kit/install/migration_registry.rb
|
|
237
239
|
- lib/generators/docs_kit/install/sync_report.rb
|
|
238
240
|
- lib/generators/docs_kit/install/templates/Dockerfile.tt
|
|
239
241
|
- lib/generators/docs_kit/install/templates/agents_md.erb
|