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.
- checksums.yaml +4 -4
- data/README.md +240 -9
- data/lib/generators/thecore/action_companion.rb +116 -0
- data/lib/generators/thecore/association_wiring.rb +217 -0
- data/lib/generators/thecore/atom_aware.rb +14 -3
- data/lib/generators/thecore/companion_files.rb +142 -0
- data/lib/generators/thecore/member_action/member_action_generator.rb +61 -0
- data/lib/generators/thecore/member_action/templates/action.html.erb.tt +16 -0
- data/lib/generators/thecore/member_action/templates/action.js.tt +44 -0
- data/lib/generators/thecore/member_action/templates/action.rb.tt +25 -0
- data/lib/generators/thecore/member_action/templates/action.scss.tt +38 -0
- data/lib/generators/thecore/migration/migration_generator.rb +18 -7
- data/lib/generators/thecore/model/model_generator.rb +55 -15
- data/lib/generators/thecore/root_action/root_action_generator.rb +80 -0
- data/lib/generators/thecore/root_action/templates/action.html.erb.tt +13 -0
- data/lib/generators/thecore/root_action/templates/action.js.tt +42 -0
- data/lib/generators/thecore/root_action/templates/action.rb.tt +33 -0
- data/lib/generators/thecore/root_action/templates/action.scss.tt +38 -0
- data/lib/generators/thecore/workspace_context.rb +48 -0
- data/lib/tasks/thecore_generators_tasks.rake +33 -0
- data/lib/thecore_generators/check_practices.rb +392 -0
- data/lib/thecore_generators/railtie.rb +4 -0
- data/lib/thecore_generators/version.rb +1 -1
- metadata +16 -1
|
@@ -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
|
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.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
|