docs-kit 1.1.1 → 1.2.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 +74 -0
- data/README.md +28 -11
- data/app/components/docs_ui/code.rb +29 -10
- data/lib/docs_kit/templates/new_site.rb +1 -1
- data/lib/docs_kit/version.rb +1 -1
- data/lib/generators/docs_kit/install/install_generator.rb +39 -4
- data/lib/generators/docs_kit/install/migration_registry.rb +12 -9
- data/lib/generators/docs_kit/install/migrations/v1_2_0.rb +120 -0
- data/lib/generators/docs_kit/install/sync_report.rb +87 -1
- data/lib/generators/docs_kit/install/templates/Dockerfile.tt +1 -1
- data/lib/generators/docs_kit/install/templates/agents_md.erb +3 -0
- data/lib/generators/docs_kit/page/page_generator.rb +10 -4
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04c64683910c10c37f660008f8891ca9f87296a0aea1ddc67f9d9a199975cccd
|
|
4
|
+
data.tar.gz: d357822c1df8899b50008f28366992bde0b9d6f622ec90975b5926f11d2de2d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2b905a1b80c5e156ea7e652ba45cb4d2ec54c91ad040754112cdd481644b7698fd5356459faf06f46ac140c0c6562b8989039b69585b3f257a3b5629cfe0d49
|
|
7
|
+
data.tar.gz: 9837ae7885fb3c6ad358b585853aa7d0dee63b9807c574f15e8543f70ae5de6ecea4f5a2755391588a111fa5d05fcf2456a76b9040a648b86bcf3cf767700171
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,80 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Upgrading from 1.1
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bundle update docs-kit
|
|
9
|
+
bin/rails g docs_kit:install --sync # applies the 1.2 migration, prints what it can't
|
|
10
|
+
bun install && bun run build:css
|
|
11
|
+
bundle exec rspec
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **Ruby 3.3 or newer is required.** docs-kit 1.2 won't install on 3.2. Move
|
|
15
|
+
`.ruby-version` and the Dockerfile's `ARG RUBY_VERSION` first (`--sync` warns
|
|
16
|
+
about either one if it's still below 3.3).
|
|
17
|
+
- **`--sync` raises the toolchain it scaffolded.** In a Dockerfile that still
|
|
18
|
+
carries docs-kit's old `ARG BUN_VERSION=1.3.2`, it bumps that to 1.4.2. In
|
|
19
|
+
`package.json`, it raises docs-kit's old `tailwindcss` / `@tailwindcss/cli
|
|
20
|
+
^4.1.18` and `daisyui ^5.6.0` floors to `^4.3.3` / `^5.7.47`. Values you
|
|
21
|
+
picked yourself are never rewritten: an older one is printed as a warning,
|
|
22
|
+
and a newer one is left alone. Review the result with `git diff`.
|
|
23
|
+
- **Deploys pick up Node 24 actions and `ubuntu-26.04` automatically** when your
|
|
24
|
+
`deploy-docs.yml` calls `zoolutions/docs-kit/.github/workflows/deploy.yml@main`
|
|
25
|
+
(what `docs-kit new` scaffolds). `--sync` warns about a caller pinned to a tag
|
|
26
|
+
or SHA; move the pin forward.
|
|
27
|
+
- **Untrack `app/assets/stylesheets/tailwind.sources.css`** if it's committed:
|
|
28
|
+
`git rm --cached app/assets/stylesheets/tailwind.sources.css`. The generator
|
|
29
|
+
now gitignores it; `--sync` reminds you when it's still tracked.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
- **Ruby 3.2 support.** `required_ruby_version` is now `>= 3.3` (3.2 reached
|
|
34
|
+
end of life in March 2026), and CI no longer tests it.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **daisyui 2.x is allowed.** The dependency is now `>= 1.2, < 3`. 1.2.0 was
|
|
39
|
+
tagged but never reached RubyGems (its release run failed a stale spec), so
|
|
40
|
+
1.2.1 is the first published 1.2 release.
|
|
41
|
+
- **The reusable deploy workflow runs on Node 24 actions.** `deploy.yml` now
|
|
42
|
+
uses `actions/checkout@v7`, `docker/setup-buildx-action@v4`,
|
|
43
|
+
`docker/login-action@v4`, `docker/build-push-action@v7` and
|
|
44
|
+
`webfactory/ssh-agent@v0.10.0`, clearing the Node 20 deprecation warning for
|
|
45
|
+
every site that calls it. Its jobs run on `ubuntu-26.04` (ahead of the
|
|
46
|
+
`ubuntu-latest` migration).
|
|
47
|
+
- **`docs_kit:install` scaffolds current toolchain floors.** The `package.json`
|
|
48
|
+
stub requires `tailwindcss`/`@tailwindcss/cli` `^4.3.3` and `daisyui`
|
|
49
|
+
`^5.7.47` (a spec keeps them in lockstep with the gem's own docs site), and
|
|
50
|
+
the generated `Dockerfile` installs Bun 1.4.2 (and falls back to Ruby 3.4.11
|
|
51
|
+
when the host Ruby version can't be read).
|
|
52
|
+
- **`docs_kit:install` gitignores the generated `tailwind.sources.css`.**
|
|
53
|
+
`bin/build-css` rewrites it on every build with machine-specific gem paths, so
|
|
54
|
+
a committed copy only churns. `--sync` adds the `.gitignore` entry and warns
|
|
55
|
+
when the file is still tracked. (#72)
|
|
56
|
+
- **`--sync` runs its first release migration** (1.1 → 1.2, above). The
|
|
57
|
+
migration registry no longer ships empty.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **`docs_kit:page` injected the registry line once per group.** In a registry
|
|
62
|
+
laid out as blank-line-separated groups (the layout `docs_kit:install`
|
|
63
|
+
produces), the "page line not followed by a page line" Regexp anchor matched
|
|
64
|
+
the end of *every* group and Thor's `inject_into_file` replaced every match.
|
|
65
|
+
The anchor is now the file's last `page` line as a String, so one run adds
|
|
66
|
+
exactly one line, at the end of the last group.
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- **`DocsUI::Code` infers the language from `filename:`.**
|
|
71
|
+
`DocsUI::Code(<<~YAML, filename: "config/deploy.yml")` now highlights as YAML
|
|
72
|
+
instead of Ruby: with no `lexer:`, the lexer is guessed from Rouge's filename
|
|
73
|
+
globs (`*.yml` → yaml, `Dockerfile` → docker, `*.sh` → shell, …). An explicit
|
|
74
|
+
`lexer:` still wins, and a block with neither (or an unguessable filename)
|
|
75
|
+
stays Ruby, so existing output is unchanged.
|
|
76
|
+
|
|
77
|
+
## [1.1.1] and earlier
|
|
78
|
+
|
|
5
79
|
### Fixed
|
|
6
80
|
|
|
7
81
|
- **SEO `og:image` 404.** The og:image tag pointed at the raw config path
|
data/README.md
CHANGED
|
@@ -102,9 +102,17 @@ A site created before the stamp existed (no comment) is treated as the earliest
|
|
|
102
102
|
version, so it gets every migration. Migrations are **warn-only-safe** exactly
|
|
103
103
|
like the drift report: what a step can't safely automate it prints as a manual
|
|
104
104
|
checklist (`migration steps to apply by hand:`), never a destructive rewrite of
|
|
105
|
-
a line you've edited.
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
a line you've edited. A value is only rewritten when it is exactly what an older
|
|
106
|
+
docs-kit generated; anything you picked yourself is reported, never changed.
|
|
107
|
+
|
|
108
|
+
Every release that needs site changes lists them under **"Upgrading from X.Y"**
|
|
109
|
+
at the top of its [CHANGELOG](CHANGELOG.md) entry. That's what `--sync` automates
|
|
110
|
+
and what it leaves to you. Read it before a `bundle update docs-kit` that crosses
|
|
111
|
+
a minor version.
|
|
112
|
+
|
|
113
|
+
| Release | `--sync` does | You do |
|
|
114
|
+
|---------|---------------|--------|
|
|
115
|
+
| 1.2 | Raises the Bun and Tailwind/daisyUI floors that docs-kit generated. Warns about Ruby below 3.3, hand-picked older floors, and a deploy workflow pinned off `@main` | Move to Ruby >= 3.3 before updating. `git rm --cached` a tracked `tailwind.sources.css` |
|
|
108
116
|
|
|
109
117
|
### One-time cleanup for sites created before these landed
|
|
110
118
|
|
|
@@ -118,6 +126,7 @@ generators:
|
|
|
118
126
|
| `app/helpers/icon_helper.rb` | docs-kit renders icons via rails_icons (`DocsUI::Icon`) | Delete the file. |
|
|
119
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. |
|
|
120
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
|
+
| `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. |
|
|
121
130
|
|
|
122
131
|
### Upgrading your Dockerfile
|
|
123
132
|
|
|
@@ -433,6 +442,10 @@ DocsUI::Section("Add the gem", id: "add", description: …) # title positional
|
|
|
433
442
|
DocsUI::Code(source, lexer: :ruby, filename: "Gemfile") # source positional
|
|
434
443
|
```
|
|
435
444
|
|
|
445
|
+
`DocsUI::Code` infers the language from `filename:` (`config/deploy.yml` →
|
|
446
|
+
YAML, `Dockerfile` → docker, `bin/setup.sh` → shell); `lexer:` overrides the
|
|
447
|
+
guess, and with neither the block is highlighted as Ruby.
|
|
448
|
+
|
|
436
449
|
For the two wrappers that take **no** positional argument — prose and a
|
|
437
450
|
multi-language example — `DocsUI::Page` gives you lowercase helpers so a block
|
|
438
451
|
needs no parens:
|
|
@@ -930,7 +943,7 @@ jobs:
|
|
|
930
943
|
```yaml
|
|
931
944
|
service: <repo>
|
|
932
945
|
image: zoolutions/<repo>
|
|
933
|
-
minimum_version: 4.0.
|
|
946
|
+
minimum_version: 4.0.7 # dash 4.0.7: doctor stale-proxy recovery + registry credential fail-fast
|
|
934
947
|
retain_containers: 2
|
|
935
948
|
error_pages_path: public # 502/503/504.html served during a deploy gap
|
|
936
949
|
registry: { server: ghcr.io, username: mhenrixon, password: [DASH_REGISTRY_PASSWORD] }
|
|
@@ -1082,17 +1095,21 @@ application.register("reactive", ReactiveController)
|
|
|
1082
1095
|
|
|
1083
1096
|
## Releasing (maintainers)
|
|
1084
1097
|
|
|
1085
|
-
Cut a release with
|
|
1098
|
+
Cut a release with `bin/release`. Never `gem push` by hand:
|
|
1086
1099
|
|
|
1087
1100
|
```bash
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1101
|
+
bin/release list # last releases + what each bump would give
|
|
1102
|
+
bin/release --dry-run # the version it would cut + the changes since the last tag
|
|
1103
|
+
bin/release # patch bump (minor / major / 1.3.0.rc1 for others)
|
|
1104
|
+
bin/release 1.2.0 --force # delete + re-create an existing tag/release
|
|
1091
1105
|
```
|
|
1092
1106
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1107
|
+
`bin/release` checks you're on a clean, up-to-date `main`, confirms, and hands off
|
|
1108
|
+
to `rake release[X.Y.Z]` (`rakelib/release.rake`). That task bumps
|
|
1109
|
+
`lib/docs_kit/version.rb` and the `docs-kit` pin in `docs/Gemfile.lock` (in place,
|
|
1110
|
+
no re-resolve), verifies `gem build --strict`, commits, pushes, and creates the
|
|
1111
|
+
GitHub Release. Both files belong to the zoolutions release kit that every gem
|
|
1112
|
+
shares; see [RELEASE_KIT.md](RELEASE_KIT.md) to adopt or upgrade it elsewhere. Publishing the tag fires
|
|
1096
1113
|
`.github/workflows/release.yml`, which runs the suite, rebuilds + content-checks
|
|
1097
1114
|
the gem, signs it with Sigstore, and pushes to RubyGems over **OIDC trusted
|
|
1098
1115
|
publishing** (no API token stored anywhere).
|
|
@@ -9,12 +9,16 @@ module DocsUI
|
|
|
9
9
|
# stylesheet asset is required.
|
|
10
10
|
#
|
|
11
11
|
# render DocsUI::Code.new(ruby_source) # ruby, no title
|
|
12
|
-
# render DocsUI::Code.new(
|
|
12
|
+
# render DocsUI::Code.new(yaml, filename: "config/deploy.yml") # yaml, inferred
|
|
13
|
+
# render DocsUI::Code.new(py, lexer: :python, filename: "a.py") # explicit wins
|
|
13
14
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# The language resolves in order: an explicit `lexer:`; a guess from the
|
|
16
|
+
# `filename:` (Rouge's own filename globs — *.yml → yaml, Dockerfile → docker,
|
|
17
|
+
# *.sh → shell, …); else ruby. Any language Rouge knows (~200 lexers) works by
|
|
18
|
+
# its name or alias — python, go, rust, elixir, kotlin, swift, json,
|
|
19
|
+
# dockerfile, ... — no allowlist. Add friendly lexer aliases via
|
|
20
|
+
# DocsKit.configure (code_lexer_aliases). An unknown language falls back to
|
|
21
|
+
# plaintext (never raises). (Tab labels are a
|
|
18
22
|
# DocsUI::Example concern — set via code_language_labels, not here; Code has no
|
|
19
23
|
# label, only a filename.)
|
|
20
24
|
class Code < Phlex::HTML
|
|
@@ -22,7 +26,7 @@ module DocsUI
|
|
|
22
26
|
|
|
23
27
|
FORMATTER = Rouge::Formatters::HTML.new
|
|
24
28
|
|
|
25
|
-
def initialize(source, lexer:
|
|
29
|
+
def initialize(source, lexer: nil, filename: nil)
|
|
26
30
|
@source = source.to_s.strip
|
|
27
31
|
@lexer = lexer
|
|
28
32
|
@filename = filename
|
|
@@ -66,11 +70,26 @@ module DocsUI
|
|
|
66
70
|
end
|
|
67
71
|
end
|
|
68
72
|
|
|
69
|
-
# Resolve
|
|
70
|
-
# class/instance passed through; a configured friendly
|
|
71
|
-
# registry
|
|
73
|
+
# Resolve the lexer to a Rouge lexer instance. Order: an explicit Rouge::Lexer
|
|
74
|
+
# class/instance passed through; an explicit name via a configured friendly
|
|
75
|
+
# alias → Rouge's own registry → the configured fallback (plaintext); with no
|
|
76
|
+
# `lexer:` given, a guess from the filename; else the ruby default.
|
|
72
77
|
def lexer
|
|
73
|
-
explicit_lexer ||
|
|
78
|
+
explicit_lexer ||
|
|
79
|
+
(@lexer && (find_lexer(@lexer.to_s) || Rouge::Lexers::PlainText).new) ||
|
|
80
|
+
guessed_lexer ||
|
|
81
|
+
Rouge::Lexers::Ruby.new
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A lexer inferred from @filename via Rouge's declared filename globs, or nil
|
|
85
|
+
# when there is no filename, nothing matches, or the match is ambiguous.
|
|
86
|
+
# (Lexer.guesses, not Lexer.guess: the latter answers PlainText for a
|
|
87
|
+
# no-match, which is indistinguishable from a real guess.)
|
|
88
|
+
def guessed_lexer
|
|
89
|
+
return nil if @filename.nil?
|
|
90
|
+
|
|
91
|
+
guesses = Rouge::Lexer.guesses(filename: @filename.to_s)
|
|
92
|
+
guesses.size == 1 ? guesses.first.new : nil
|
|
74
93
|
end
|
|
75
94
|
|
|
76
95
|
# A Rouge::Lexer instance passed directly (class or instance), else nil.
|
|
@@ -64,7 +64,7 @@ after_bundle do
|
|
|
64
64
|
|
|
65
65
|
# dash 4 renamed the on-host proxy (kamal-proxy → dash-proxy) and migrates a
|
|
66
66
|
# host in place; an older CLI must not deploy this config.
|
|
67
|
-
minimum_version: 4.0.
|
|
67
|
+
minimum_version: 4.0.7
|
|
68
68
|
|
|
69
69
|
# A stateless docs site never rolls back far — keep the host tidy.
|
|
70
70
|
retain_containers: 2
|
data/lib/docs_kit/version.rb
CHANGED
|
@@ -57,6 +57,12 @@ module DocsKit
|
|
|
57
57
|
|
|
58
58
|
def self.synced_stamp(version = DocsKit::VERSION) = "# docs-kit synced: v#{version}"
|
|
59
59
|
|
|
60
|
+
# The generated Tailwind @source globs file bin/build-css rewrites on
|
|
61
|
+
# every build — gitignored fleet-wide (see ignore_generated_css_sources).
|
|
62
|
+
# The covering/negation line regexes live beside the path in SyncReport,
|
|
63
|
+
# which shares them for the tracked-file drift check.
|
|
64
|
+
TAILWIND_SOURCES = SyncReport::TAILWIND_SOURCES
|
|
65
|
+
|
|
60
66
|
# The RuboCop wiring docs-kit injects. REQUIRE loads the cops;
|
|
61
67
|
# INHERIT_GEM/INHERIT_PATH enable + scope them (see config/rubocop/docs_kit.yml).
|
|
62
68
|
RUBOCOP_REQUIRE = "docs_kit/rubocop"
|
|
@@ -192,6 +198,35 @@ module DocsKit
|
|
|
192
198
|
create_file "app/assets/builds/.keep", ""
|
|
193
199
|
end
|
|
194
200
|
|
|
201
|
+
# Gitignore the bin/build-css-generated @source globs file (#71). It
|
|
202
|
+
# carries machine-specific absolute gem paths, so a committed copy churns
|
|
203
|
+
# on every rebuild by a different machine/Ruby — and no build consumes it
|
|
204
|
+
# (.dockerignore excludes it; every build path runs bin/build-css first).
|
|
205
|
+
# Additive + idempotent (tolerant of a hand-added entry with or without
|
|
206
|
+
# the leading slash), and NOT --sync-guarded: the ignore is the fleet-wide
|
|
207
|
+
# upgrade this step exists to ship. Untracking an already-committed copy
|
|
208
|
+
# is the site's call — SyncReport warns with the exact command instead
|
|
209
|
+
# (the generator never mutates git state).
|
|
210
|
+
def ignore_generated_css_sources
|
|
211
|
+
entry = "# Generated by bin/build-css (resolved gem @source globs).\n/#{TAILWIND_SOURCES}\n"
|
|
212
|
+
path = File.join(destination_root, ".gitignore")
|
|
213
|
+
return create_file(".gitignore", entry) unless File.exist?(path)
|
|
214
|
+
|
|
215
|
+
# Last-match-wins, like git reads the file: an EFFECTIVE `!` unignore is
|
|
216
|
+
# the site's deliberate opt-out — appending our entry after it would
|
|
217
|
+
# become the last matching rule and silently defeat the hand-edit, so
|
|
218
|
+
# back off (SyncReport skips its nag too). An effective ignore is done.
|
|
219
|
+
case SyncReport.tailwind_sources_rule(File.read(path))
|
|
220
|
+
when :negate
|
|
221
|
+
say_status(:skip, ".gitignore negates tailwind.sources.css (!) — respecting the site's opt-out",
|
|
222
|
+
:yellow)
|
|
223
|
+
when :ignore
|
|
224
|
+
say_status(:identical, ".gitignore (tailwind.sources.css)", :blue)
|
|
225
|
+
else
|
|
226
|
+
append_to_file ".gitignore", "\n#{entry}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
195
230
|
# Install the `docs_kit:og` rake task — gem-owned wiring, refreshed on every
|
|
196
231
|
# run so a site picks up task fixes. It does NOT ship an OG image: the
|
|
197
232
|
# social-share image is SITE content, generated into the site's OWN
|
|
@@ -539,9 +574,9 @@ module DocsKit
|
|
|
539
574
|
"watch:css": "bin/build-css --watch"
|
|
540
575
|
},
|
|
541
576
|
"devDependencies": {
|
|
542
|
-
"@tailwindcss/cli": "^4.
|
|
543
|
-
"daisyui": "^5.
|
|
544
|
-
"tailwindcss": "^4.
|
|
577
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
578
|
+
"daisyui": "^5.7.47",
|
|
579
|
+
"tailwindcss": "^4.3.3"
|
|
545
580
|
}
|
|
546
581
|
}
|
|
547
582
|
JSON
|
|
@@ -578,7 +613,7 @@ module DocsKit
|
|
|
578
613
|
# The RUBY_VERSION build ARG default: the host's running Ruby (X.Y.Z), so a
|
|
579
614
|
# site's image matches its dev Ruby. Used in Dockerfile.tt.
|
|
580
615
|
def ruby_version_arg
|
|
581
|
-
RUBY_VERSION[/\d+\.\d+\.\d+/] || "3.4.
|
|
616
|
+
RUBY_VERSION[/\d+\.\d+\.\d+/] || "3.4.11"
|
|
582
617
|
end
|
|
583
618
|
|
|
584
619
|
# True when the site bundles Thruster (HTTP caching + compression +
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "migration"
|
|
4
|
+
require_relative "migrations/v1_2_0"
|
|
4
5
|
require_relative "../../../docs_kit/version"
|
|
5
6
|
|
|
6
7
|
module DocsKit
|
|
@@ -18,24 +19,26 @@ module DocsKit
|
|
|
18
19
|
# re-run on EVERY sync forever. It can't legitimately exist anyway (a site
|
|
19
20
|
# can't have "arrived" at a release it doesn't have), so it's filtered out.
|
|
20
21
|
#
|
|
21
|
-
# `.default` is the registry the generator uses.
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# a one-line `Migration.new(...)` addition here once a release needs one.
|
|
22
|
+
# `.default` is the registry the generator uses. Each release that changes
|
|
23
|
+
# what a site must carry adds one `Migration.new(...)` entry to MIGRATIONS,
|
|
24
|
+
# its transform in migrations/vX_Y_Z.rb.
|
|
25
25
|
class MigrationRegistry
|
|
26
26
|
def initialize(migrations = [])
|
|
27
27
|
@migrations = migrations.sort_by(&:to)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# The registry the install generator runs during `--sync`.
|
|
30
|
+
# The registry the install generator runs during `--sync`.
|
|
31
31
|
def self.default
|
|
32
32
|
@default ||= new(MIGRATIONS)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
# Ordered `Migration.new(to: "1.x.0", description: "...") { ... }` entries, one
|
|
36
|
+
# per release that changes config knobs, routes, templates, or toolchain.
|
|
37
|
+
MIGRATIONS = [
|
|
38
|
+
Migration.new(to: "1.2.0", description: Migrations::V1_2_0::DESCRIPTION) do |root, _generator|
|
|
39
|
+
Migrations::V1_2_0.call(root)
|
|
40
|
+
end
|
|
41
|
+
].freeze
|
|
39
42
|
|
|
40
43
|
# The migrations a site last synced at `from_version` still needs, ascending
|
|
41
44
|
# by version — those in `(from_version, upto]`. `upto` defaults to the
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
module Generators
|
|
5
|
+
module Migrations
|
|
6
|
+
# The 1.1 → 1.2 upgrade. docs-kit 1.2 needs Ruby >= 3.3, scaffolds Bun 1.4.2
|
|
7
|
+
# and newer Tailwind/daisyUI floors, and its reusable deploy workflow moved to
|
|
8
|
+
# Node 24 actions (only callers that track `@main` get that).
|
|
9
|
+
#
|
|
10
|
+
# Warn-only-safe: a value is rewritten only when it is EXACTLY what an older
|
|
11
|
+
# docs-kit generated (so it can't be a hand edit); anything else older is
|
|
12
|
+
# handed back as a warning, and a newer value is left alone.
|
|
13
|
+
module V1_2_0 # rubocop:disable Naming/ClassAndModuleCamelCase -- named after the release
|
|
14
|
+
DESCRIPTION = "docs-kit 1.2: Ruby >= 3.3, Bun 1.4.2, Tailwind 4.3 / daisyUI 5.7 floors, Node 24 deploy"
|
|
15
|
+
MIN_RUBY = Gem::Version.new("3.3")
|
|
16
|
+
BUN = { from: "1.3.2", to: "1.4.2" }.freeze
|
|
17
|
+
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" }
|
|
21
|
+
}.freeze
|
|
22
|
+
DEPLOY_CALL = %r{zoolutions/docs-kit/\.github/workflows/deploy\.yml@(\S+)}
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Returns the warnings it could not safely automate.
|
|
27
|
+
def call(root)
|
|
28
|
+
bun(root) + ruby(root) + package_floors(root) + deploy_pins(root)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def bun(root)
|
|
32
|
+
edit(root, "Dockerfile") do |source, warnings|
|
|
33
|
+
current = source[/^ARG BUN_VERSION=(\S+)$/, 1]
|
|
34
|
+
next source unless current && older?(current, BUN[:to])
|
|
35
|
+
next source.sub("ARG BUN_VERSION=#{current}", "ARG BUN_VERSION=#{BUN[:to]}") if current == BUN[:from]
|
|
36
|
+
|
|
37
|
+
warnings << "Dockerfile: ARG BUN_VERSION=#{current} is older than the #{BUN[:to]} docs-kit now " \
|
|
38
|
+
"scaffolds; raise it (a bun.lock written by a newer Bun can fail " \
|
|
39
|
+
"`bun install --frozen-lockfile`)"
|
|
40
|
+
source
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ruby(root)
|
|
45
|
+
{
|
|
46
|
+
"Dockerfile" => read(root, "Dockerfile")&.[](/^ARG RUBY_VERSION=\S+$/),
|
|
47
|
+
".ruby-version" => read(root, ".ruby-version")&.strip
|
|
48
|
+
}.filter_map do |file, value|
|
|
49
|
+
next unless value && below_min_ruby?(value.delete_prefix("ARG RUBY_VERSION="))
|
|
50
|
+
|
|
51
|
+
"#{file}: #{value} — docs-kit 1.2 requires Ruby >= #{MIN_RUBY}; move the site to 3.3 or newer"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def package_floors(root)
|
|
56
|
+
edit(root, "package.json") do |source, warnings|
|
|
57
|
+
PACKAGE_FLOORS.reduce(source) do |json, (name, floor)|
|
|
58
|
+
raise_floor(json, name, floor, warnings)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def raise_floor(json, name, floor, warnings)
|
|
64
|
+
pair = /("#{Regexp.escape(name)}"\s*:\s*")([^"]+)(")/
|
|
65
|
+
current = json[pair, 2]
|
|
66
|
+
return json unless current && older?(current, floor[:to])
|
|
67
|
+
if current == floor[:from]
|
|
68
|
+
return json.sub(pair) { "#{Regexp.last_match(1)}#{floor[:to]}#{Regexp.last_match(3)}" }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
warnings << "package.json: #{name} #{current} is below the #{floor[:to]} docs-kit now builds against"
|
|
72
|
+
json
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def deploy_pins(root)
|
|
76
|
+
Dir[File.join(root, ".github/workflows/*.{yml,yaml}")].filter_map do |path|
|
|
77
|
+
ref = File.read(path)[DEPLOY_CALL, 1]
|
|
78
|
+
next if ref.nil? || ref == "main"
|
|
79
|
+
|
|
80
|
+
"#{File.basename(path)}: calls docs-kit's deploy.yml@#{ref}; the Node 24 actions and " \
|
|
81
|
+
"ubuntu-26.04 runners are on @main — move the pin forward (or track @main)"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Yields [source, warnings] for an existing file; writes back what the block returns.
|
|
86
|
+
def edit(root, file)
|
|
87
|
+
source = read(root, file)
|
|
88
|
+
return [] unless source
|
|
89
|
+
|
|
90
|
+
warnings = []
|
|
91
|
+
updated = yield(source, warnings)
|
|
92
|
+
File.write(File.join(root, file), updated) unless updated == source
|
|
93
|
+
warnings
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def read(root, file)
|
|
97
|
+
path = File.join(root, file)
|
|
98
|
+
File.exist?(path) ? File.read(path) : nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# "^5.6.0" / "~> 4.1" / "3.2.4" / "ruby-3.2.4" → its version, or nil when it
|
|
102
|
+
# isn't a plain version (a tag like "latest", a git URL, a range).
|
|
103
|
+
def version_of(value)
|
|
104
|
+
number = value.to_s[/\A(?:ruby-)?[\^~]?>?\s*(\d+(?:\.\d+)*)\z/, 1]
|
|
105
|
+
number && Gem::Version.new(number)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def older?(current, target)
|
|
109
|
+
current_version = version_of(current)
|
|
110
|
+
current_version ? current_version < version_of(target) : false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def below_min_ruby?(value)
|
|
114
|
+
version = version_of(value)
|
|
115
|
+
version ? version < MIN_RUBY : false
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -17,10 +17,54 @@ module DocsKit
|
|
|
17
17
|
# - a dead IconHelper copy — the gem renders icons via rails_icons.
|
|
18
18
|
# - a Dockerfile stamped by an OLDER docs-kit than the gem now ships — the
|
|
19
19
|
# site should diff against the current template and adopt the improvements.
|
|
20
|
+
# - a git-tracked tailwind.sources.css — generated per-build with machine
|
|
21
|
+
# absolute gem paths (#71); gitignored now, but untracking a committed
|
|
22
|
+
# copy stages a deletion, so the site runs `git rm --cached` itself.
|
|
20
23
|
class SyncReport
|
|
21
24
|
APPLICATION_CONTROLLER = "app/controllers/application_controller.rb"
|
|
22
25
|
ICON_HELPER = "app/helpers/icon_helper.rb"
|
|
23
26
|
DOCKERFILE = "Dockerfile"
|
|
27
|
+
TAILWIND_SOURCES = "app/assets/stylesheets/tailwind.sources.css"
|
|
28
|
+
|
|
29
|
+
# A .gitignore line covering the generated file per gitignore semantics:
|
|
30
|
+
# the anchored path (leading slash optional — a slash-containing pattern
|
|
31
|
+
# is root-anchored either way) or the bare filename (no slash → matches
|
|
32
|
+
# at any depth), each optionally **/-prefixed. Deliberately NOT a full
|
|
33
|
+
# gitignore matcher: an exotic broader glob (`app/assets/stylesheets/*`)
|
|
34
|
+
# is missed at the cost of one redundant, harmless line. This regex
|
|
35
|
+
# drives the generator's APPEND decision only; the drift warning asks
|
|
36
|
+
# git itself (see #ignored_by_git?, scoped to the committed .gitignore
|
|
37
|
+
# files so a user's personal excludes never decide repo content).
|
|
38
|
+
# `[ \t\r]*` (not `[ \t]*`): `$` matches before `\n` but never past a
|
|
39
|
+
# `\r`, so the class must consume the CR of a CRLF-checked-out file.
|
|
40
|
+
TAILWIND_SOURCES_COVER =
|
|
41
|
+
"(?:(?:/|\\*\\*/)?#{Regexp.escape(File.dirname(TAILWIND_SOURCES))}/|(?:\\*\\*/)?)" \
|
|
42
|
+
"#{Regexp.escape(File.basename(TAILWIND_SOURCES))}[ \t\r]*".freeze
|
|
43
|
+
TAILWIND_SOURCES_IGNORED_RE = /^#{TAILWIND_SOURCES_COVER}$/
|
|
44
|
+
# An explicit `!` unignore of the file — the site's deliberate opt-out
|
|
45
|
+
# from the fleet convention (it wants the file committed). The
|
|
46
|
+
# generator's append respects it when it's the file's effective rule
|
|
47
|
+
# (see .tailwind_sources_rule); the drift warning asks git instead
|
|
48
|
+
# (#ignored_by_git?), which resolves full pattern semantics.
|
|
49
|
+
TAILWIND_SOURCES_NEGATED_RE = /^!#{TAILWIND_SOURCES_COVER}$/
|
|
50
|
+
|
|
51
|
+
# The file's effective disposition among the recognized .gitignore lines,
|
|
52
|
+
# honoring git's last-match-wins: `:negate` (the site's genuine opt-out),
|
|
53
|
+
# `:ignore` (already covered), or nil (no recognized line). A `!` line
|
|
54
|
+
# overridden by a LATER ignore line is dead — git ignores the file, so
|
|
55
|
+
# treating it as an opt-out would misreport the site's intent. Drives the
|
|
56
|
+
# generator's append decision only (the drift check asks git itself).
|
|
57
|
+
def self.tailwind_sources_rule(gitignore_content)
|
|
58
|
+
rule = nil
|
|
59
|
+
gitignore_content.each_line do |line|
|
|
60
|
+
if line.match?(TAILWIND_SOURCES_NEGATED_RE)
|
|
61
|
+
rule = :negate
|
|
62
|
+
elsif line.match?(TAILWIND_SOURCES_IGNORED_RE)
|
|
63
|
+
rule = :ignore
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
rule
|
|
67
|
+
end
|
|
24
68
|
|
|
25
69
|
# Matches the version stamp the Dockerfile template writes, e.g.
|
|
26
70
|
# `# docs-kit Dockerfile v1.0.2`. Absent on a hand-written Dockerfile a site
|
|
@@ -34,7 +78,7 @@ module DocsKit
|
|
|
34
78
|
# The drift messages, in the order a site should act on them. Empty when
|
|
35
79
|
# the site is clean.
|
|
36
80
|
def items
|
|
37
|
-
[render_page_drift, icon_helper_drift, dockerfile_drift].compact
|
|
81
|
+
[render_page_drift, icon_helper_drift, dockerfile_drift, tailwind_sources_drift].compact
|
|
38
82
|
end
|
|
39
83
|
|
|
40
84
|
def clean?
|
|
@@ -79,6 +123,48 @@ module DocsKit
|
|
|
79
123
|
"diff against the template (bin/rails g docs_kit:install shows the path) and adopt the changes."
|
|
80
124
|
end
|
|
81
125
|
|
|
126
|
+
# tailwind.sources.css is regenerated by bin/build-css with machine-local
|
|
127
|
+
# absolute gem paths — the generator gitignores it, but a copy committed
|
|
128
|
+
# before the ignore stays tracked (gitignore doesn't untrack). Untracking
|
|
129
|
+
# stages a deletion, so we hand the site the exact command instead of
|
|
130
|
+
# touching its index.
|
|
131
|
+
def tailwind_sources_drift
|
|
132
|
+
return unless tracked_by_git?(TAILWIND_SOURCES)
|
|
133
|
+
# git's own verdict, not the recognized-lines regex: git resolves the
|
|
134
|
+
# FULL pattern semantics (broad globs, ordering, nested .gitignores),
|
|
135
|
+
# so a dead `!` line before a broader ignore still warns, while a
|
|
136
|
+
# genuinely effective negation (the site's opt-out) stays quiet.
|
|
137
|
+
return unless ignored_by_git?(TAILWIND_SOURCES)
|
|
138
|
+
|
|
139
|
+
"#{TAILWIND_SOURCES} is generated by bin/build-css but tracked by git — " \
|
|
140
|
+
"run `git rm --cached #{TAILWIND_SOURCES}` and commit (the ignore entry is in place)."
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# True when the repo's own .gitignore rules cover `rel` — git's full
|
|
144
|
+
# pattern semantics (broad globs, `!` ordering, nested .gitignores), but
|
|
145
|
+
# SCOPED to the repo's committed convention: `--exclude-per-directory`
|
|
146
|
+
# consults only the .gitignore files, never `.git/info/exclude` or a
|
|
147
|
+
# global core.excludesFile, so the verdict (and the "the ignore entry is
|
|
148
|
+
# in place" guidance it backs) is identical on every machine.
|
|
149
|
+
# `--cached --ignored` reports a TRACKED path matching an exclude — the
|
|
150
|
+
# exact state this drift check exists to catch. Only called after
|
|
151
|
+
# tracked_by_git? proved git + a repo exist.
|
|
152
|
+
def ignored_by_git?(rel)
|
|
153
|
+
out = IO.popen(["git", "-C", @root, "ls-files", "--cached", "--ignored",
|
|
154
|
+
"--exclude-per-directory=.gitignore", "--", rel],
|
|
155
|
+
err: File::NULL, &:read)
|
|
156
|
+
!out.strip.empty?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# True when the site's git index tracks `rel`. Conservative: no git on
|
|
160
|
+
# PATH, not a repo, or an untracked file all read as "no drift". ls-files
|
|
161
|
+
# only consults the index (no commit needed), and `git -C` resolves the
|
|
162
|
+
# repo upward, so a docs site living in a subdir of a larger repo works.
|
|
163
|
+
def tracked_by_git?(rel)
|
|
164
|
+
system("git", "-C", @root, "ls-files", "--error-unmatch", rel,
|
|
165
|
+
out: File::NULL, err: File::NULL) == true
|
|
166
|
+
end
|
|
167
|
+
|
|
82
168
|
def read(rel)
|
|
83
169
|
path = File.join(@root, rel)
|
|
84
170
|
File.exist?(path) ? File.read(path) : nil
|
|
@@ -66,6 +66,9 @@ end
|
|
|
66
66
|
- **The primary argument is positional; modifiers are keywords.**
|
|
67
67
|
`Section("Title", description:)`, `Code(source, filename:)`,
|
|
68
68
|
`Header("Title", eyebrow:)`.
|
|
69
|
+
- **`Code`'s `filename:` selects the language** (`*.yml` → yaml, `Dockerfile`
|
|
70
|
+
→ docker, `*.sh` → shell, …); pass `lexer:` only to override the guess or when
|
|
71
|
+
there is no filename (the default is ruby).
|
|
69
72
|
- **Wrappers that take no positional arg use lowercase page helpers** so a block
|
|
70
73
|
needs no parens: `md <<~'MD' … MD`, `prose { … }`, `example { |ex| … }`,
|
|
71
74
|
`operation "operationId"`. (A bare `DocsUI::Prose do` is a Ruby SyntaxError; the
|
|
@@ -101,13 +101,19 @@ module DocsKit
|
|
|
101
101
|
source.match?(/^\s*entries\s*\[/) && !source.match?(/^\s*page\s+["']/)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
# Inject after the last existing `page` line so ordering lands
|
|
105
|
-
# of the group; else after view_namespace/path_prefix;
|
|
106
|
-
# `extend DocsKit::Registry` line.
|
|
104
|
+
# Inject after the last existing `page` line of the FILE so ordering lands
|
|
105
|
+
# at the end of the last group; else after view_namespace/path_prefix;
|
|
106
|
+
# else after the `extend DocsKit::Registry` line.
|
|
107
|
+
#
|
|
108
|
+
# The `page` anchor is the last matching line as a String, not a Regexp:
|
|
109
|
+
# Thor's inject_into_file replaces EVERY match of a Regexp `after:`, so a
|
|
110
|
+
# "page line not followed by a page line" pattern fires once per group in
|
|
111
|
+
# a registry laid out with blank-line-separated groups (the layout
|
|
112
|
+
# docs_kit:install produces) and duplicates the entry.
|
|
107
113
|
def registry_anchor(source)
|
|
108
114
|
case source
|
|
109
115
|
when /^\s*page\s+["']/
|
|
110
|
-
/^\s*page
|
|
116
|
+
source.lines.grep(/^\s*page\s+["']/).last
|
|
111
117
|
when /^\s*view_namespace\s/
|
|
112
118
|
/^\s*view_namespace .*\n/
|
|
113
119
|
when /^\s*path_prefix\s/
|
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.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -18,7 +18,7 @@ dependencies:
|
|
|
18
18
|
version: '1.2'
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: '
|
|
21
|
+
version: '3'
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -28,7 +28,7 @@ dependencies:
|
|
|
28
28
|
version: '1.2'
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: '
|
|
31
|
+
version: '3'
|
|
32
32
|
- !ruby/object:Gem::Dependency
|
|
33
33
|
name: phlex-rails
|
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -244,6 +244,7 @@ files:
|
|
|
244
244
|
- lib/generators/docs_kit/install/install_generator.rb
|
|
245
245
|
- lib/generators/docs_kit/install/migration.rb
|
|
246
246
|
- lib/generators/docs_kit/install/migration_registry.rb
|
|
247
|
+
- lib/generators/docs_kit/install/migrations/v1_2_0.rb
|
|
247
248
|
- lib/generators/docs_kit/install/sync_report.rb
|
|
248
249
|
- lib/generators/docs_kit/install/templates/Dockerfile.tt
|
|
249
250
|
- lib/generators/docs_kit/install/templates/agents_md.erb
|
|
@@ -281,7 +282,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
281
282
|
requirements:
|
|
282
283
|
- - ">="
|
|
283
284
|
- !ruby/object:Gem::Version
|
|
284
|
-
version: '3.
|
|
285
|
+
version: '3.3'
|
|
285
286
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
286
287
|
requirements:
|
|
287
288
|
- - ">="
|