rails_visualizer 0.1.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 +7 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +509 -0
- data/lib/rails_visualizer/assets/dist/index.html +76 -0
- data/lib/rails_visualizer/configuration.rb +35 -0
- data/lib/rails_visualizer/controllers_inspector.rb +200 -0
- data/lib/rails_visualizer/gems_inspector.rb +105 -0
- data/lib/rails_visualizer/introspector.rb +191 -0
- data/lib/rails_visualizer/jobs_inspector.rb +120 -0
- data/lib/rails_visualizer/mailers_inspector.rb +126 -0
- data/lib/rails_visualizer/migrations_inspector.rb +68 -0
- data/lib/rails_visualizer/path_helper.rb +29 -0
- data/lib/rails_visualizer/railtie.rb +9 -0
- data/lib/rails_visualizer/renderer.rb +31 -0
- data/lib/rails_visualizer/routes_inspector.rb +124 -0
- data/lib/rails_visualizer/schema/cache.rb +234 -0
- data/lib/rails_visualizer/schema/index_inspector.rb +43 -0
- data/lib/rails_visualizer/schema/model_inspector.rb +245 -0
- data/lib/rails_visualizer/serializer.rb +124 -0
- data/lib/rails_visualizer/version.rb +5 -0
- data/lib/rails_visualizer.rb +22 -0
- data/lib/tasks/rails_visualizer.rake +59 -0
- metadata +76 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsVisualizer
|
|
4
|
+
module Schema
|
|
5
|
+
class ModelInspector
|
|
6
|
+
include RailsVisualizer::PathHelper
|
|
7
|
+
|
|
8
|
+
SERIALIZABLE_TYPES = [String, Symbol, Integer, Float, TrueClass, FalseClass, NilClass, Array, Hash].freeze
|
|
9
|
+
CALLBACK_TYPES = %w[validate save create update destroy commit rollback find initialize touch].freeze
|
|
10
|
+
|
|
11
|
+
# file_cache and method_location_cache are shared across all ModelInspector
|
|
12
|
+
# instances in a single run to avoid redundant file reads and instance_method
|
|
13
|
+
# lookups for concern/parent methods included by many models.
|
|
14
|
+
def initialize(model, schema_cache = nil, file_cache = nil, method_location_cache = nil)
|
|
15
|
+
@model = model
|
|
16
|
+
@schema_cache = schema_cache
|
|
17
|
+
@file_cache = file_cache || {}
|
|
18
|
+
@method_location_cache = method_location_cache || {}
|
|
19
|
+
@index_map = if schema_cache
|
|
20
|
+
schema_cache.index_map_for(model.table_name)
|
|
21
|
+
else
|
|
22
|
+
IndexInspector.new(model).call
|
|
23
|
+
end
|
|
24
|
+
@method_cache = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call
|
|
28
|
+
{
|
|
29
|
+
name: @model.name,
|
|
30
|
+
table_name: @model.table_name,
|
|
31
|
+
abstract: false,
|
|
32
|
+
file_path: source_path_for(@model),
|
|
33
|
+
columns: columns,
|
|
34
|
+
associations: associations,
|
|
35
|
+
validations: validations,
|
|
36
|
+
enums: enums,
|
|
37
|
+
scopes: scopes,
|
|
38
|
+
callbacks: callbacks
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def columns
|
|
45
|
+
return [] unless table_exists?
|
|
46
|
+
|
|
47
|
+
raw = @schema_cache ? @schema_cache.columns_for(@model.table_name) : @model.columns
|
|
48
|
+
ignored = ignored_column_names
|
|
49
|
+
raw = raw.reject { |col| ignored.include?(col.name) } if ignored.any?
|
|
50
|
+
raw.map { |col| build_column(col) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_column(col)
|
|
54
|
+
data = {
|
|
55
|
+
name: col.name,
|
|
56
|
+
type: col.type.to_s,
|
|
57
|
+
primary: col.name == model_primary_key,
|
|
58
|
+
null: col.null,
|
|
59
|
+
default: col.default
|
|
60
|
+
}
|
|
61
|
+
data[:index] = @index_map[col.name].first if @index_map[col.name]
|
|
62
|
+
enum_vals = cached_enums[col.name]
|
|
63
|
+
data[:enum] = enum_vals.keys if enum_vals
|
|
64
|
+
data
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def associations
|
|
68
|
+
@model.reflect_on_all_associations.map { |r| build_association(r) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_association(reflection)
|
|
72
|
+
assoc = {
|
|
73
|
+
macro: reflection.macro.to_s,
|
|
74
|
+
name: reflection.name.to_s,
|
|
75
|
+
class_name: resolve_class_name(reflection),
|
|
76
|
+
foreign_key: resolve_foreign_key(reflection)
|
|
77
|
+
}
|
|
78
|
+
apply_assoc_options(assoc, reflection.options)
|
|
79
|
+
assoc
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def apply_assoc_options(assoc, opts)
|
|
83
|
+
assoc[:through] = opts[:through].to_s if opts[:through]
|
|
84
|
+
assoc[:as] = opts[:as].to_s if opts[:as]
|
|
85
|
+
assoc[:polymorphic] = true if opts[:polymorphic]
|
|
86
|
+
assoc[:dependent] = opts[:dependent].to_s if opts[:dependent]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validations
|
|
90
|
+
attribute_validators + method_validators
|
|
91
|
+
rescue StandardError
|
|
92
|
+
[]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def attribute_validators
|
|
96
|
+
@model.validators.map do |v|
|
|
97
|
+
{
|
|
98
|
+
kind: v.kind.to_s,
|
|
99
|
+
attributes: v.attributes.map(&:to_s),
|
|
100
|
+
options: serializable_options(v.options),
|
|
101
|
+
validator_class: v.class.name
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
rescue StandardError
|
|
105
|
+
[]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def method_validators
|
|
109
|
+
@model._validate_callbacks.filter_map do |cb|
|
|
110
|
+
next unless cb.filter.is_a?(Symbol)
|
|
111
|
+
next unless app_method_info(cb.filter)
|
|
112
|
+
|
|
113
|
+
{ kind: cb.filter.to_s, attributes: [], options: {}, validator_class: nil }
|
|
114
|
+
end
|
|
115
|
+
rescue StandardError
|
|
116
|
+
[]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Two-level cache: per-model @method_cache avoids repeated instance_method
|
|
120
|
+
# lookups within one model, and @method_location_cache (shared across ALL
|
|
121
|
+
# models) caches the app_root check by [owner_id, method_name] — so a
|
|
122
|
+
# concern method included by 500 models does one source_location check globally.
|
|
123
|
+
def app_method_info(name)
|
|
124
|
+
@method_cache.fetch(name) do
|
|
125
|
+
unbound = @model.instance_method(name)
|
|
126
|
+
owner = unbound.owner
|
|
127
|
+
loc_key = [owner.object_id, name]
|
|
128
|
+
|
|
129
|
+
app_defined = @method_location_cache.fetch(loc_key) do
|
|
130
|
+
location = unbound.source_location
|
|
131
|
+
@method_location_cache[loc_key] = location&.first&.start_with?(app_root)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@method_cache[name] = app_defined ? owner : nil
|
|
135
|
+
end
|
|
136
|
+
rescue StandardError
|
|
137
|
+
@method_cache[name] = nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def serializable_options(options)
|
|
141
|
+
options.each_with_object({}) do |(key, value), hash|
|
|
142
|
+
next if %i[if unless on].include?(key)
|
|
143
|
+
next unless SERIALIZABLE_TYPES.any? { |t| value.is_a?(t) }
|
|
144
|
+
|
|
145
|
+
hash[key] = value.is_a?(Symbol) ? value.to_s : value
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def cached_enums
|
|
150
|
+
@cached_enums ||= @model.defined_enums
|
|
151
|
+
rescue StandardError
|
|
152
|
+
@cached_enums ||= {}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def enums
|
|
156
|
+
cached_enums.transform_values(&:keys)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def scopes
|
|
160
|
+
file_path = source_path_for(@model)
|
|
161
|
+
return [] unless file_path
|
|
162
|
+
|
|
163
|
+
content = read_file(file_path)
|
|
164
|
+
return [] unless content
|
|
165
|
+
|
|
166
|
+
content.scan(/^\s*scope\s+:(\w+)/).flatten
|
|
167
|
+
rescue StandardError
|
|
168
|
+
[]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def read_file(relative_path)
|
|
172
|
+
@file_cache.fetch(relative_path) do
|
|
173
|
+
absolute = Rails.root.join(relative_path)
|
|
174
|
+
@file_cache[relative_path] = absolute.exist? ? File.read(absolute) : nil
|
|
175
|
+
end
|
|
176
|
+
rescue StandardError
|
|
177
|
+
@file_cache[relative_path] = nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def callbacks
|
|
181
|
+
CALLBACK_TYPES.flat_map do |type|
|
|
182
|
+
chain = @model.send(:"_#{type}_callbacks")
|
|
183
|
+
chain.filter_map do |cb|
|
|
184
|
+
next unless cb.filter.is_a?(Symbol)
|
|
185
|
+
|
|
186
|
+
owner = app_method_info(cb.filter)
|
|
187
|
+
next unless owner
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
kind: "#{cb.kind}_#{type}",
|
|
191
|
+
name: cb.filter.to_s,
|
|
192
|
+
defined_in: owner == @model ? nil : owner.name,
|
|
193
|
+
conditional: cb.instance_variable_get(:@if).present? ||
|
|
194
|
+
cb.instance_variable_get(:@unless).present?
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
rescue StandardError
|
|
198
|
+
[]
|
|
199
|
+
end
|
|
200
|
+
rescue StandardError
|
|
201
|
+
[]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def model_primary_key
|
|
205
|
+
@model_primary_key ||= if @schema_cache
|
|
206
|
+
@schema_cache.primary_key_for(@model.table_name)
|
|
207
|
+
else
|
|
208
|
+
@model.primary_key
|
|
209
|
+
end
|
|
210
|
+
rescue StandardError
|
|
211
|
+
'id'
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def table_exists?
|
|
215
|
+
if @schema_cache
|
|
216
|
+
@schema_cache.table_exists?(@model.table_name)
|
|
217
|
+
else
|
|
218
|
+
@model.connection.table_exists?(@model.table_name)
|
|
219
|
+
end
|
|
220
|
+
rescue StandardError
|
|
221
|
+
false
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def ignored_column_names
|
|
225
|
+
Set.new(@model.ignored_columns.map(&:to_s))
|
|
226
|
+
rescue StandardError
|
|
227
|
+
Set.new
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def resolve_class_name(reflection)
|
|
231
|
+
return nil if reflection.options[:polymorphic]
|
|
232
|
+
|
|
233
|
+
reflection.class_name
|
|
234
|
+
rescue NameError
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def resolve_foreign_key(reflection)
|
|
239
|
+
reflection.foreign_key.to_s
|
|
240
|
+
rescue StandardError
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module RailsVisualizer
|
|
8
|
+
LOGO_CANDIDATES = [
|
|
9
|
+
['public/favicon.svg', 'image/svg+xml'],
|
|
10
|
+
['public/favicon.png', 'image/png'],
|
|
11
|
+
['public/favicon.ico', 'image/x-icon'],
|
|
12
|
+
['public/apple-touch-icon.png', 'image/png']
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
class Serializer
|
|
16
|
+
def initialize(app_data, configuration = RailsVisualizer.configuration)
|
|
17
|
+
@app_data = app_data
|
|
18
|
+
@configuration = configuration
|
|
19
|
+
@key_cache = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
JSON.generate(deep_camelize(payload))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def payload
|
|
29
|
+
{
|
|
30
|
+
generated_at: Time.now.utc.iso8601,
|
|
31
|
+
rails_version: rails_version,
|
|
32
|
+
ruby_version: RUBY_VERSION,
|
|
33
|
+
db_version: db_version,
|
|
34
|
+
app_name: @app_data.fetch(:app_name, nil),
|
|
35
|
+
app_title: app_title,
|
|
36
|
+
app_logo: app_logo_data_uri,
|
|
37
|
+
app_root: app_root,
|
|
38
|
+
models: @app_data.fetch(:models, []),
|
|
39
|
+
routes: @app_data.fetch(:routes, []),
|
|
40
|
+
jobs: @app_data.fetch(:jobs, {}),
|
|
41
|
+
controllers: @app_data.fetch(:controllers, []),
|
|
42
|
+
migrations: @app_data.fetch(:migrations, {}),
|
|
43
|
+
mailers: @app_data.fetch(:mailers, []),
|
|
44
|
+
gems: @app_data.fetch(:gems, [])
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def rails_version
|
|
49
|
+
Rails::VERSION::STRING if defined?(Rails::VERSION)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def app_title
|
|
53
|
+
root = app_root
|
|
54
|
+
layout = File.join(root, 'app/views/layouts/application.html.erb')
|
|
55
|
+
return nil unless File.file?(layout)
|
|
56
|
+
|
|
57
|
+
content = File.read(layout)
|
|
58
|
+
# Only extract plain-text titles — skip any that contain ERB expressions
|
|
59
|
+
match = content.match(%r{<title>([^<%>]+)</title>})
|
|
60
|
+
title = match&.[](1)&.strip
|
|
61
|
+
title.presence
|
|
62
|
+
rescue StandardError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def db_version
|
|
67
|
+
return nil unless defined?(ActiveRecord::Base)
|
|
68
|
+
|
|
69
|
+
conn = ActiveRecord::Base.connection
|
|
70
|
+
version = case conn.adapter_name.downcase
|
|
71
|
+
when /postgresql|postgis/
|
|
72
|
+
conn.select_value("SELECT current_setting('server_version')")
|
|
73
|
+
when /mysql/
|
|
74
|
+
conn.select_value('SELECT VERSION()')
|
|
75
|
+
when /sqlite/
|
|
76
|
+
conn.select_value('SELECT sqlite_version()')
|
|
77
|
+
end
|
|
78
|
+
version&.to_s&.strip
|
|
79
|
+
rescue StandardError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def app_root
|
|
84
|
+
defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.to_s : Dir.pwd
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def app_logo_data_uri
|
|
88
|
+
root = app_root
|
|
89
|
+
RailsVisualizer::LOGO_CANDIDATES.each do |relative_path, mime|
|
|
90
|
+
full_path = File.join(root, relative_path)
|
|
91
|
+
next unless File.file?(full_path)
|
|
92
|
+
|
|
93
|
+
encoded = Base64.strict_encode64(File.binread(full_path))
|
|
94
|
+
return "data:#{mime};base64,#{encoded}"
|
|
95
|
+
rescue StandardError
|
|
96
|
+
next
|
|
97
|
+
end
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Recursively converts all Hash keys from snake_case to camelCase.
|
|
102
|
+
# Key conversions are memoised — the same ~30 key names appear millions
|
|
103
|
+
# of times across thousands of models/controllers/routes.
|
|
104
|
+
def deep_camelize(obj)
|
|
105
|
+
case obj
|
|
106
|
+
when Hash
|
|
107
|
+
obj.each_with_object({}) do |(k, v), h|
|
|
108
|
+
h[camelize(k.to_s)] = deep_camelize(v)
|
|
109
|
+
end
|
|
110
|
+
when Array
|
|
111
|
+
obj.map { |v| deep_camelize(v) }
|
|
112
|
+
else
|
|
113
|
+
obj
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def camelize(str)
|
|
118
|
+
@key_cache[str] ||= begin
|
|
119
|
+
parts = str.split('_')
|
|
120
|
+
parts.first + parts[1..].map(&:capitalize).join
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rails_visualizer/version'
|
|
4
|
+
require_relative 'rails_visualizer/configuration'
|
|
5
|
+
require_relative 'rails_visualizer/path_helper'
|
|
6
|
+
require_relative 'rails_visualizer/schema/cache'
|
|
7
|
+
require_relative 'rails_visualizer/schema/index_inspector'
|
|
8
|
+
require_relative 'rails_visualizer/schema/model_inspector'
|
|
9
|
+
require_relative 'rails_visualizer/introspector'
|
|
10
|
+
require_relative 'rails_visualizer/routes_inspector'
|
|
11
|
+
require_relative 'rails_visualizer/jobs_inspector'
|
|
12
|
+
require_relative 'rails_visualizer/controllers_inspector'
|
|
13
|
+
require_relative 'rails_visualizer/migrations_inspector'
|
|
14
|
+
require_relative 'rails_visualizer/mailers_inspector'
|
|
15
|
+
require_relative 'rails_visualizer/gems_inspector'
|
|
16
|
+
require_relative 'rails_visualizer/serializer'
|
|
17
|
+
require_relative 'rails_visualizer/renderer'
|
|
18
|
+
|
|
19
|
+
module RailsVisualizer
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
require_relative 'rails_visualizer/railtie' if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
desc 'Introspect the Rails app and open the interactive RailsVisualizer dashboard in your browser'
|
|
4
|
+
task rails_visualizer: :environment do # rubocop:disable Metrics/BlockLength
|
|
5
|
+
require 'rails_visualizer'
|
|
6
|
+
|
|
7
|
+
puts 'RailsVisualizer: Introspecting app...'
|
|
8
|
+
app_data = RailsVisualizer::Introspector.new.call
|
|
9
|
+
|
|
10
|
+
# Build a set of "controller#action" pairs from non-internal routes only
|
|
11
|
+
# (mirrors the UI's `routes.filter(r => !r.internal)` logic).
|
|
12
|
+
routed_pairs = app_data[:routes].each_with_object(Set.new) do |r, set|
|
|
13
|
+
set.add("#{r[:controller]}##{r[:action]}") unless r[:internal]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Count public routable actions with no matching route, excluding actions that
|
|
17
|
+
# are registered as callbacks — they run as filters, not dispatched actions,
|
|
18
|
+
# so having no route for them is expected (mirrors deadCode.ts logic).
|
|
19
|
+
no_route_count = app_data[:controllers].sum do |ctrl|
|
|
20
|
+
callback_names = [
|
|
21
|
+
*(ctrl[:before_actions] || []),
|
|
22
|
+
*(ctrl[:after_actions] || []),
|
|
23
|
+
*(ctrl[:around_actions] || [])
|
|
24
|
+
].to_set { |cb| cb[:name] }
|
|
25
|
+
|
|
26
|
+
(ctrl[:actions] || []).count do |action|
|
|
27
|
+
callback_names.exclude?(action[:name]) &&
|
|
28
|
+
routed_pairs.exclude?("#{ctrl[:controller_path]}##{action[:name]}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
pl = ->(n, singular, plural = "#{singular}s") { "#{n} #{n == 1 ? singular : plural}" }
|
|
33
|
+
|
|
34
|
+
pending_count = app_data.dig(:migrations, :pending_count) || 0
|
|
35
|
+
migration_icon = pending_count.positive? ? '⚠' : '✓'
|
|
36
|
+
|
|
37
|
+
puts " ✓ #{pl.call(app_data[:models].size, 'model')}"
|
|
38
|
+
puts " ✓ #{pl.call(app_data[:routes].size, 'route')}"
|
|
39
|
+
puts " ✓ #{pl.call(app_data.dig(:jobs, :active_jobs)&.size || 0, 'job')}"
|
|
40
|
+
controller_icon = no_route_count.positive? ? '⚠' : '✓'
|
|
41
|
+
no_route_suffix = no_route_count.positive? ? " (#{pl.call(no_route_count, 'action')} without route)" : ''
|
|
42
|
+
puts " #{controller_icon} #{pl.call(app_data[:controllers].size, 'controller')}#{no_route_suffix}"
|
|
43
|
+
pending_suffix = pending_count.positive? ? " (#{pl.call(pending_count, 'pending migration')})" : ''
|
|
44
|
+
puts " #{migration_icon} #{pl.call(app_data.dig(:migrations, :total_count) || 0, 'migration')}#{pending_suffix}"
|
|
45
|
+
puts " ✓ #{pl.call(app_data[:mailers].size, 'mailer')}"
|
|
46
|
+
|
|
47
|
+
puts 'RailsVisualizer: Rendering...'
|
|
48
|
+
json = RailsVisualizer::Serializer.new(app_data).call
|
|
49
|
+
|
|
50
|
+
output_dir = Rails.root.join(RailsVisualizer.configuration.output_path)
|
|
51
|
+
FileUtils.mkdir_p(output_dir)
|
|
52
|
+
|
|
53
|
+
output_file = output_dir.join(RailsVisualizer.configuration.filename)
|
|
54
|
+
File.write(output_file, RailsVisualizer::Renderer.new(json).call)
|
|
55
|
+
|
|
56
|
+
puts "RailsVisualizer: Done → #{output_file}"
|
|
57
|
+
|
|
58
|
+
system('open', output_file.to_s) if RailsVisualizer.configuration.open_browser
|
|
59
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails_visualizer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Raj Kamal Lashkari
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-18 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |-
|
|
14
|
+
rails_visualizer introspects a Rails application and generates a self-contained
|
|
15
|
+
interactive HTML dashboard with no server, no config, and no Node.js required for
|
|
16
|
+
end users. Run one Rake task to explore models, associations, routes, controllers,
|
|
17
|
+
jobs, mailers, migrations, and gems through a polished 8-tab UI with search,
|
|
18
|
+
filters, an ER diagram canvas, and built-in health checks.
|
|
19
|
+
email:
|
|
20
|
+
- rajkamallashkari@gmail.com
|
|
21
|
+
executables: []
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- CHANGELOG.md
|
|
26
|
+
- LICENSE.txt
|
|
27
|
+
- README.md
|
|
28
|
+
- lib/rails_visualizer.rb
|
|
29
|
+
- lib/rails_visualizer/assets/dist/index.html
|
|
30
|
+
- lib/rails_visualizer/configuration.rb
|
|
31
|
+
- lib/rails_visualizer/controllers_inspector.rb
|
|
32
|
+
- lib/rails_visualizer/gems_inspector.rb
|
|
33
|
+
- lib/rails_visualizer/introspector.rb
|
|
34
|
+
- lib/rails_visualizer/jobs_inspector.rb
|
|
35
|
+
- lib/rails_visualizer/mailers_inspector.rb
|
|
36
|
+
- lib/rails_visualizer/migrations_inspector.rb
|
|
37
|
+
- lib/rails_visualizer/path_helper.rb
|
|
38
|
+
- lib/rails_visualizer/railtie.rb
|
|
39
|
+
- lib/rails_visualizer/renderer.rb
|
|
40
|
+
- lib/rails_visualizer/routes_inspector.rb
|
|
41
|
+
- lib/rails_visualizer/schema/cache.rb
|
|
42
|
+
- lib/rails_visualizer/schema/index_inspector.rb
|
|
43
|
+
- lib/rails_visualizer/schema/model_inspector.rb
|
|
44
|
+
- lib/rails_visualizer/serializer.rb
|
|
45
|
+
- lib/rails_visualizer/version.rb
|
|
46
|
+
- lib/tasks/rails_visualizer.rake
|
|
47
|
+
homepage: https://github.com/rajkamallashkari/rails_visualizer
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
changelog_uri: https://github.com/rajkamallashkari/rails_visualizer/blob/main/CHANGELOG.md
|
|
52
|
+
source_code_uri: https://github.com/rajkamallashkari/rails_visualizer
|
|
53
|
+
bug_tracker_uri: https://github.com/rajkamallashkari/rails_visualizer/issues
|
|
54
|
+
documentation_uri: https://github.com/rajkamallashkari/rails_visualizer#readme
|
|
55
|
+
rubygems_mfa_required: 'true'
|
|
56
|
+
post_install_message:
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '3.0'
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.5.22
|
|
72
|
+
signing_key:
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: Interactive Rails app visualizer — models, routes, controllers, and more
|
|
75
|
+
in one dashboard.
|
|
76
|
+
test_files: []
|