thecore_generators 3.1.0 → 3.6.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.
@@ -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, :root do
2
+ show_in_sidebar true
3
+ show_in_navigation false
4
+ breadcrumb_parent [nil]
5
+ # This ensures the action only shows up for authorized users
6
+ visible? authorized?
7
+ # Not a member action
8
+ member false
9
+ # Not a collection action
10
+ collection false
11
+ # Have a look at https://fontawesome.com/v5/search for available icons
12
+ link_icon 'fas fa-file'
13
+ # The controller which will be used to compute the action and the REST verbs it will respond to
14
+ http_methods [:get]
15
+ # Adding the controller which is needed to compute calls from the ui
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
+ }
@@ -77,6 +77,54 @@ module Thecore
77
77
  File.directory?(atom_dir) && !gemspec_path_for(atom_dir).nil?
78
78
  end
79
79
 
80
+ # Every valid ATOM directory under `<app_root>/vendor/submodules/`,
81
+ # deterministically (alphabetically) ordered. Used by
82
+ # Thecore::CheckPractices to scan the host app plus every ATOM in one
83
+ # pass when no `--atom=NAME` is given.
84
+ def all_atom_dirs(app_root)
85
+ submodules_dir = File.join(app_root.to_s, "vendor", "submodules")
86
+ return [] unless File.directory?(submodules_dir)
87
+
88
+ Dir.children(submodules_dir).sort.filter_map do |name|
89
+ atom_dir = File.join(submodules_dir, name)
90
+ atom_dir if valid_atom_dir?(atom_dir)
91
+ end
92
+ end
93
+
94
+ # Where does `app/models/<class_name>.rb` actually live? Used by
95
+ # Thecore::Generators::AssociationWiring to tell whether a
96
+ # `references` column's target model sits in the same app/ATOM as the
97
+ # generator invocation (destination_root, after AtomAware's own
98
+ # redirection) or a different one entirely — the cross-boundary case
99
+ # from docs/adr/0003-migration-driven-inverse-association-wiring.md in
100
+ # the thecore repo, where the generator still writes the concern but
101
+ # only logs the gemspec/Gemfile dependency a human needs to add.
102
+ #
103
+ # Searches the host app's own app/models first, then every valid ATOM
104
+ # under vendor/submodules/ (deterministically, alphabetically).
105
+ # Returns the absolute app/ATOM root the model was found under, or nil
106
+ # when it isn't found anywhere (e.g. the target model doesn't exist
107
+ # yet) — callers should treat "not found" as "assume same app/ATOM"
108
+ # rather than erroring, since this is a best-effort lookup, not a
109
+ # requirement.
110
+ def model_root_for(class_name:, app_root:)
111
+ file_name = "#{class_name.to_s.underscore}.rb"
112
+ app_root = app_root.to_s
113
+
114
+ return app_root if File.exist?(File.join(app_root, "app", "models", file_name))
115
+
116
+ submodules_dir = File.join(app_root, "vendor", "submodules")
117
+ return nil unless File.directory?(submodules_dir)
118
+
119
+ Dir.children(submodules_dir).sort.each do |name|
120
+ atom_dir = File.join(submodules_dir, name)
121
+ next unless valid_atom_dir?(atom_dir)
122
+ return atom_dir if File.exist?(File.join(atom_dir, "app", "models", file_name))
123
+ end
124
+
125
+ nil
126
+ end
127
+
80
128
  class << self
81
129
  private
82
130
 
