thecore_generators 3.8.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +130 -22
- data/lib/generators/thecore/association_wiring.rb +2 -1
- data/lib/generators/thecore/atom/atom_generator.rb +481 -0
- data/lib/generators/thecore/atom/templates/abilities.rb.tt +16 -0
- data/lib/generators/thecore/atom/templates/seeds.rb.tt +1 -0
- data/lib/generators/thecore/collection_action/collection_action_generator.rb +64 -0
- data/lib/generators/thecore/collection_action/templates/action.html.erb.tt +13 -0
- data/lib/generators/thecore/collection_action/templates/action.js.tt +42 -0
- data/lib/generators/thecore/collection_action/templates/action.rb.tt +33 -0
- data/lib/generators/thecore/collection_action/templates/action.scss.tt +38 -0
- data/lib/generators/thecore/sample_fetcher.rb +68 -0
- data/lib/generators/thecore/tty_detection.rb +19 -0
- data/lib/templates/app_template.rb +42 -4
- data/lib/thecore_generators/check_practices.rb +20 -14
- data/lib/thecore_generators/version.rb +1 -1
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 146693a69bac954bb16fae029f76d60a5f92d2702f90d1cfda0b19e8a4e1a796
|
|
4
|
+
data.tar.gz: 728d2a12604ebb33b6efa6c7e27e33f7b086e3f61f7042492e5db0d4f9772b6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 738f18d403082dedbbfc4ce40a3376bc03c5057c6789a63436114bf135e262e6127e201c44a65c7ce76a0423eaab0cc198705d47501057d855c96b634ba1ec33
|
|
7
|
+
data.tar.gz: 3559b1310d5d59046158ec81232bc8b86a527c05130c605b571589a65f11c23566b119029d9f36a35b985351b13d31306c66acfbb2f5ee281d6b259a95b2dcd7
|
data/README.md
CHANGED
|
@@ -25,10 +25,22 @@ thecore:check_practices` (Scaffold Files + Models + Actions, `--fix` included) a
|
|
|
25
25
|
**Phase 3's App application template** (porting `createApp.js`) is complete as of this release —
|
|
26
26
|
both the core (`lib/templates/app_template.rb`: Rails app + Gemfile stack + vendor placeholders,
|
|
27
27
|
thecore_generators#17) and devcontainer/CI/CLAUDE.md asset fetching from the thecore repo's own
|
|
28
|
-
samples (thecore_generators#18) have shipped.
|
|
29
|
-
Action generator remain deferred to a future session (ADR 0005). See
|
|
28
|
+
samples (thecore_generators#18) have shipped. See
|
|
30
29
|
[ADR 0005](https://github.com/gabrieletassoni/thecore/blob/release/3/docs/adr/0005-app-template-scoped-to-rails-new-m-assets-sourced-from-thecore-samples.md)
|
|
31
|
-
in the thecore repo for the full design.
|
|
30
|
+
in the thecore repo for the full design. **The Collection Action generator** (`rails generate
|
|
31
|
+
thecore:collection_action`, thecore_generators#21) is also complete as of this release — see
|
|
32
|
+
[ADR 0006](https://github.com/gabrieletassoni/thecore/blob/master/docs/adr/0006-atom-generator-dual-ci-manual-submodule-wiring-collection-action-reuses-existing-infra.md)
|
|
33
|
+
in the thecore repo (`master`, thecore's actual default branch — unlike this gem's own
|
|
34
|
+
`release/3`; the ADR 0001-0005 links above predate that distinction being double-checked; note
|
|
35
|
+
that as of this gem's 3.11.0 release the ADR 0006 commit exists only in a local `thecore`
|
|
36
|
+
checkout, not yet pushed to `origin/master` — same class of operational sequencing issue
|
|
37
|
+
`CLAUDE.md` documents for the App template's samples fetch). **`thecore:atom`**
|
|
38
|
+
(`rails generate thecore:atom NAME`, thecore_generators#20/#22) is feature-complete as of this
|
|
39
|
+
release, including its `CLAUDE.md` fetch from thecore's own `samples/ATOM_CLAUDE.md` — but that
|
|
40
|
+
fetch shares the exact same not-yet-pushed caveat as ADR 0006/the App template's own samples
|
|
41
|
+
fetch above: it 404s against the real default GitHub URL until `thecore`'s `master` actually
|
|
42
|
+
carries the commit that added `samples/ATOM_CLAUDE.md`. Phase 4's one remaining piece is
|
|
43
|
+
documenting both new generators in the thecore repo's `GUIDE.md`/`WALKTHROUGH.md` (thecore#19).
|
|
32
44
|
|
|
33
45
|
### What `rails generate model`/`rails generate migration` do now
|
|
34
46
|
|
|
@@ -207,6 +219,92 @@ templates exactly:
|
|
|
207
219
|
XHR `GET` here vs. `fetch` there), and the view adds a `form_with(..., method: :patch)` for
|
|
208
220
|
the PATCH half of the example.
|
|
209
221
|
|
|
222
|
+
### `rails generate thecore:atom NAME`
|
|
223
|
+
|
|
224
|
+
A Ruby port of `thecore_code_extension`'s `createATOM.js` — produces a complete, working ATOM
|
|
225
|
+
(a Rails engine, the Thecore unit of modular functionality) from a terminal, no VS Code
|
|
226
|
+
required:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
rails generate thecore:atom tcp_debugger
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Unlike every other generator in this gem, `thecore:atom` takes **no `--atom=NAME` option** —
|
|
233
|
+
creating a *new* ATOM only ever makes sense from a host app's own root, so it doesn't include
|
|
234
|
+
`Thecore::Generators::AtomAware` at all. It fails fast with a clear error if `vendor/submodules/`
|
|
235
|
+
doesn't exist yet in the current app, if `NAME` isn't a valid gem name (lowercase letters,
|
|
236
|
+
digits, underscores, hyphens, starting with a letter), or if `vendor/submodules/<NAME>` already
|
|
237
|
+
exists — the last one matters because `rails plugin new`'s own `-f` (force) flag would otherwise
|
|
238
|
+
silently overwrite an existing ATOM with the same name.
|
|
239
|
+
|
|
240
|
+
It prompts (interactively, or via `--summary=`/`--description=`/`--author=`/`--email=`/`--url=`
|
|
241
|
+
with `--non-interactive`) for the same five fields `createATOM.js` always has, then:
|
|
242
|
+
|
|
243
|
+
- Shells to `rails plugin new <path> -fG --skip-gemfile-entry --skip-hotwire --full` (via
|
|
244
|
+
`bundle exec`, for robust gem resolution regardless of environment), producing the standard
|
|
245
|
+
Rails engine skeleton.
|
|
246
|
+
- Asks (`yes?`-style, default yes; `--skip-api-admin-deps` in non-interactive mode) whether to
|
|
247
|
+
add `model_driven_api`/`thecore_ui_rails_admin` as dependencies — at this gem's own ADR 0001
|
|
248
|
+
floor (`~> 3.9`/`~> 3.8`), not `createATOM.js`'s stale `~> 3.1`/`~> 3.2`. Declining adds
|
|
249
|
+
neither; the Scaffold Files/directories below are created either way.
|
|
250
|
+
- Creates the same Scaffold Files/directories `createATOM.js` always has: `db/migrate`, the
|
|
251
|
+
API/RailsAdmin concern directories, `config/initializers`, `config/locales`,
|
|
252
|
+
`lib/root_actions`/`member_actions`/`collection_actions`, JS/CSS asset directories, the
|
|
253
|
+
RailsAdmin main view directory, `.github/workflows` — plus `after_initialize.rb`/`assets.rb`/
|
|
254
|
+
`abilities.rb`, `db/seeds.rb`, and `en.yml`/`it.yml` locale files, all with the same skeleton
|
|
255
|
+
content `createATOM.js` already produces.
|
|
256
|
+
- Generates **both** `.github/workflows/gempush.yml` and `.gitlab-ci.yml` unconditionally — no
|
|
257
|
+
hosting-profile prompt (see ADR 0006 for why: which git host a developer pushes to is a
|
|
258
|
+
per-developer choice this ecosystem already treats as generic). `gempush.yml`'s two
|
|
259
|
+
long-standing bugs are fixed here (a broken `awk` pipeline computing the version string; a
|
|
260
|
+
`version_exists` check referenced in `if:` conditions but never actually set, so the
|
|
261
|
+
tag/publish steps have never run for any ATOM generated this way). The gemspec's
|
|
262
|
+
`allowed_push_host` stays hardcoded to `https://rubygems.org`.
|
|
263
|
+
- Rewrites the gemspec's authors/email/homepage/summary/description/metadata fields via targeted
|
|
264
|
+
in-memory substitutions (not `createATOM.js`'s own blind full-file line rewrite, which —
|
|
265
|
+
verified directly against a current `rails plugin new --full` gemspec — silently drops the
|
|
266
|
+
gem's own `rails` dependency when adding the two Thecore ones; this appends instead of
|
|
267
|
+
replacing).
|
|
268
|
+
- `git init`s the new ATOM directory with one initial commit, then **logs** (never runs) the
|
|
269
|
+
exact follow-up commands to create a remote and `git submodule add` it into the host app —
|
|
270
|
+
worded generically, no GitHub/GitLab assumption (ADR 0006: automating a remote-repo-creating,
|
|
271
|
+
credential-dependent step isn't something this generator does on your behalf).
|
|
272
|
+
- Adds `gem "<name>", path: "vendor/submodules/<name>"` to the host app's own `Gemfile` (via
|
|
273
|
+
Thor's own `gem` action) — skipped, with a warning, if the Gemfile already declares a gem with
|
|
274
|
+
that name (the same real collision the `vendor/submodules/<NAME>` guard above protects against,
|
|
275
|
+
one layer up: a duplicate entry for the same gem breaks the next `bundle install`). Does
|
|
276
|
+
**not** run `bundle install` — same as every other generator in this gem that touches a
|
|
277
|
+
Gemfile.
|
|
278
|
+
- Fetches a `CLAUDE.md` skeleton from `thecore`'s own `samples/ATOM_CLAUDE.md` and writes it as
|
|
279
|
+
the new ATOM's `CLAUDE.md`, via `Thecore::Generators::SampleFetcher` — a shared module using
|
|
280
|
+
the exact same `THECORE_SAMPLES_SOURCE` override mechanism the App template's own asset fetch
|
|
281
|
+
already established (`ENV["THECORE_SAMPLES_SOURCE"]`, defaulting to `thecore`'s `master`
|
|
282
|
+
branch; http(s) fetched over the network, anything else read as a local directory; fails fast
|
|
283
|
+
with a clear message naming the file/source/error on failure). Like the App template's own
|
|
284
|
+
`CLAUDE.md`/`.gitlab-ci.yml` fetches, this 404s against the real default URL until `thecore`'s
|
|
285
|
+
`master` actually carries the commit that added `samples/ATOM_CLAUDE.md` — see the caveat
|
|
286
|
+
above.
|
|
287
|
+
|
|
288
|
+
### `rails generate thecore:collection_action NAME`
|
|
289
|
+
|
|
290
|
+
The third sibling to `thecore:root_action`/`thecore:member_action` — structurally identical
|
|
291
|
+
(same shared `AtomAware`/`CompanionFiles`/`ActionCompanion` modules, same file layout, same
|
|
292
|
+
`--atom=NAME`/idempotent-re-run behavior). Unlike Root/Member, there's no
|
|
293
|
+
`addCollectionAction.js` this ports — `checkPractices.js` audited `collection_actions` but
|
|
294
|
+
nothing ever generated them, so this is new, not a port:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
rails generate thecore:collection_action my_action
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Its own `templates/action.rb.tt` mirrors Root Action's simplicity — `add_action "my_action",
|
|
301
|
+
:base, :collection do ... end`, a minimal GET/JSON example with an `ActivityLogChannel`
|
|
302
|
+
broadcast — rather than the more complex, hand-written `save_filters.rb`/`load_filters.rb`
|
|
303
|
+
pattern already living in `thecore_ui_rails_admin`: a generator's starter template exists to be
|
|
304
|
+
customized from a simple base, not to demonstrate every RailsAdmin `:collection` feature. A
|
|
305
|
+
collection action operates against a model's whole index (all records), as distinct from a
|
|
306
|
+
member action (one record) or a root action (global, no model scope at all).
|
|
307
|
+
|
|
210
308
|
### `rails thecore:check_practices`
|
|
211
309
|
|
|
212
310
|
A Ruby port of `thecore_code_extension`'s `checkPractices.js` — audits **Scaffold Files**
|
|
@@ -237,17 +335,15 @@ Flags combine freely, e.g. `rails thecore:check_practices -- --atom=my_atom --fi
|
|
|
237
335
|
`cattr_accessor :json_attrs` for `Api::`, `rails_admin do` for `RailsAdmin::`). Not fixable —
|
|
238
336
|
regenerating over an existing, hand-edited concern could clobber real customization.
|
|
239
337
|
- **Actions** — scans `root_actions`, `member_actions`, and `collection_actions` (in `lib/` for
|
|
240
|
-
an ATOM, `config/` for the host app), with the same rules for all three
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
*
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
kinds), and a missing locale entry checked against **every** `*.yml` already present in the
|
|
250
|
-
locales directory, not just `en`/`it` (fixable, all three kinds).
|
|
338
|
+
an ATOM, `config/` for the host app), with the same rules for all three. Reports
|
|
339
|
+
missing/broken action-file markers (`RailsAdmin::Config::Actions.add_action`, `http_methods`
|
|
340
|
+
— never fixable), a missing companion view/JS/SCSS or one present but missing its own marker
|
|
341
|
+
(a *missing* companion is fixable for all three kinds — `root_action`/`member_action`/
|
|
342
|
+
`collection_action` — by delegating straight to that kind's own generator's template
|
|
343
|
+
rendering; an *existing* companion missing a marker is never fixable, same reasoning as
|
|
344
|
+
Models), a missing `after_initialize.rb` require line (fixable, all three kinds), and a
|
|
345
|
+
missing locale entry checked against **every** `*.yml` already present in the locales
|
|
346
|
+
directory, not just `en`/`it` (fixable, all three kinds).
|
|
251
347
|
|
|
252
348
|
Default output is human-readable text grouped by file; `--json` emits
|
|
253
349
|
`{ "violations": [{ "file", "line", "message", "severity", "fixable", "code" }] }` — `code` is
|
|
@@ -255,9 +351,10 @@ a stable identifier (e.g. `missing_after_initialize`, `orphan_api_include`,
|
|
|
255
351
|
`missing_companion_view`) a future consumer can filter on without depending on `message` text.
|
|
256
352
|
`--fix` applies every fixable violation in one pass with no confirmation of its own — whoever
|
|
257
353
|
passes it has already decided — then re-scans and reports/exits based on whatever violations
|
|
258
|
-
remain (so a violation this run can't fix, e.g.
|
|
259
|
-
after `--fix`). The task exits
|
|
260
|
-
usable as a CI gate either
|
|
354
|
+
remain (so a violation this run can't fix, e.g. an action file's own broken
|
|
355
|
+
`RailsAdmin::Config::Actions.add_action` marker, still shows up after `--fix`). The task exits
|
|
356
|
+
non-zero whenever any violation remains, zero otherwise, so it's usable as a CI gate either
|
|
357
|
+
with or without `--fix`.
|
|
261
358
|
|
|
262
359
|
### Application Template (`rails new -m`) (thecore_generators#17/#18)
|
|
263
360
|
|
|
@@ -294,9 +391,12 @@ for the full design and why the two stay separate).
|
|
|
294
391
|
- **Devcontainer/CI/CLAUDE.md**, fetched from the `thecore` repo's own `samples/` (single source
|
|
295
392
|
of truth, not duplicated here) and written unconditionally, overwriting whatever the bootstrap
|
|
296
393
|
"Setup Devcontainer" step created: `.devcontainer/*` (base image, plugin mounts, `gh`/`glab`
|
|
297
|
-
CLI config mounts commented out by default), `.gitlab-ci.yml`
|
|
298
|
-
customer-specific paths
|
|
299
|
-
|
|
394
|
+
CLI config mounts commented out by default), **both** `.gitlab-ci.yml` and
|
|
395
|
+
`.github/workflows/ci.yml` (build/test/lint/deploy, no customer-specific paths — no hosting-
|
|
396
|
+
profile prompt, same git-hosting-agnostic reasoning ADR 0006 established for `thecore:atom`'s
|
|
397
|
+
own dual-CI generation, extended here by ADR 0007/thecore_generators#25), and `CLAUDE.md`
|
|
398
|
+
(universal sections only, project-specific sections left as TODO placeholders). The fetch
|
|
399
|
+
location is one overridable point,
|
|
300
400
|
`ENV["THECORE_SAMPLES_SOURCE"]`, defaulting to the raw GitHub URL for `thecore`'s `samples/`
|
|
301
401
|
on `master` (`thecore`'s actual default branch).
|
|
302
402
|
- **The standard installer chain** (`devise:install`, `rails_admin:install`, `active_storage:
|
|
@@ -304,8 +404,16 @@ for the full design and why the two stay separate).
|
|
|
304
404
|
each preceded by the necessary `bundle install`) is genuinely optional, gated behind an
|
|
305
405
|
interactive prompt (`yes?`, wrapped in `after_bundle` so it only ever runs once the gems
|
|
306
406
|
above are actually bundled) — a developer bootstrapping without network access can decline
|
|
307
|
-
and run these by hand later.
|
|
308
|
-
|
|
407
|
+
and run these by hand later.
|
|
408
|
+
- **Non-interactive/unattended mode**: set `THECORE_APP_TEMPLATE_NON_INTERACTIVE` (any non-blank
|
|
409
|
+
value) to bypass the installer-chain prompt above. When set, `THECORE_APP_TEMPLATE_RUN_INSTALLERS`
|
|
410
|
+
(`"true"` or `"false"`) is required — the template aborts with a clear message if it's missing
|
|
411
|
+
or unrecognized, rather than guessing. Example:
|
|
412
|
+
```bash
|
|
413
|
+
THECORE_APP_TEMPLATE_NON_INTERACTIVE=1 THECORE_APP_TEMPLATE_RUN_INSTALLERS=true \
|
|
414
|
+
rails new myapp --database=postgresql --asset-pipeline=sprockets \
|
|
415
|
+
-m https://raw.githubusercontent.com/gabrieletassoni/thecore_generators/release/3/lib/templates/app_template.rb
|
|
416
|
+
```
|
|
309
417
|
|
|
310
418
|
## Installation
|
|
311
419
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "generators/thecore/workspace_context"
|
|
2
|
+
require "generators/thecore/tty_detection"
|
|
2
3
|
|
|
3
4
|
module Thecore
|
|
4
5
|
module Generators
|
|
@@ -114,7 +115,7 @@ module Thecore
|
|
|
114
115
|
# includer was explicitly told not to prompt via --non-interactive) -
|
|
115
116
|
# default straight to has_many, per ADR 0003.
|
|
116
117
|
def interactive_association_prompt?
|
|
117
|
-
!options[:non_interactive] &&
|
|
118
|
+
!options[:non_interactive] && Thecore::Generators::TtyDetection.real_tty?
|
|
118
119
|
end
|
|
119
120
|
|
|
120
121
|
def concern_path_for(target_class_name)
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
require "rails/generators/named_base"
|
|
2
|
+
require "generators/thecore/tty_detection"
|
|
3
|
+
require "generators/thecore/sample_fetcher"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Thecore
|
|
8
|
+
module Generators
|
|
9
|
+
# `rails generate thecore:atom NAME` (thecore_generators#20, per ADR 0006 in the
|
|
10
|
+
# thecore repo) — a Ruby port of thecore_code_extension's createATOM.js, producing
|
|
11
|
+
# a complete, working ATOM end-to-end from a terminal: no VS Code, no extension
|
|
12
|
+
# required.
|
|
13
|
+
#
|
|
14
|
+
# Unlike every other generator in this gem, AtomGenerator does NOT include
|
|
15
|
+
# Thecore::Generators::AtomAware and takes no --atom=NAME option — creating a
|
|
16
|
+
# *new* ATOM is only ever a host-app-root operation (it makes no sense to run
|
|
17
|
+
# "from inside" an ATOM that doesn't exist yet). destination_root therefore stays
|
|
18
|
+
# fixed at the host app root for the whole generator; every path this class writes
|
|
19
|
+
# is expressed relative to that root via #atom_root ("vendor/submodules/<name>"),
|
|
20
|
+
# not via AtomAware's destination_root-redirection trick.
|
|
21
|
+
#
|
|
22
|
+
# Two Gemfiles are in play here — the host app's own (destination_root/Gemfile)
|
|
23
|
+
# and the freshly-scaffolded ATOM's own (vendor/submodules/<name>/Gemfile) — which
|
|
24
|
+
# is why Rails::Generators::Actions#gem (used once, in #add_gem_to_host_gemfile)
|
|
25
|
+
# can't be reused for the ATOM's own Gemfile: #gem's own implementation always
|
|
26
|
+
# writes through `in_root { ... }`, and Thor::Actions#in_root is hardcoded to
|
|
27
|
+
# `@destination_stack.first` (the *original* destination_root), not whatever
|
|
28
|
+
# #inside might currently have pushed — so it can never be redirected to a nested
|
|
29
|
+
# path. The ATOM's own Gemfile is instead mutated via plain #append_to_file calls
|
|
30
|
+
# against an explicit relative path.
|
|
31
|
+
class AtomGenerator < Rails::Generators::NamedBase
|
|
32
|
+
class_option :non_interactive, type: :boolean, default: false,
|
|
33
|
+
desc: "Skip all interactive prompts; every required value must be supplied " \
|
|
34
|
+
"via --summary/--description/--author/--email/--url, and the " \
|
|
35
|
+
"API/Admin dependency choice defaults to included unless " \
|
|
36
|
+
"--skip-api-admin-deps is also passed"
|
|
37
|
+
class_option :summary, type: :string, default: nil,
|
|
38
|
+
desc: "The ATOM's one-line summary (required with --non-interactive)"
|
|
39
|
+
class_option :description, type: :string, default: nil,
|
|
40
|
+
desc: "The ATOM's longer description (required with --non-interactive)"
|
|
41
|
+
class_option :author, type: :string, default: nil,
|
|
42
|
+
desc: "The ATOM's author name (required with --non-interactive)"
|
|
43
|
+
class_option :email, type: :string, default: nil,
|
|
44
|
+
desc: "The ATOM author's email (required with --non-interactive)"
|
|
45
|
+
class_option :url, type: :string, default: nil,
|
|
46
|
+
desc: "The ATOM's homepage URL (required with --non-interactive)"
|
|
47
|
+
class_option :skip_api_admin_deps, type: :boolean, default: false,
|
|
48
|
+
desc: "Don't add model_driven_api/thecore_ui_rails_admin as dependencies " \
|
|
49
|
+
"(only consulted with --non-interactive; interactively this is its " \
|
|
50
|
+
"own yes/no prompt, default yes)"
|
|
51
|
+
|
|
52
|
+
source_root File.expand_path("templates", __dir__)
|
|
53
|
+
|
|
54
|
+
# Validated the same way as createATOM.js's own six prompts: every field must
|
|
55
|
+
# be present; email must contain "@"; url must start with "http". Order matches
|
|
56
|
+
# the original prompt sequence.
|
|
57
|
+
REQUIRED_STRING_FIELDS = {
|
|
58
|
+
"summary" => ->(v) { !v.to_s.strip.empty? },
|
|
59
|
+
"description" => ->(v) { !v.to_s.strip.empty? },
|
|
60
|
+
"author" => ->(v) { !v.to_s.strip.empty? },
|
|
61
|
+
"email" => ->(v) { v.to_s.include?("@") },
|
|
62
|
+
"url" => ->(v) { v.to_s.start_with?("http") },
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
# This gem's own ADR 0001 floor (matching the App template's already-corrected
|
|
66
|
+
# versions) — not createATOM.js's stale "~> 3.1"/"~> 3.2".
|
|
67
|
+
MODEL_DRIVEN_API_VERSION = "~> 3.9"
|
|
68
|
+
THECORE_UI_RAILS_ADMIN_VERSION = "~> 3.8"
|
|
69
|
+
|
|
70
|
+
# Faithful port of templates/createATOM/after_initialize.rb /
|
|
71
|
+
# templates/createATOM/assets.rb in thecore_code_extension — static content,
|
|
72
|
+
# no per-ATOM interpolation, so these are plain constants rather than .tt files.
|
|
73
|
+
AFTER_INITIALIZE_CONTENT = <<~RUBY
|
|
74
|
+
Rails.application.configure do
|
|
75
|
+
config.after_initialize do
|
|
76
|
+
# For example, it can be used to load a root action defined in lib, for example:
|
|
77
|
+
# require 'root_actions/tcp_debug'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
RUBY
|
|
81
|
+
|
|
82
|
+
ASSETS_CONTENT = <<~RUBY
|
|
83
|
+
# PLEASE, uncomment if needed.
|
|
84
|
+
# For Example: in the case there's a root action called tcp_debug, add the following lines to include css and javascripts for auto loading:
|
|
85
|
+
# Rails.application.config.assets.precompile += %w(
|
|
86
|
+
# main_tcp_debug.js
|
|
87
|
+
# main_tcp_debug.css
|
|
88
|
+
# )
|
|
89
|
+
RUBY
|
|
90
|
+
|
|
91
|
+
# Faithful port of createATOM.js's addCICDFiles gempush.yml, with its two
|
|
92
|
+
# long-standing bugs fixed (thecore_generators#20 acceptance criteria): the awk
|
|
93
|
+
# pipeline computing the version string had a stray, unmatched `)` instead of a
|
|
94
|
+
# closing `}'`; and `version_exists` was referenced in two steps' `if:`
|
|
95
|
+
# conditions but never actually set anywhere (the "check" step only ever did
|
|
96
|
+
# `echo $?`), so those two steps have never run for any ATOM generated this way.
|
|
97
|
+
# Fixed here by actually writing `version_exists` to $GITHUB_ENV based on
|
|
98
|
+
# whether a git tag for the computed version already exists - kept as an
|
|
99
|
+
# `env.*`-style if: condition (not switched to `steps.*.outputs.*`) to stay as
|
|
100
|
+
# close to the original's structure as the fix allows.
|
|
101
|
+
GEMPUSH_YML_CONTENT = <<~YAML
|
|
102
|
+
name: Ruby Gem
|
|
103
|
+
on: push
|
|
104
|
+
jobs:
|
|
105
|
+
build:
|
|
106
|
+
name: Build + Publish
|
|
107
|
+
runs-on: ubuntu-latest
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/checkout@v3
|
|
110
|
+
- name: Check if version already exists
|
|
111
|
+
run: |
|
|
112
|
+
version=$(grep -oP 'VERSION = "\\K[^"]+' lib/*/version.rb | awk -F'.' '{print $1"."$2"."$3}')
|
|
113
|
+
git fetch --unshallow --tags
|
|
114
|
+
if git rev-parse "$version" >/dev/null 2>&1; then
|
|
115
|
+
echo "version_exists=true" >> "$GITHUB_ENV"
|
|
116
|
+
else
|
|
117
|
+
echo "version_exists=false" >> "$GITHUB_ENV"
|
|
118
|
+
fi
|
|
119
|
+
- name: Set git tag
|
|
120
|
+
if: env.version_exists == 'false'
|
|
121
|
+
run: |
|
|
122
|
+
git config --local user.email "noreply@alchemic.it"
|
|
123
|
+
git config --local user.name "AlchemicIT"
|
|
124
|
+
version=$(grep -oP 'VERSION = "\\K[^"]+' lib/*/version.rb | awk -F'.' '{print $1"."$2"."$3}')
|
|
125
|
+
git tag -a $version -m "Version $version"
|
|
126
|
+
git push --tags
|
|
127
|
+
- name: Publish to RubyGems
|
|
128
|
+
if: env.version_exists == 'false'
|
|
129
|
+
env:
|
|
130
|
+
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
|
|
131
|
+
run: |
|
|
132
|
+
mkdir -p $HOME/.gem
|
|
133
|
+
touch $HOME/.gem/credentials
|
|
134
|
+
chmod 0600 $HOME/.gem/credentials
|
|
135
|
+
printf -- "---\\n:rubygems_api_key: ${GEM_HOST_API_KEY}\\n" > $HOME/.gem/credentials
|
|
136
|
+
gem build *.gemspec
|
|
137
|
+
gem push *.gem
|
|
138
|
+
YAML
|
|
139
|
+
|
|
140
|
+
SCAFFOLD_DIRECTORIES = %w[
|
|
141
|
+
db/migrate
|
|
142
|
+
app/models/concerns/api
|
|
143
|
+
app/models/concerns/rails_admin
|
|
144
|
+
config/initializers
|
|
145
|
+
config/locales
|
|
146
|
+
lib/root_actions
|
|
147
|
+
lib/member_actions
|
|
148
|
+
lib/collection_actions
|
|
149
|
+
app/assets/javascripts
|
|
150
|
+
app/assets/stylesheets
|
|
151
|
+
app/views/rails_admin/main
|
|
152
|
+
.github/workflows
|
|
153
|
+
].freeze
|
|
154
|
+
|
|
155
|
+
# Gem-name convention (matches real examples in this very ecosystem, including
|
|
156
|
+
# the hyphenated `thecore-spot-overrides`) - lowercase, starting with a letter,
|
|
157
|
+
# letters/digits/underscore/hyphen only. Deliberately stricter than NamedBase's
|
|
158
|
+
# own permissive `name` parsing: without this, a name containing a space or
|
|
159
|
+
# shell metacharacter would flow straight into the unescaped shell-out in
|
|
160
|
+
# #create_rails_engine (word-splitting or, worse, executing arbitrary shell),
|
|
161
|
+
# and a namespaced name (`acme/widget`) would desync #atom_root (which uses
|
|
162
|
+
# only #file_name, "widget") from #class_name (which uses the full namespaced
|
|
163
|
+
# "Acme::Widget"), breaking the abilities.rb template.
|
|
164
|
+
NAME_PATTERN = /\A[a-z][a-z0-9_-]*\z/
|
|
165
|
+
|
|
166
|
+
def validate_atom_name!
|
|
167
|
+
return if file_name.match?(NAME_PATTERN)
|
|
168
|
+
|
|
169
|
+
raise Thor::Error,
|
|
170
|
+
"'#{file_name}' is not a valid ATOM name - use lowercase letters, digits, " \
|
|
171
|
+
"underscores, or hyphens, starting with a letter (e.g. tcp_debugger)."
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def ensure_submodules_dir_exists!
|
|
175
|
+
return if File.directory?(File.join(destination_root, "vendor", "submodules"))
|
|
176
|
+
|
|
177
|
+
raise Thor::Error,
|
|
178
|
+
"vendor/submodules does not exist under #{destination_root} - run `rails generate " \
|
|
179
|
+
"thecore:atom` from a Thecore host app root that already has it (see the App " \
|
|
180
|
+
"application template, thecore_generators#17/#18) before creating an ATOM."
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# `rails plugin new`'s own `-f` (force) suppresses its normal file-collision
|
|
184
|
+
# prompt, so without this guard a name colliding with an existing ATOM (a real
|
|
185
|
+
# scenario in this very host app: `vendor/submodules/mytask` already exists)
|
|
186
|
+
# would silently overwrite that ATOM's working tree with freshly-generated
|
|
187
|
+
# plugin skeleton files - caught during review, reproduced directly against
|
|
188
|
+
# this app's own real `mytask` submodule.
|
|
189
|
+
def ensure_atom_does_not_already_exist!
|
|
190
|
+
return unless File.exist?(File.join(destination_root, atom_root))
|
|
191
|
+
|
|
192
|
+
raise Thor::Error,
|
|
193
|
+
"#{atom_root} already exists - choose a different name, or remove it first if you " \
|
|
194
|
+
"really mean to regenerate it."
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def validate_non_interactive_options!
|
|
198
|
+
return unless effectively_non_interactive?
|
|
199
|
+
|
|
200
|
+
missing = REQUIRED_STRING_FIELDS.reject { |key, valid| valid.call(options[key]) }.keys
|
|
201
|
+
return if missing.empty?
|
|
202
|
+
|
|
203
|
+
raise Thor::Error,
|
|
204
|
+
"Missing required flags for --non-interactive: #{missing.map { |k| "--#{k}" }.join(", ")}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def collect_metadata
|
|
208
|
+
@summary = required_field("summary", "the summary of the ATOM, i.e. TCP Debugger")
|
|
209
|
+
@description = required_field("description", "the description of the ATOM, i.e. TCP Debugger")
|
|
210
|
+
@author = required_field("author", "the author of the ATOM, i.e. Alchemic IT")
|
|
211
|
+
@email = required_field("email", "the email of the ATOM author")
|
|
212
|
+
@url = required_field("url", "the url of the ATOM")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def collect_api_admin_deps_choice
|
|
216
|
+
@include_api_admin_deps =
|
|
217
|
+
if effectively_non_interactive?
|
|
218
|
+
!options[:skip_api_admin_deps]
|
|
219
|
+
else
|
|
220
|
+
ask(
|
|
221
|
+
"Include model_driven_api/thecore_ui_rails_admin as dependencies?",
|
|
222
|
+
default: "yes", limited_to: %w[yes no]
|
|
223
|
+
) == "yes"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Faithful to createATOM.js's own `rails plugin new "<path>" -fG
|
|
228
|
+
# --skip-gemfile-entry --skip-hotwire --full` invocation, cwd'd to
|
|
229
|
+
# vendor/submodules (via #inside, which - unlike file-writing actions below -
|
|
230
|
+
# genuinely changes the OS process's cwd for #run's sake). `bundle exec` is a
|
|
231
|
+
# deliberate addition over the JS original (which shells a bare `rails`,
|
|
232
|
+
# relying entirely on whatever's globally on PATH): it makes gem resolution
|
|
233
|
+
# explicit rather than incidental, and costs nothing in the real host-app case,
|
|
234
|
+
# where cwd already sits under that app's own Gemfile either way. `file_name`
|
|
235
|
+
# is Shellwords-escaped even though #validate_atom_name! already restricts it
|
|
236
|
+
# to a shell-safe character set - defense in depth, matching the same care
|
|
237
|
+
# #git_init_and_commit already takes with the free-text @author/@email.
|
|
238
|
+
def create_rails_engine
|
|
239
|
+
inside("vendor/submodules") do
|
|
240
|
+
run("bundle exec rails plugin new #{Shellwords.escape(file_name)} " \
|
|
241
|
+
"-fG --skip-gemfile-entry --skip-hotwire --full",
|
|
242
|
+
abort_on_failure: true)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# #create_file already `mkdir_p`s its own parent directory, so a separate
|
|
247
|
+
# #empty_directory call per entry would just be redundant work (and, for the
|
|
248
|
+
# 3 of these 12 that #create_scaffold_files/#create_locale_files/
|
|
249
|
+
# #create_ci_files populate with a real file moments later, entirely so).
|
|
250
|
+
def create_scaffold_directories
|
|
251
|
+
SCAFFOLD_DIRECTORIES.each { |dir| create_file(File.join(atom_root, dir, ".keep")) }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def create_scaffold_files
|
|
255
|
+
create_file File.join(atom_root, "config/initializers/after_initialize.rb"), AFTER_INITIALIZE_CONTENT
|
|
256
|
+
create_file File.join(atom_root, "config/initializers/add_to_db_migration.rb"),
|
|
257
|
+
"Rails.application.config.paths['db/migrate'] << File.expand_path(\"../../db/migrate\", __dir__)\n"
|
|
258
|
+
create_file File.join(atom_root, "config/initializers/assets.rb"), ASSETS_CONTENT
|
|
259
|
+
template "abilities.rb.tt", File.join(atom_root, "config/initializers/abilities.rb")
|
|
260
|
+
template "seeds.rb.tt", File.join(atom_root, "db/seeds.rb")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Bare "en:\n"/"it:\n", matching the exact convention
|
|
264
|
+
# Thecore::Generators::CompanionFiles#write_action_locale_entries! already uses
|
|
265
|
+
# for this identical "no locale file yet" bootstrap case elsewhere in this gem
|
|
266
|
+
# - not the YAML-document-with-null-value shape `{"en"=>nil}.to_yaml` produces,
|
|
267
|
+
# which is equivalent once parsed but an unnecessary second on-disk convention
|
|
268
|
+
# for the same thing.
|
|
269
|
+
def create_locale_files
|
|
270
|
+
create_file File.join(atom_root, "config/locales/en.yml"), "en:\n"
|
|
271
|
+
create_file File.join(atom_root, "config/locales/it.yml"), "it:\n"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def create_ci_files
|
|
275
|
+
create_file File.join(atom_root, ".github/workflows/gempush.yml"), GEMPUSH_YML_CONTENT
|
|
276
|
+
|
|
277
|
+
gitlab_ci = {
|
|
278
|
+
"image" => "gabrieletassoni/vscode-devcontainers-thecore:3",
|
|
279
|
+
"variables" => {
|
|
280
|
+
"GITLAB_EMAIL" => @email,
|
|
281
|
+
"GITLAB_USER_NAME" => @author,
|
|
282
|
+
"GITLAB_GEM_REPO_TARGET" => 'https://${GEM_HOST}/',
|
|
283
|
+
"GEM_HOST_API_KEY" => '${GEMS_REPO_CREDENTIALS}',
|
|
284
|
+
},
|
|
285
|
+
"stages" => %w[build release],
|
|
286
|
+
"build_gem" => {
|
|
287
|
+
"rules" => [{ "if" => "$CI_COMMIT_TAG", "when" => "never" }, { "when" => "always" }],
|
|
288
|
+
"stage" => "build",
|
|
289
|
+
"script" => ["/usr/bin/gem-compile.sh"],
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
create_file File.join(atom_root, ".gitlab-ci.yml"), gitlab_ci.to_yaml
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def setup_gemfile
|
|
296
|
+
gemfile_addition = +"\ngem 'pg'\n"
|
|
297
|
+
gemfile_addition << "gem 'model_driven_api', '#{MODEL_DRIVEN_API_VERSION}'\n" \
|
|
298
|
+
"gem 'thecore_ui_rails_admin', '#{THECORE_UI_RAILS_ADMIN_VERSION}'\n" if @include_api_admin_deps
|
|
299
|
+
append_to_file File.join(atom_root, "Gemfile"), gemfile_addition
|
|
300
|
+
|
|
301
|
+
return unless @include_api_admin_deps
|
|
302
|
+
|
|
303
|
+
append_to_file File.join(atom_root, "lib", "#{file_name}.rb"),
|
|
304
|
+
"\nrequire 'model_driven_api'\nrequire 'thecore_ui_rails_admin'\n"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# One read, one write, one pass of in-memory substitutions - not createATOM.js's
|
|
308
|
+
# own blind "rewrite every line, branching on substring" approach (the current
|
|
309
|
+
# `rails plugin new --full` gemspec template, verified directly against a real
|
|
310
|
+
# generation rather than assumed from the JS original, has moved on
|
|
311
|
+
# significantly: new `homepage_uri`/`license` lines, different summary/
|
|
312
|
+
# description wording - a full-file line-by-line port would silently stop
|
|
313
|
+
# matching several of these fields), and not 8 separate #gsub_file calls
|
|
314
|
+
# either (each its own full read-modify-write cycle against the same small
|
|
315
|
+
# file - wasted I/O for no benefit). One deliberate correctness fix over the
|
|
316
|
+
# original along the way: the JS's `.add_dependency` branch *replaces* the
|
|
317
|
+
# whole line, which happens to be `spec.add_dependency "rails", ...` in
|
|
318
|
+
# current Rails - silently dropping the gem's own Rails dependency entirely.
|
|
319
|
+
# This appends the two Thecore dependencies right after that line instead of
|
|
320
|
+
# replacing it.
|
|
321
|
+
def setup_gemspec
|
|
322
|
+
gemspec_path = File.join(atom_root, "#{file_name}.gemspec")
|
|
323
|
+
content = File.read(File.join(destination_root, gemspec_path))
|
|
324
|
+
|
|
325
|
+
content = content.sub(/^(\s*spec\.add_dependency\s+["']rails["'].*)$/) do
|
|
326
|
+
next Regexp.last_match(1) unless @include_api_admin_deps
|
|
327
|
+
|
|
328
|
+
"#{Regexp.last_match(1)}\n spec.add_dependency \"model_driven_api\", \"#{MODEL_DRIVEN_API_VERSION}\"\n" \
|
|
329
|
+
" spec.add_dependency \"thecore_ui_rails_admin\", \"#{THECORE_UI_RAILS_ADMIN_VERSION}\""
|
|
330
|
+
end
|
|
331
|
+
content = content.sub(/^\s*spec\.authors\s*=.*$/, " spec.authors = [#{@author.to_s.inspect}]")
|
|
332
|
+
content = content.sub(/^\s*spec\.email\s*=.*$/, " spec.email = [#{@email.to_s.inspect}]")
|
|
333
|
+
content = content.sub(/^\s*spec\.homepage\s*=.*$/, " spec.homepage = #{@url.to_s.inspect}")
|
|
334
|
+
content = content.sub(/^\s*spec\.summary\s*=.*$/, " spec.summary = #{@summary.to_s.inspect}")
|
|
335
|
+
content = content.sub(/^\s*spec\.description\s*=.*$/, " spec.description = #{@description.to_s.inspect}")
|
|
336
|
+
content = content.sub(/^\s*spec\.metadata\["allowed_push_host"\]\s*=.*$/,
|
|
337
|
+
' spec.metadata["allowed_push_host"] = "https://rubygems.org"')
|
|
338
|
+
content = content.sub(/^\s*spec\.metadata\["source_code_uri"\]\s*=.*$/,
|
|
339
|
+
' spec.metadata["source_code_uri"] = spec.homepage')
|
|
340
|
+
content = content.sub(/^\s*spec\.metadata\["changelog_uri"\]\s*=.*$/,
|
|
341
|
+
' spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"')
|
|
342
|
+
|
|
343
|
+
create_file gemspec_path, content, force: true
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Completes this generator's scope (thecore_generators#22, ADR 0006): fetches
|
|
347
|
+
# thecore's own samples/ATOM_CLAUDE.md and writes it as the new ATOM's
|
|
348
|
+
# CLAUDE.md, via the shared Thecore::Generators::SampleFetcher - the same
|
|
349
|
+
# THECORE_SAMPLES_SOURCE mechanism the App template's own (separate, still
|
|
350
|
+
# independent - see sample_fetcher.rb's own header for why) asset fetch
|
|
351
|
+
# already established.
|
|
352
|
+
#
|
|
353
|
+
# A fetch failure here raises SystemExit (via Kernel#abort), not Thor::Error
|
|
354
|
+
# like every guard method above - a deliberate inconsistency, not an
|
|
355
|
+
# oversight: it matches the App template's own established convention for
|
|
356
|
+
# this exact class of failure (an external, this-run-only fetch, as opposed
|
|
357
|
+
# to a validation the generator could have caught before doing any real
|
|
358
|
+
# work), and the ticket's own acceptance criteria asks for "fail-fast abort,"
|
|
359
|
+
# not a Thor::Error. A failure here does leave the partially-generated
|
|
360
|
+
# vendor/submodules/<name> directory behind (rails plugin new/the Gemfile/
|
|
361
|
+
# gemspec edits already succeeded) - #ensure_atom_does_not_already_exist!
|
|
362
|
+
# then refuses a same-named retry until it's removed by hand; the abort
|
|
363
|
+
# message says so.
|
|
364
|
+
#
|
|
365
|
+
# NOTE: like the App template's own fetch of thecore/samples/CLAUDE.md
|
|
366
|
+
# before it, this 404s against the real default GitHub URL until thecore's
|
|
367
|
+
# `master` actually carries the commit that added samples/ATOM_CLAUDE.md
|
|
368
|
+
# (thecore#18) - as of this gem's 3.11.0 release that commit exists only in
|
|
369
|
+
# a local `thecore` checkout, not yet pushed. Same operational sequencing
|
|
370
|
+
# note as the App template's own CLAUDE.md section: push `thecore` before
|
|
371
|
+
# relying on the default in production.
|
|
372
|
+
def fetch_claude_md
|
|
373
|
+
Thecore::Generators::SampleFetcher.fetch_thecore_sample(
|
|
374
|
+
self, "ATOM_CLAUDE.md", File.join(atom_root, "CLAUDE.md"), label: "the thecore:atom generator"
|
|
375
|
+
)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# `-fG` (`rails plugin new`'s own force+skip-git flags) means no git repo and no
|
|
379
|
+
# .gitignore exist yet at this point - a deliberate gap in createATOM.js this
|
|
380
|
+
# ticket narrows, not fully closes (ADR 0006): a local, safe `git init` +
|
|
381
|
+
# initial commit, but remote creation and `git submodule add` stay a logged,
|
|
382
|
+
# human-run follow-up rather than something this generator automates. No
|
|
383
|
+
# .gitignore is written (out of this ticket's scope - see ADR 0006/the ticket's
|
|
384
|
+
# own acceptance criteria, which doesn't list one): verified directly that a
|
|
385
|
+
# fresh `rails plugin new --full` output has no log/tmp/sqlite artifacts yet to
|
|
386
|
+
# need ignoring - nothing has been bundled or run against the dummy app at this
|
|
387
|
+
# point, so the initial commit is clean regardless.
|
|
388
|
+
def git_init_and_commit
|
|
389
|
+
atom_path = File.join(destination_root, atom_root)
|
|
390
|
+
committed = inside(atom_root) do
|
|
391
|
+
run("git init -q -b master", abort_on_failure: true)
|
|
392
|
+
run("git add -A", abort_on_failure: true)
|
|
393
|
+
# Not abort_on_failure: a freshly-generated tree always has something to
|
|
394
|
+
# commit in normal use, but `git commit` failing (e.g. "nothing to
|
|
395
|
+
# commit", however that state arose) shouldn't kill the whole process via
|
|
396
|
+
# a raw, unexplained Kernel#abort when every file this generator actually
|
|
397
|
+
# promises has already been written successfully by this point. The
|
|
398
|
+
# result is still checked below, so a real failure changes what gets
|
|
399
|
+
# logged rather than being silently treated as success.
|
|
400
|
+
run(
|
|
401
|
+
"git -c user.name=#{Shellwords.escape(@author)} -c user.email=#{Shellwords.escape(@email)} " \
|
|
402
|
+
'commit -q -m "Initial commit"',
|
|
403
|
+
abort_on_failure: false
|
|
404
|
+
)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
if committed
|
|
408
|
+
say_status :next_steps, <<~MSG.strip, :yellow
|
|
409
|
+
#{file_name} is git-initialized locally with one commit, but has no remote yet. To finish wiring it in:
|
|
410
|
+
1. Create a repository for it on the git host of your choice (GitHub, GitLab, ...)
|
|
411
|
+
2. cd #{atom_path} && git remote add origin <remote-url> && git push -u origin master
|
|
412
|
+
3. From this app's root: git submodule add <remote-url> vendor/submodules/#{file_name}
|
|
413
|
+
MSG
|
|
414
|
+
else
|
|
415
|
+
say_status :warning, <<~MSG.strip, :red
|
|
416
|
+
#{file_name} was git-initialized, but `git commit` did not succeed - it has no commit
|
|
417
|
+
yet. Check the output above, commit by hand once resolved, then follow the usual
|
|
418
|
+
steps to create a remote and `git submodule add` it into this app.
|
|
419
|
+
MSG
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Guards against the same name-collision scenario #ensure_atom_does_not_already_exist!
|
|
424
|
+
# protects the ATOM directory itself from: a host Gemfile that already
|
|
425
|
+
# declares a same-named gem (real in this very host app - `mytask` is
|
|
426
|
+
# resolved from a gem server today) would otherwise get a second, conflicting
|
|
427
|
+
# `gem "mytask", path: ...` line appended, and the next `bundle install`
|
|
428
|
+
# fails outright ("You cannot specify the same gem twice"). In the normal
|
|
429
|
+
# case (no prior entry) this is unreachable in practice anyway, since
|
|
430
|
+
# #ensure_atom_does_not_already_exist! already refuses a name whose
|
|
431
|
+
# vendor/submodules/<name> directory exists - kept as its own explicit check
|
|
432
|
+
# since a Gemfile entry and a vendor/submodules directory are two independent
|
|
433
|
+
# pieces of state that could in principle drift apart.
|
|
434
|
+
def add_gem_to_host_gemfile
|
|
435
|
+
gemfile_path = File.join(destination_root, "Gemfile")
|
|
436
|
+
if File.exist?(gemfile_path) && File.read(gemfile_path).match?(/^\s*gem\s+["']#{Regexp.escape(file_name)}["']/)
|
|
437
|
+
say_status :skip, "Gemfile already declares '#{file_name}' - not adding a second entry", :yellow
|
|
438
|
+
return
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
gem file_name, path: "vendor/submodules/#{file_name}"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
private
|
|
445
|
+
|
|
446
|
+
def atom_root
|
|
447
|
+
File.join("vendor", "submodules", file_name)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# True whenever prompting for input isn't viable: --non-interactive was
|
|
451
|
+
# passed explicitly, or (via the shared Thecore::Generators::TtyDetection,
|
|
452
|
+
# also used by AssociationWiring's own `interactive_association_prompt?`)
|
|
453
|
+
# there's no real TTY behind stdin/stdout at all - a CI runner or a
|
|
454
|
+
# shelled-out child process that simply forgot the flag. Without this,
|
|
455
|
+
# #required_field's own `ask`-in-a-loop would spin forever re-prompting a
|
|
456
|
+
# stream that can never supply input, since a closed/EOF stdin makes Thor's
|
|
457
|
+
# `ask` return nil immediately.
|
|
458
|
+
def effectively_non_interactive?
|
|
459
|
+
options[:non_interactive] || !Thecore::Generators::TtyDetection.real_tty?
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Interactive: loop with Thor's own `ask` until the validator passes, echoing
|
|
463
|
+
# the same "not valid, try again" wording createATOM.js's input boxes used.
|
|
464
|
+
# Non-interactive (explicit or TTY-detected): already validated present by
|
|
465
|
+
# #validate_non_interactive_options! (a task method that always runs first),
|
|
466
|
+
# so this simply reads the option.
|
|
467
|
+
def required_field(key, prompt_hint)
|
|
468
|
+
return options[key] if effectively_non_interactive?
|
|
469
|
+
|
|
470
|
+
validate = REQUIRED_STRING_FIELDS.fetch(key)
|
|
471
|
+
loop do
|
|
472
|
+
value = ask("Enter #{prompt_hint}:")
|
|
473
|
+
return value if validate.call(value)
|
|
474
|
+
|
|
475
|
+
say_status :error, "The #{key} is not valid. Please try again.", :red
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Abilities
|
|
2
|
+
class <%= class_name %>
|
|
3
|
+
include CanCan::Ability
|
|
4
|
+
def initialize user
|
|
5
|
+
if user.present?
|
|
6
|
+
# Users' abilities
|
|
7
|
+
# Example: can :read, ModelName
|
|
8
|
+
# Example: can [:read, :create], ModelName
|
|
9
|
+
if user.admin?
|
|
10
|
+
# Admins' abilities
|
|
11
|
+
# Example: can :manage, :all
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
puts "Seeding Data into DB from <%= file_name %>"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require "rails/generators/named_base"
|
|
2
|
+
require "generators/thecore/atom_aware"
|
|
3
|
+
require "generators/thecore/companion_files"
|
|
4
|
+
require "generators/thecore/action_companion"
|
|
5
|
+
|
|
6
|
+
module Thecore
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails generate thecore:collection_action NAME` (thecore_generators#21,
|
|
9
|
+
# per ADR 0006 in the thecore repo) — the third sibling to
|
|
10
|
+
# RootActionGenerator/MemberActionGenerator. Unlike those two, there is
|
|
11
|
+
# no prior thecore_code_extension JS command this ports (no
|
|
12
|
+
# `addCollectionAction.js` ever existed — collection_actions were only
|
|
13
|
+
# ever audited by check_practices, never generated), so its own
|
|
14
|
+
# `templates/action.rb.tt` deliberately mirrors RootActionGenerator's
|
|
15
|
+
# simplicity (a minimal GET/JSON example with an ActivityLogChannel
|
|
16
|
+
# broadcast) rather than the real, more complex hand-written
|
|
17
|
+
# `save_filters.rb`/`load_filters.rb` pattern already living in
|
|
18
|
+
# thecore_ui_rails_admin — a generator's starter template exists to be
|
|
19
|
+
# customized from a simple base, not to demonstrate every RailsAdmin
|
|
20
|
+
# :collection feature.
|
|
21
|
+
#
|
|
22
|
+
# Structurally identical to RootActionGenerator/MemberActionGenerator:
|
|
23
|
+
# same three includes, same thin task-method sequence (see
|
|
24
|
+
# ActionCompanion's own comment for why those can't be shared further),
|
|
25
|
+
# only `action_kind` and this class's own templates differ. No changes
|
|
26
|
+
# needed anywhere in AtomAware/CompanionFiles/ActionCompanion —
|
|
27
|
+
# `action_kind "collection_action"` alone is enough for placement
|
|
28
|
+
# (lib/collection_actions or config/collection_actions),
|
|
29
|
+
# pluralization, and validation wording to fall out correctly.
|
|
30
|
+
class CollectionActionGenerator < Rails::Generators::NamedBase
|
|
31
|
+
include Thecore::Generators::AtomAware
|
|
32
|
+
include Thecore::Generators::CompanionFiles
|
|
33
|
+
include Thecore::Generators::ActionCompanion
|
|
34
|
+
|
|
35
|
+
action_kind "collection_action"
|
|
36
|
+
|
|
37
|
+
source_root File.expand_path("templates", __dir__)
|
|
38
|
+
|
|
39
|
+
def validate_action_name!
|
|
40
|
+
validate_action_name_for_kind!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_action_file
|
|
44
|
+
template "action.rb.tt", action_file_path
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create_view_js_scss_companions
|
|
48
|
+
render_view_js_scss_companions!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_after_initialize_require
|
|
52
|
+
ensure_after_initialize_require!(require_line)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def add_assets_precompile_line
|
|
56
|
+
ensure_assets_precompile_line!(assets_precompile_line)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_locale_entries
|
|
60
|
+
write_action_locale_entries!(file_name, title_case_name)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%%= stylesheet_link_tag 'rails_admin/actions/<%= file_name %>' %>
|
|
2
|
+
<div class="card mb-3">
|
|
3
|
+
<div class="card-body">
|
|
4
|
+
<div class="response <%= file_name %>-response" id="<%= file_name %>-response">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="loader d-none" id="<%= file_name %>-loader">
|
|
7
|
+
<div class="double-bounce1"></div>
|
|
8
|
+
<div class="double-bounce2"></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<button class="btn btn-primary" id="<%= file_name %>-id" data-url="<%%= rails_admin.<%= file_name %>_path %>">Click me</button>
|
|
13
|
+
<%%= javascript_include_tag "rails_admin/actions/<%= file_name %>" %>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
var <%= action_name_camel_case %>Cable = null;
|
|
2
|
+
// If the <%= action_name_camel_case %>Function is already defined, then don't redefine it and don't attach it to the eventListener
|
|
3
|
+
if (typeof <%= action_name_camel_case %>Function !== 'function') {
|
|
4
|
+
function <%= action_name_camel_case %>Function(event) {
|
|
5
|
+
console.log('Hello from <%= file_name %>', event);
|
|
6
|
+
// Action Cable WebSocket connection only if <%= action_name_camel_case %>Cable is not already defined and valid
|
|
7
|
+
if (typeof <%= action_name_camel_case %>Cable !== 'object' || <%= action_name_camel_case %>Cable === null) {
|
|
8
|
+
<%= action_name_camel_case %>Cable = App.cable.subscriptions.create("ActivityLogChannel", {
|
|
9
|
+
connected() {
|
|
10
|
+
console.log("Connected to the channel:", this);
|
|
11
|
+
this.send({ message: '<%= file_name %> Client is connected', topic: "<%= file_name %>", namespace: "subscriptions" });
|
|
12
|
+
},
|
|
13
|
+
disconnected() {
|
|
14
|
+
console.log("<%= file_name %> Client Disconnected");
|
|
15
|
+
},
|
|
16
|
+
received(data) {
|
|
17
|
+
if(data["topic"] == "<%= file_name %>") {
|
|
18
|
+
console.log("<%= file_name %>", data);
|
|
19
|
+
document.getElementById('<%= file_name %>-response').innerHTML = data["message"];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// Send a message to the server
|
|
25
|
+
<%= action_name_camel_case %>Cable.send({ message: '<%= file_name %> Client is sending a message', topic: "<%= file_name %>", namespace: "subscriptions" });
|
|
26
|
+
// Attach a click event listener to the button which sends a fetch GET request and shows the response.
|
|
27
|
+
// The URL is read from the data-url attribute to avoid ERB interpolation in plain .js files.
|
|
28
|
+
document.getElementById('<%= file_name %>-id').addEventListener('click', function() {
|
|
29
|
+
var url = this.dataset.url;
|
|
30
|
+
document.getElementById('<%= file_name %>-loader').classList.remove('d-none');
|
|
31
|
+
fetch(url, { headers: { 'Accept': 'application/json' } })
|
|
32
|
+
.then(response => response.json())
|
|
33
|
+
.then(data => {
|
|
34
|
+
console.log(data);
|
|
35
|
+
document.getElementById('<%= file_name %>-response').innerHTML = data.message;
|
|
36
|
+
document.getElementById('<%= file_name %>-loader').classList.add('d-none');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Attach the function to the eventListener
|
|
42
|
+
document.addEventListener('turbo:load', <%= action_name_camel_case %>Function);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
RailsAdmin::Config::Actions.add_action "<%= file_name %>", :base, :collection do
|
|
2
|
+
# show_in_sidebar/show_in_navigation/breadcrumb_parent are deliberately not
|
|
3
|
+
# overridden here (unlike a root action) - RailsAdmin's own :collection
|
|
4
|
+
# defaults already do the right thing: sidebar-visible, not top-nav, and
|
|
5
|
+
# breadcrumbing back to the model's index page.
|
|
6
|
+
# This ensures the action only shows up for authorized users
|
|
7
|
+
visible? authorized?
|
|
8
|
+
# Have a look at https://fontawesome.com/v5/search for available icons
|
|
9
|
+
link_icon 'fas fa-file'
|
|
10
|
+
# The controller which will be used to compute the action and the REST verbs it will respond to
|
|
11
|
+
http_methods [:get]
|
|
12
|
+
# Adding the controller which is needed to compute calls from the ui
|
|
13
|
+
# This is a collection action: it runs against the whole model index (all
|
|
14
|
+
# records), not a single record (member) or globally (root) - @abstract_model
|
|
15
|
+
# is available here to scope the collection this action operates on.
|
|
16
|
+
controller do
|
|
17
|
+
proc do # This is needed because we need that this code is re-evaluated each time is called
|
|
18
|
+
if request.format.json?
|
|
19
|
+
# This is the code that is executed when the action is called
|
|
20
|
+
# It is executed in the context of the controller
|
|
21
|
+
# So you can access all the controller methods
|
|
22
|
+
# and instance variables
|
|
23
|
+
status = 200
|
|
24
|
+
message = "Hello World!"
|
|
25
|
+
# Note: ActivityLogChannel is expected to re-broadcast messages from the "messages" channel
|
|
26
|
+
ActionCable.server.broadcast("messages", { topic: :<%= file_name %>, status: status, message: message})
|
|
27
|
+
render json: {message: message}.to_json, status: status
|
|
28
|
+
else
|
|
29
|
+
# Renders the action.html.erb view for browser requests (HTML format)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Spinner
|
|
2
|
+
.loader {
|
|
3
|
+
width: 40px;
|
|
4
|
+
height: 40px;
|
|
5
|
+
position: relative;
|
|
6
|
+
margin: 0 auto;
|
|
7
|
+
}
|
|
8
|
+
.double-bounce1,
|
|
9
|
+
.double-bounce2 {
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
background-color: #333;
|
|
14
|
+
opacity: 0.6;
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
animation: sk-bounce 2.0s infinite ease-in-out;
|
|
19
|
+
}
|
|
20
|
+
.double-bounce2 {
|
|
21
|
+
animation-delay: -1.0s;
|
|
22
|
+
}
|
|
23
|
+
@keyframes sk-bounce {
|
|
24
|
+
0%,
|
|
25
|
+
100% {
|
|
26
|
+
transform: scale(0.0);
|
|
27
|
+
}
|
|
28
|
+
50% {
|
|
29
|
+
transform: scale(1.0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// End Spinner
|
|
33
|
+
#<%= file_name %>-response {
|
|
34
|
+
border-radius: 1em;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Thecore
|
|
2
|
+
module Generators
|
|
3
|
+
# Fetches one asset from thecore's own samples/ directory, via one overridable
|
|
4
|
+
# point: ENV["THECORE_SAMPLES_SOURCE"], resolved fresh on every call (never
|
|
5
|
+
# memoized), defaulting to the real raw GitHub URL for thecore's samples/ on
|
|
6
|
+
# `master` (thecore's own default branch). An http(s) value is fetched over the
|
|
7
|
+
# network via Thor's `get`; anything else is treated as a local directory and
|
|
8
|
+
# read directly with `File.read` (this gem's own tests point it at a fixture,
|
|
9
|
+
# so the suite runs offline and deterministically). Blank-string-safe
|
|
10
|
+
# (`nil? || empty?`, not `||` alone — `ENV["X"] || default` would treat
|
|
11
|
+
# THECORE_SAMPLES_SOURCE="" as a real override). Always writes `force: true` —
|
|
12
|
+
# a clean overwrite, no interactive Thor conflict prompt. Fails fast, naming
|
|
13
|
+
# the file/source/underlying error, rather than letting a raw
|
|
14
|
+
# OpenURI::HTTPError propagate and leave the caller half-scaffolded.
|
|
15
|
+
#
|
|
16
|
+
# A plain module function, not a mixin — `get`/`create_file` are public
|
|
17
|
+
# Thor::Actions instance methods (verified directly against Thor's own
|
|
18
|
+
# source), so they're callable on any `actor` passed in. `abort` (Kernel's) is
|
|
19
|
+
# private, so it's invoked via `actor.send(:abort, ...)` rather than a direct
|
|
20
|
+
# call — the one place this differs from calling it bare on `self`.
|
|
21
|
+
#
|
|
22
|
+
# A trailing slash on THECORE_SAMPLES_SOURCE is tolerated (`.chomp("/")`) before
|
|
23
|
+
# building the http(s) URL - a natural way to write/copy a base URL, and one
|
|
24
|
+
# `lib/templates/app_template.rb`'s own independent copy does not guard against
|
|
25
|
+
# (its own plain string interpolation would request a double-slashed path,
|
|
26
|
+
# 404ing against most static hosts/CDNs); caught here during review and not
|
|
27
|
+
# backported there, since that copy is intentionally left untouched (see below).
|
|
28
|
+
#
|
|
29
|
+
# AtomGenerator uses this module directly (`require`d normally, like any other
|
|
30
|
+
# file in this gem). The App application template's own `fetch_thecore_sample`
|
|
31
|
+
# (lib/templates/app_template.rb) is a *separate*, deliberately self-contained
|
|
32
|
+
# copy, NOT switched to call this module — that generator's initial version
|
|
33
|
+
# of this comment claimed the reason was instance_eval/mixin incompatibility,
|
|
34
|
+
# which review correctly identified as wrong (a module function needs no
|
|
35
|
+
# inheritance/mixin relationship to its caller; TtyDetection is proof this
|
|
36
|
+
# already works the same way from an instance_eval'd context). The real
|
|
37
|
+
# reason is deployment, not syntax: the App template's primary real-world
|
|
38
|
+
# invocation is `rails new myapp -m https://raw.githubusercontent.com/.../app_template.rb`
|
|
39
|
+
# — Thor's `apply`/`instance_eval` fetches and evaluates *that one URL's
|
|
40
|
+
# content only*, with no mechanism to also pull in a sibling file from this
|
|
41
|
+
# gem's own repo the way a normal `require` would. At the moment that command
|
|
42
|
+
# runs there is no app yet, so nothing has installed `thecore_generators` as a
|
|
43
|
+
# dependency either — a `require "generators/thecore/sample_fetcher"` inside
|
|
44
|
+
# the template would only work by accident (a global gem install happening to
|
|
45
|
+
# already be on the load path), not by design. So the App template keeps its
|
|
46
|
+
# own independent copy, and this module is not a hard requirement it could
|
|
47
|
+
# `require` — extracting it here still removes the duplication between this
|
|
48
|
+
# module and AtomGenerator, `thecore_generators`' own second, in-gem consumer.
|
|
49
|
+
module SampleFetcher
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
def fetch_thecore_sample(actor, relative_path, destination, label:)
|
|
53
|
+
source = ENV["THECORE_SAMPLES_SOURCE"]
|
|
54
|
+
source = "https://raw.githubusercontent.com/gabrieletassoni/thecore/master/samples" if source.nil? || source.empty?
|
|
55
|
+
|
|
56
|
+
if source.start_with?("http://", "https://")
|
|
57
|
+
actor.get("#{source.chomp("/")}/#{relative_path}", destination, force: true)
|
|
58
|
+
else
|
|
59
|
+
actor.create_file(destination, File.read(File.join(source, relative_path)), force: true)
|
|
60
|
+
end
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
actor.send(:abort, "Failed to fetch #{relative_path} from #{source} (#{e.class}: #{e.message}) " \
|
|
63
|
+
"-- aborting #{label}. Set THECORE_SAMPLES_SOURCE to override the source. If this left " \
|
|
64
|
+
"a partially-generated directory behind, remove it before retrying.")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Thecore
|
|
2
|
+
module Generators
|
|
3
|
+
# A caller with no real TTY behind stdin/stdout (CI, a shelled-out child
|
|
4
|
+
# process) can never answer an interactive prompt. Shared by
|
|
5
|
+
# AssociationWiring's own inverse-association cardinality prompt and
|
|
6
|
+
# AtomGenerator's metadata/dependency prompts, extracted after the two
|
|
7
|
+
# independently implemented the identical condition (caught in review,
|
|
8
|
+
# thecore_generators#20) - a single source of truth means a future
|
|
9
|
+
# refinement to this detection (e.g. an ENV["CI"] check) can't silently
|
|
10
|
+
# apply to one generator's prompts and not the other's.
|
|
11
|
+
module TtyDetection
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def real_tty?
|
|
15
|
+
$stdin.tty? && $stdout.tty?
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -153,6 +153,10 @@ chmod ".devcontainer/link-host-home.sh", 0o755
|
|
|
153
153
|
chmod ".devcontainer/check-plugins.sh", 0o755
|
|
154
154
|
|
|
155
155
|
fetch_thecore_sample(".gitlab-ci.yml", ".gitlab-ci.yml")
|
|
156
|
+
# thecore_generators#25 (ADR 0007): fetched unconditionally, alongside .gitlab-ci.yml above --
|
|
157
|
+
# no hosting-profile prompt, same reasoning ADR 0006 already established for thecore:atom's own
|
|
158
|
+
# dual-CI generation (git hosting is a generic, per-developer choice, not something to ask about).
|
|
159
|
+
fetch_thecore_sample(".github/workflows/ci.yml", ".github/workflows/ci.yml")
|
|
156
160
|
fetch_thecore_sample("CLAUDE.md", "CLAUDE.md")
|
|
157
161
|
|
|
158
162
|
# --- Standard installer chain ------------------------------------------------
|
|
@@ -170,10 +174,44 @@ if options[:asset_pipeline].to_s != "sprockets"
|
|
|
170
174
|
:yellow
|
|
171
175
|
end
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
# thecore_generators#23: non-interactive mode, triggered ONLY by the explicit env var
|
|
178
|
+
# below -- deliberately NOT auto-detected from tty absence the way `thecore:atom`'s
|
|
179
|
+
# `TtyDetection`-based `effectively_non_interactive?` is (ADR 0006). That precedent
|
|
180
|
+
# works because `AtomGenerator` runs in-process inside its own test, where stubbing
|
|
181
|
+
# `$stdin.tty?`/`$stdout.tty?` (see `association_wiring_test.rb`) is meaningful. This
|
|
182
|
+
# template instead always runs as a genuine separate `rails new` subprocess (its own
|
|
183
|
+
# test seam spawns it via Open3), whose stdin is a pipe -- never a real tty -- even
|
|
184
|
+
# when the test is legitimately simulating interactive use by feeding an answer via
|
|
185
|
+
# `stdin_data`. Auto-triggering on tty absence would misfire on every such simulated-
|
|
186
|
+
# interactive test run; the explicit, deliberate opt-in below avoids that with no loss
|
|
187
|
+
# of real-world safety (an unattended CI run with neither var set still fails loudly
|
|
188
|
+
# below, just one env var short of `thecore:atom`'s own auto-detected equivalent).
|
|
189
|
+
non_interactive = !ENV["THECORE_APP_TEMPLATE_NON_INTERACTIVE"].to_s.strip.empty?
|
|
190
|
+
|
|
191
|
+
run_setup_now =
|
|
192
|
+
if non_interactive
|
|
193
|
+
run_installers_raw = ENV["THECORE_APP_TEMPLATE_RUN_INSTALLERS"]
|
|
194
|
+
if run_installers_raw.nil? || run_installers_raw.strip.empty?
|
|
195
|
+
abort(
|
|
196
|
+
"THECORE_APP_TEMPLATE_NON_INTERACTIVE is set but THECORE_APP_TEMPLATE_RUN_INSTALLERS " \
|
|
197
|
+
"is not -- set it to \"true\" or \"false\" to say whether the installer chain " \
|
|
198
|
+
"(devise/rails_admin/active_storage/action_text/action_mailbox/cancan/erd) should run."
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
normalized = run_installers_raw.strip.downcase
|
|
202
|
+
unless %w[true false].include?(normalized)
|
|
203
|
+
abort(
|
|
204
|
+
"THECORE_APP_TEMPLATE_RUN_INSTALLERS must be \"true\" or \"false\", got " \
|
|
205
|
+
"#{run_installers_raw.inspect}."
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
normalized == "true"
|
|
209
|
+
else
|
|
210
|
+
yes?(
|
|
211
|
+
"Run `bundle install` and the standard installer generators (devise, rails_admin, " \
|
|
212
|
+
"active_storage, action_text, action_mailbox, cancan, erd) now? (y/n)"
|
|
213
|
+
)
|
|
214
|
+
end
|
|
177
215
|
|
|
178
216
|
after_bundle do
|
|
179
217
|
next unless run_setup_now
|
|
@@ -5,6 +5,7 @@ require "generators/thecore/workspace_context"
|
|
|
5
5
|
require "generators/thecore/action_companion"
|
|
6
6
|
require "generators/thecore/root_action/root_action_generator"
|
|
7
7
|
require "generators/thecore/member_action/member_action_generator"
|
|
8
|
+
require "generators/thecore/collection_action/collection_action_generator"
|
|
8
9
|
|
|
9
10
|
module Thecore
|
|
10
11
|
# `rails thecore:check_practices` — a Ruby port of thecore_code_extension's
|
|
@@ -50,8 +51,9 @@ module Thecore
|
|
|
50
51
|
# then re-scans and returns whatever violations remain, per ADR 0004:
|
|
51
52
|
# "exits non-zero whenever violations remain unresolved after any --fix
|
|
52
53
|
# pass" — this makes a fix that turns out incomplete (or a violation
|
|
53
|
-
# this
|
|
54
|
-
#
|
|
54
|
+
# this gem doesn't know how to fix at all, e.g. a broken action-file
|
|
55
|
+
# marker, never fixable for any kind) visible in the result rather than
|
|
56
|
+
# silently assumed fixed.
|
|
55
57
|
def self.run(app_root:, atom_name: nil, fix: false)
|
|
56
58
|
violations = Runner.new(app_root: app_root, atom_name: atom_name).run
|
|
57
59
|
return violations unless fix
|
|
@@ -63,12 +65,15 @@ module Thecore
|
|
|
63
65
|
# A Thor::Group instance whose only purpose is running
|
|
64
66
|
# Thecore::Generators::CompanionFiles' generic (action-kind-agnostic)
|
|
65
67
|
# after_initialize.rb/locale fixes against a specific destination_root
|
|
66
|
-
# and action name — used by Runner's Actions check for all three kinds
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
68
|
+
# and action name — used by Runner's Actions check for all three kinds.
|
|
69
|
+
# Companion-file (view/JS/SCSS) fixes do NOT go through this class for
|
|
70
|
+
# any kind — those delegate straight to the kind's own real generator
|
|
71
|
+
# (RootActionGenerator/MemberActionGenerator/CollectionActionGenerator,
|
|
72
|
+
# via ACTION_GENERATOR_CLASSES below) so the fix always uses that
|
|
73
|
+
# generator's own template. Deliberately not placed under
|
|
74
|
+
# lib/generators/ (and so never discovered as a `rails generate`
|
|
75
|
+
# namespace) — it is an internal implementation detail of
|
|
76
|
+
# check_practices' --fix, not a public command.
|
|
72
77
|
class GenericFixTarget < Rails::Generators::NamedBase
|
|
73
78
|
include Thecore::Generators::AtomAware
|
|
74
79
|
include Thecore::Generators::CompanionFiles
|
|
@@ -85,14 +90,15 @@ module Thecore
|
|
|
85
90
|
VIEW_MARKERS = ["stylesheet_link_tag", "javascript_include_tag"].freeze
|
|
86
91
|
JS_MARKERS = ["document.addEventListener('turbo:load'"].freeze
|
|
87
92
|
SCSS_MARKERS = ["@keyframes sk-bounce"].freeze
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
+
# All three kinds now have a generator whose own template rendering a
|
|
94
|
+
# companion-file fix can delegate to (thecore_generators#21, per ADR
|
|
95
|
+
# 0006 in the thecore repo) - collection_action's missing-companion
|
|
96
|
+
# violations were never fixable before this (ADR 0004 tracked it as a
|
|
97
|
+
# deliberate, temporary gap: "no generator to have gotten it right").
|
|
93
98
|
ACTION_GENERATOR_CLASSES = {
|
|
94
99
|
"root_action" => Thecore::Generators::RootActionGenerator,
|
|
95
100
|
"member_action" => Thecore::Generators::MemberActionGenerator,
|
|
101
|
+
"collection_action" => Thecore::Generators::CollectionActionGenerator,
|
|
96
102
|
}.freeze
|
|
97
103
|
|
|
98
104
|
def initialize(app_root:, atom_name: nil)
|
|
@@ -235,7 +241,7 @@ module Thecore
|
|
|
235
241
|
end
|
|
236
242
|
|
|
237
243
|
# Renders only the one missing companion file, via the actual
|
|
238
|
-
# Root/Member Action generator's own template - never the bundled
|
|
244
|
+
# Root/Member/Collection Action generator's own template - never the bundled
|
|
239
245
|
# "render all three companions" method, which would risk a
|
|
240
246
|
# non-interactive file-collision hang/prompt on a hand-customized
|
|
241
247
|
# sibling file that already exists with different content. A file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: thecore_generators
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriele Tassoni
|
|
@@ -38,7 +38,15 @@ files:
|
|
|
38
38
|
- Rakefile
|
|
39
39
|
- lib/generators/thecore/action_companion.rb
|
|
40
40
|
- lib/generators/thecore/association_wiring.rb
|
|
41
|
+
- lib/generators/thecore/atom/atom_generator.rb
|
|
42
|
+
- lib/generators/thecore/atom/templates/abilities.rb.tt
|
|
43
|
+
- lib/generators/thecore/atom/templates/seeds.rb.tt
|
|
41
44
|
- lib/generators/thecore/atom_aware.rb
|
|
45
|
+
- lib/generators/thecore/collection_action/collection_action_generator.rb
|
|
46
|
+
- lib/generators/thecore/collection_action/templates/action.html.erb.tt
|
|
47
|
+
- lib/generators/thecore/collection_action/templates/action.js.tt
|
|
48
|
+
- lib/generators/thecore/collection_action/templates/action.rb.tt
|
|
49
|
+
- lib/generators/thecore/collection_action/templates/action.scss.tt
|
|
42
50
|
- lib/generators/thecore/companion_files.rb
|
|
43
51
|
- lib/generators/thecore/member_action/member_action_generator.rb
|
|
44
52
|
- lib/generators/thecore/member_action/templates/action.html.erb.tt
|
|
@@ -54,6 +62,8 @@ files:
|
|
|
54
62
|
- lib/generators/thecore/root_action/templates/action.js.tt
|
|
55
63
|
- lib/generators/thecore/root_action/templates/action.rb.tt
|
|
56
64
|
- lib/generators/thecore/root_action/templates/action.scss.tt
|
|
65
|
+
- lib/generators/thecore/sample_fetcher.rb
|
|
66
|
+
- lib/generators/thecore/tty_detection.rb
|
|
57
67
|
- lib/generators/thecore/workspace_context.rb
|
|
58
68
|
- lib/tasks/thecore_generators_tasks.rake
|
|
59
69
|
- lib/templates/app_template.rb
|