@@ -0,0 +1,33 @@
1
+ require "optparse"
2
+ require "thecore_generators/check_practices"
3
+
4
+ namespace :thecore do
5
+ desc "Audit Thecore scaffolding conventions (Scaffold Files, Models, Actions). " \
6
+ "Usage: rails thecore:check_practices -- [--json] [--atom=NAME] [--fix]"
7
+ task check_practices: :environment do
8
+ # Rake's own option parser only understands its own flags (--trace, -T,
9
+ # ...) - anything meant for the task itself must follow a literal `--`
10
+ # separator, which Rake then leaves untouched at the front of ARGV (see
11
+ # https://ruby.github.io/rake/doc/rakefile_rdoc.html#label-Task+Arguments,
12
+ # the standard convention for passing CLI-style flags to a rake task).
13
+ extra_argv = ARGV.drop_while { |arg| arg != "--" }
14
+ extra_argv.shift
15
+
16
+ options = { json: false, atom: nil, fix: false }
17
+ OptionParser.new do |parser|
18
+ parser.on("--json", "Emit structured JSON instead of human-readable text") { options[:json] = true }
19
+ parser.on("--atom=NAME", "Scope the audit to a single ATOM under vendor/submodules/") { |value| options[:atom] = value }
20
+ parser.on("--fix", "Apply every fixable violation in one pass, no confirmation") { options[:fix] = true }
21
+ end.parse!(extra_argv)
22
+
23
+ begin
24
+ violations = Thecore::CheckPractices.run(app_root: Rails.root, atom_name: options[:atom], fix: options[:fix])
25
+ rescue Thor::Error => e
26
+ abort(e.message)
27
+ end
28
+
29
+ puts(options[:json] ? Thecore::CheckPractices::Reporter.json(violations) : Thecore::CheckPractices::Reporter.text(violations))
30
+
31
+ exit(1) unless violations.empty?
32
+ end
33
+ end
@@ -0,0 +1,392 @@
1
+ require "json"
2
+ require "rails/generators"
3
+ require "rails/generators/named_base"
4
+ require "generators/thecore/workspace_context"
5
+ require "generators/thecore/action_companion"
6
+ require "generators/thecore/root_action/root_action_generator"
7
+ require "generators/thecore/member_action/member_action_generator"
8
+
9
+ module Thecore
10
+ # `rails thecore:check_practices` — a Ruby port of thecore_code_extension's
11
+ # checkPractices.js: the Scaffold Files check and the Model concern check
12
+ # (thecore_generators#13), plus the Actions check and `--fix`
13
+ # (thecore_generators#14, per ADR 0004 in the thecore repo).
14
+ #
15
+ # Unlike checkPractices.js, which only ever ran the Scaffold Files check in
16
+ # ATOM context, Runner applies every check uniformly to every context root
17
+ # it scans — the host app root plus every ATOM under vendor/submodules/ by
18
+ # default, or a single named ATOM.
19
+ #
20
+ # The Model check is rescoped per ADR 0001's consequence: since
21
+ # Api::ModelName/RailsAdmin::ModelName concern files are no longer
22
+ # generated by default, their plain absence is not a violation. Only an
23
+ # orphan `include Api::X`/`RailsAdmin::X` (pointing at a missing concern
24
+ # file) or a concern file present but missing its required marker is
25
+ # flagged.
26
+ module CheckPractices
27
+ # One reported issue. `to_h` matches the JSON schema this ticket's
28
+ # acceptance criteria specifies: file, line, message, severity, fixable,
29
+ # code (a stable machine-readable identifier a future consumer, e.g. the
30
+ # VS Code extension, can filter on without depending on `message` text).
31
+ # `fix` (a zero-arg Proc, or nil) is deliberately excluded from `to_h` —
32
+ # it exists only for `--fix` to invoke internally, mirroring
33
+ # checkPractices.js's own `violation.fix.apply(ctx)` pattern; it isn't
34
+ # part of the public JSON contract.
35
+ Violation = Struct.new(:file, :line, :message, :severity, :fixable, :code, :fix, keyword_init: true) do
36
+ def to_h
37
+ {
38
+ "file" => file,
39
+ "line" => line,
40
+ "message" => message,
41
+ "severity" => severity.to_s,
42
+ "fixable" => fixable,
43
+ "code" => code,
44
+ }
45
+ end
46
+ end
47
+
48
+ # `fix: true` applies every fixable violation in one pass (no
49
+ # confirmation of its own — whoever passes `fix:` has already decided),
50
+ # then re-scans and returns whatever violations remain, per ADR 0004:
51
+ # "exits non-zero whenever violations remain unresolved after any --fix
52
+ # pass" — this makes a fix that turns out incomplete (or a violation
53
+ # this ticket doesn't know how to fix, e.g. a collection_action
54
+ # companion) visible in the result rather than silently assumed fixed.
55
+ def self.run(app_root:, atom_name: nil, fix: false)
56
+ violations = Runner.new(app_root: app_root, atom_name: atom_name).run
57
+ return violations unless fix
58
+
59
+ violations.each { |v| v.fix&.call if v.fixable }
60
+ Runner.new(app_root: app_root, atom_name: atom_name).run
61
+ end
62
+
63
+ # A Thor::Group instance whose only purpose is running
64
+ # Thecore::Generators::CompanionFiles' generic (action-kind-agnostic)
65
+ # 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
+ # including collection_action, which has no generator of its own to
68
+ # delegate companion-file (view/JS/SCSS) rendering to. Deliberately not
69
+ # placed under lib/generators/ (and so never discovered as a
70
+ # `rails generate` namespace) — it is an internal implementation detail
71
+ # of check_practices' --fix, not a public command.
72
+ class GenericFixTarget < Rails::Generators::NamedBase
73
+ include Thecore::Generators::AtomAware
74
+ include Thecore::Generators::CompanionFiles
75
+ end
76
+
77
+ class Runner
78
+ AFTER_INITIALIZE_MARKER = "Rails.application.configure do"
79
+ ASSETS_MARKER = "Rails.application.config.assets.precompile"
80
+ API_CONCERN_MARKERS = ["extend ActiveSupport::Concern", "cattr_accessor :json_attrs"].freeze
81
+ RAILS_ADMIN_CONCERN_MARKERS = ["extend ActiveSupport::Concern", "rails_admin do"].freeze
82
+
83
+ ACTION_KINDS = %w[root_action member_action collection_action].freeze
84
+ ACTION_FILE_MARKERS = ["RailsAdmin::Config::Actions.add_action", "http_methods"].freeze
85
+ VIEW_MARKERS = ["stylesheet_link_tag", "javascript_include_tag"].freeze
86
+ JS_MARKERS = ["document.addEventListener('turbo:load'"].freeze
87
+ SCSS_MARKERS = ["@keyframes sk-bounce"].freeze
88
+ # Only root_action/member_action have a generator whose own template
89
+ # rendering a companion-file fix can delegate to; collection_action
90
+ # has none (ADR 0004: "no generator to have gotten it right" - a
91
+ # tracked, deliberate gap), so its missing-companion violations are
92
+ # never fixable.
93
+ ACTION_GENERATOR_CLASSES = {
94
+ "root_action" => Thecore::Generators::RootActionGenerator,
95
+ "member_action" => Thecore::Generators::MemberActionGenerator,
96
+ }.freeze
97
+
98
+ def initialize(app_root:, atom_name: nil)
99
+ @app_root = app_root.to_s
100
+ @atom_name = atom_name
101
+ end
102
+
103
+ def run
104
+ context_roots.flat_map do |root|
105
+ scaffold_file_violations(root) + model_violations(root) + action_violations(root)
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ # Delegates entirely to WorkspaceContext.atom_dir_for for the
112
+ # `--atom=NAME` case, the same resolution+error-message logic
113
+ # AtomAware uses for the Model/Migration/Root Action generators'
114
+ # `--atom=NAME` - rather than re-deriving it here, which would leave
115
+ # two copies of the same "no ATOM named X" message to keep in sync.
116
+ # It raises Thor::Error (not a CheckPractices-specific class) on an
117
+ # unknown name; the rake task rescues that directly.
118
+ def context_roots
119
+ if @atom_name
120
+ [Thecore::Generators::WorkspaceContext.atom_dir_for(cwd: nil, app_root: @app_root, atom_name: @atom_name)]
121
+ else
122
+ [@app_root, *Thecore::Generators::WorkspaceContext.all_atom_dirs(@app_root)]
123
+ end
124
+ end
125
+
126
+ def scaffold_file_violations(root)
127
+ check_scaffold_file(
128
+ File.join(root, "config", "initializers", "after_initialize.rb"),
129
+ AFTER_INITIALIZE_MARKER, "after_initialize.rb", "after_initialize"
130
+ ) + check_scaffold_file(
131
+ File.join(root, "config", "initializers", "assets.rb"),
132
+ ASSETS_MARKER, "assets.rb", "assets"
133
+ )
134
+ end
135
+
136
+ def check_scaffold_file(path, marker, label, code_prefix)
137
+ unless File.exist?(path)
138
+ return [Violation.new(
139
+ file: path, line: 0, message: "Missing Scaffold File: #{label}",
140
+ severity: :error, fixable: false, code: "missing_#{code_prefix}"
141
+ )]
142
+ end
143
+
144
+ return [] if File.read(path).include?(marker)
145
+
146
+ [Violation.new(
147
+ file: path, line: 0, message: "#{label} is missing the `#{marker}` marker",
148
+ severity: :error, fixable: false, code: "missing_#{code_prefix}_marker"
149
+ )]
150
+ end
151
+
152
+ def model_violations(root)
153
+ model_dir = File.join(root, "app", "models")
154
+ return [] unless File.directory?(model_dir)
155
+
156
+ Dir.children(model_dir).select { |f| f.end_with?(".rb") }.sort.flat_map do |file|
157
+ check_model(root, file)
158
+ end
159
+ end
160
+
161
+ def check_model(root, file)
162
+ model_name = File.basename(file, ".rb").camelize
163
+ model_content = File.read(File.join(root, "app", "models", file))
164
+
165
+ check_concern(root, file, model_name, model_content, type: "api", module_prefix: "Api", markers: API_CONCERN_MARKERS) +
166
+ check_concern(root, file, model_name, model_content, type: "rails_admin", module_prefix: "RailsAdmin", markers: RAILS_ADMIN_CONCERN_MARKERS)
167
+ end
168
+
169
+ def check_concern(root, file, model_name, model_content, type:, module_prefix:, markers:)
170
+ # A plain #include? substring check would false-positive: "include
171
+ # Api::Foo" is itself a substring of "include Api::FooBar" (no
172
+ # separator between them), so a model happening to include a
173
+ # differently-named, longer concern would wrongly look like it
174
+ # includes this one too. The negative lookahead requires whatever
175
+ # follows the constant name to not itself be a constant-name
176
+ # character.
177
+ return [] unless model_content.match?(/include #{Regexp.escape("#{module_prefix}::#{model_name}")}(?![A-Za-z0-9_])/)
178
+
179
+ concern_path = File.join(root, "app", "models", "concerns", type, file)
180
+ unless File.exist?(concern_path)
181
+ return [Violation.new(
182
+ file: concern_path, line: 0,
183
+ message: "#{model_name}: includes #{module_prefix}::#{model_name} but its concern file is missing",
184
+ severity: :error, fixable: false, code: "orphan_#{type}_include"
185
+ )]
186
+ end
187
+
188
+ concern_content = File.read(concern_path)
189
+ markers.reject { |marker| concern_content.include?(marker) }.map do |marker|
190
+ Violation.new(
191
+ file: concern_path, line: 0,
192
+ message: "#{model_name}: #{type} concern missing '#{marker}' marker",
193
+ severity: :error, fixable: false, code: "#{type}_concern_missing_marker"
194
+ )
195
+ end
196
+ end
197
+
198
+ # --- Actions check (thecore_generators#14) ---
199
+
200
+ def action_violations(root)
201
+ base_dir = root == @app_root ? "config" : "lib"
202
+
203
+ ACTION_KINDS.flat_map { |kind| action_kind_violations(root, base_dir, kind) }
204
+ end
205
+
206
+ def action_kind_violations(root, base_dir, kind)
207
+ dir = File.join(root, base_dir, "#{kind}s")
208
+ return [] unless File.directory?(dir)
209
+
210
+ Dir.children(dir).select { |f| f.end_with?(".rb") }.sort.flat_map do |file|
211
+ check_action(root, base_dir, kind, File.basename(file, ".rb"))
212
+ end
213
+ end
214
+
215
+ def check_action(root, base_dir, kind, action_name)
216
+ action_path = File.join(root, base_dir, "#{kind}s", "#{action_name}.rb")
217
+ content = File.read(action_path)
218
+
219
+ ACTION_FILE_MARKERS.reject { |m| content.include?(m) }.map { |m|
220
+ Violation.new(
221
+ file: action_path, line: 0, message: "#{action_name}: missing '#{m}' marker",
222
+ severity: :error, fixable: false, code: "action_file_missing_marker"
223
+ )
224
+ } + check_companion(root, kind, action_name, template: "action.html.erb.tt",
225
+ rel_path: "app/views/rails_admin/main/#{action_name}.html.erb",
226
+ markers: VIEW_MARKERS, code: "companion_view") +
227
+ check_companion(root, kind, action_name, template: "action.js.tt",
228
+ rel_path: "app/assets/javascripts/rails_admin/actions/#{action_name}.js",
229
+ markers: JS_MARKERS, code: "companion_js") +
230
+ check_companion(root, kind, action_name, template: "action.scss.tt",
231
+ rel_path: "app/assets/stylesheets/rails_admin/actions/#{action_name}.scss",
232
+ markers: SCSS_MARKERS, code: "companion_scss") +
233
+ check_action_require_line(root, base_dir, kind, action_name) +
234
+ check_action_locale_entries(root, action_name)
235
+ end
236
+
237
+ # Renders only the one missing companion file, via the actual
238
+ # Root/Member Action generator's own template - never the bundled
239
+ # "render all three companions" method, which would risk a
240
+ # non-interactive file-collision hang/prompt on a hand-customized
241
+ # sibling file that already exists with different content. A file
242
+ # that exists but lost a marker is never fixable here, same as the
243
+ # Model check above: regenerating over it could clobber real
244
+ # customization; a human has to look at it.
245
+ #
246
+ # The fix re-checks `File.exist?(full_path)` at call time, not just at
247
+ # scan time: the same companion rel_path is kind-agnostic (e.g.
248
+ # app/views/rails_admin/main/<name>.html.erb), so a root_action and a
249
+ # member_action sharing the same action name produce two independent
250
+ # violations against the identical file. Without the re-check, the
251
+ # second violation's fix would call `template` against a file the
252
+ # first violation's fix just created, tripping Thor's interactive
253
+ # file-collision prompt during a non-interactive `--fix` run - the
254
+ # exact hazard this design otherwise avoids by never calling the
255
+ # bundled render-all-three method.
256
+ def check_companion(root, kind, action_name, template:, rel_path:, markers:, code:)
257
+ full_path = File.join(root, rel_path)
258
+
259
+ unless File.exist?(full_path)
260
+ generator_class = ACTION_GENERATOR_CLASSES[kind]
261
+ fix = generator_class && -> {
262
+ next if File.exist?(full_path)
263
+
264
+ build_action_generator(generator_class, action_name, root).send(:template, template, rel_path)
265
+ }
266
+ return [Violation.new(
267
+ file: full_path, line: 0, message: "#{action_name}: missing companion #{File.basename(rel_path)}",
268
+ severity: :error, fixable: !fix.nil?, code: "missing_#{code}", fix: fix
269
+ )]
270
+ end
271
+
272
+ content = File.read(full_path)
273
+ markers.reject { |m| content.include?(m) }.map do |m|
274
+ Violation.new(
275
+ file: full_path, line: 0, message: "#{action_name}: #{code.tr("_", " ")} missing '#{m}' marker",
276
+ severity: :error, fixable: false, code: "#{code}_missing_marker"
277
+ )
278
+ end
279
+ end
280
+
281
+ # Unlike the companion-file check above, this one applies to all
282
+ # three action kinds (including collection_action) - inserting a
283
+ # require line doesn't need a kind-specific template, just
284
+ # CompanionFiles' generic, already-idempotent ensure_*! helper via
285
+ # GenericFixTarget.
286
+ def check_action_require_line(root, base_dir, kind, action_name)
287
+ after_init_path = File.join(root, "config", "initializers", "after_initialize.rb")
288
+ # A missing/markerless after_initialize.rb is already reported by
289
+ # the Scaffold Files check - don't double-report it here, matching
290
+ # checkActionFile's own `if (fs.existsSync(afterInitPath))` guard.
291
+ return [] unless File.exist?(after_init_path)
292
+
293
+ require_line = Thecore::Generators::ActionCompanion.require_line_for(
294
+ kind: kind, in_atom: base_dir == "lib", name: action_name
295
+ )
296
+ return [] if File.read(after_init_path).include?(require_line)
297
+
298
+ fix = -> { fix_target(action_name, root).send(:ensure_after_initialize_require!, require_line) }
299
+ [Violation.new(
300
+ file: after_init_path, line: 0,
301
+ message: "#{action_name}: missing require line in after_initialize.rb",
302
+ severity: :error, fixable: true, code: "missing_action_require_line", fix: fix
303
+ )]
304
+ end
305
+
306
+ def check_action_locale_entries(root, action_name)
307
+ locales_dir = File.join(root, "config", "locales")
308
+ return [] unless Dir.exist?(locales_dir)
309
+
310
+ Dir.children(locales_dir).select { |f| f.end_with?(".yml") }.sort.flat_map do |file|
311
+ check_single_locale_entry(root, locales_dir, file, action_name)
312
+ end
313
+ end
314
+
315
+ def check_single_locale_entry(root, locales_dir, file, action_name)
316
+ path = File.join(locales_dir, file)
317
+ data = YAML.load_file(path) || {}
318
+ lang = data.size == 1 ? data.keys.first : File.basename(file, ".yml")
319
+ return [] if data.dig(lang, "admin", "actions", action_name)
320
+
321
+ title = action_name.split("_").map(&:capitalize).join(" ")
322
+ # write_action_locale_entries! writes into every *.yml file already
323
+ # present, not just this one - idempotent, so a second violation
324
+ # for the same action_name against another missing locale file
325
+ # simply re-applies the same already-correct end state.
326
+ fix = -> { fix_target(action_name, root).send(:write_action_locale_entries!, action_name, title) }
327
+ [Violation.new(
328
+ file: path, line: 0, message: "#{action_name}: missing locale entry in #{file}",
329
+ severity: :warning, fixable: true, code: "missing_action_locale_entry", fix: fix
330
+ )]
331
+ end
332
+
333
+ # `--atom=NAME` is always resolved explicitly here (never left to
334
+ # cwd-based detection) so a fix always lands in the exact `root` this
335
+ # violation was found under, regardless of the check_practices
336
+ # process's own invocation cwd.
337
+ #
338
+ # For a host-app violation (`root == @app_root`), `atom_name` is nil -
339
+ # but `AtomAware#atom_dir` treats a nil/blank `--atom` as "no override,
340
+ # fall back to cwd-based detection"
341
+ # (`Thecore::Generators::WorkspaceContext.atom_dir_for`), not as "force
342
+ # host-app placement". If the check_practices process's own `Dir.pwd`
343
+ # happens to sit inside a `vendor/submodules/<atom>/` tree at the
344
+ # moment `--fix` runs (e.g. invoked via an explicit `bin/rails` path
345
+ # rather than the `rails` executable's own directory walk-up - see
346
+ # WorkspaceContext's `Dir.pwd`-reset gotcha in this gem's CLAUDE.md),
347
+ # `AtomAware#initialize` would silently redirect the freshly-built
348
+ # generator's `destination_root` into that unrelated ATOM instead of
349
+ # `@app_root`, even though this violation was found in host-app
350
+ # context. Explicitly re-asserting `destination_root = root` after
351
+ # construction closes that gap without touching `WorkspaceContext`/
352
+ # `AtomAware` themselves (shared by Model/Migration/Root/Member
353
+ # generators) - it simply overrides whatever cwd-based guess
354
+ # `AtomAware#initialize` made with the exact root this violation was
355
+ # actually found under.
356
+ def build_action_generator(generator_class, action_name, root)
357
+ atom_name = root == @app_root ? nil : File.basename(root)
358
+ generator = generator_class.new([action_name], { atom: atom_name }, destination_root: @app_root)
359
+ generator.destination_root = root
360
+ generator
361
+ end
362
+
363
+ def fix_target(action_name, root)
364
+ build_action_generator(GenericFixTarget, action_name, root)
365
+ end
366
+ end
367
+
368
+ module Reporter
369
+ module_function
370
+
371
+ def text(violations)
372
+ return "✅ No violations found.\n" if violations.empty?
373
+
374
+ violations
375
+ .group_by(&:file)
376
+ .sort
377
+ .map { |file, file_violations| text_for_file(file, file_violations) }
378
+ .join("\n\n") + "\n"
379
+ end
380
+
381
+ def text_for_file(file, violations)
382
+ lines = violations.map { |v| " [#{v.severity.to_s.upcase}] #{v.message}" }
383
+ "#{file}:\n#{lines.join("\n")}"
384
+ end
385
+ private_class_method :text_for_file
386
+
387
+ def json(violations)
388
+ JSON.generate({ "violations" => violations.map(&:to_h) })
389
+ end
390
+ end
391
+ end
392
+ end
@@ -21,5 +21,9 @@ module ThecoreGenerators
21
21
  # `--help`, it never blocks direct namespace invocation.
22
22
  class Railtie < ::Rails::Railtie
23
23
  config.app_generators.orm :thecore, migration: true, timestamps: true
24
+
25
+ rake_tasks do
26
+ load File.expand_path("../tasks/thecore_generators_tasks.rake", __dir__)
27
+ end
24
28
  end
25
29
  end
@@ -1,3 +1,3 @@
1
1
  module ThecoreGenerators
2
- VERSION = "3.1.0"
2
+ VERSION = "3.6.0"
3
3
  end
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.1.0
4
+ version: 3.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriele Tassoni
@@ -36,13 +36,28 @@ files:
36
36
  - MIT-LICENSE
37
37
  - README.md
38
38
  - Rakefile
39
+ - lib/generators/thecore/action_companion.rb
40
+ - lib/generators/thecore/association_wiring.rb
39
41
  - lib/generators/thecore/atom_aware.rb
42
+ - lib/generators/thecore/companion_files.rb
43
+ - lib/generators/thecore/member_action/member_action_generator.rb
44
+ - lib/generators/thecore/member_action/templates/action.html.erb.tt
45
+ - lib/generators/thecore/member_action/templates/action.js.tt
46
+ - lib/generators/thecore/member_action/templates/action.rb.tt
47
+ - lib/generators/thecore/member_action/templates/action.scss.tt
40
48
  - lib/generators/thecore/migration/migration_generator.rb
41
49
  - lib/generators/thecore/model/model_generator.rb
42
50
  - lib/generators/thecore/model/templates/api_concern.rb.tt
43
51
  - lib/generators/thecore/model/templates/rails_admin_concern.rb.tt
52
+ - lib/generators/thecore/root_action/root_action_generator.rb
53
+ - lib/generators/thecore/root_action/templates/action.html.erb.tt
54
+ - lib/generators/thecore/root_action/templates/action.js.tt
55
+ - lib/generators/thecore/root_action/templates/action.rb.tt
56
+ - lib/generators/thecore/root_action/templates/action.scss.tt
44
57
  - lib/generators/thecore/workspace_context.rb
58
+ - lib/tasks/thecore_generators_tasks.rake
45
59
  - lib/thecore_generators.rb
60
+ - lib/thecore_generators/check_practices.rb
46
61
  - lib/thecore_generators/railtie.rb
47
62
  - lib/thecore_generators/version.rb
48
63
  homepage: https://github.com/gabrieletassoni/thecore_generators