@reinteractive/rails-insight 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +210 -0
- package/bin/railsinsight.js +128 -0
- package/package.json +62 -0
- package/src/core/blast-radius.js +496 -0
- package/src/core/constants.js +39 -0
- package/src/core/context-loader.js +227 -0
- package/src/core/drift-detector.js +168 -0
- package/src/core/formatter.js +197 -0
- package/src/core/graph.js +510 -0
- package/src/core/indexer.js +595 -0
- package/src/core/patterns/api.js +27 -0
- package/src/core/patterns/auth.js +25 -0
- package/src/core/patterns/authorization.js +24 -0
- package/src/core/patterns/caching.js +19 -0
- package/src/core/patterns/component.js +18 -0
- package/src/core/patterns/config.js +15 -0
- package/src/core/patterns/controller.js +42 -0
- package/src/core/patterns/email.js +20 -0
- package/src/core/patterns/factory.js +31 -0
- package/src/core/patterns/gemfile.js +9 -0
- package/src/core/patterns/helper.js +10 -0
- package/src/core/patterns/job.js +12 -0
- package/src/core/patterns/model.js +123 -0
- package/src/core/patterns/realtime.js +17 -0
- package/src/core/patterns/route.js +27 -0
- package/src/core/patterns/schema.js +25 -0
- package/src/core/patterns/stimulus.js +13 -0
- package/src/core/patterns/storage.js +16 -0
- package/src/core/patterns/uploader.js +16 -0
- package/src/core/patterns/view.js +20 -0
- package/src/core/patterns/worker.js +12 -0
- package/src/core/patterns.js +27 -0
- package/src/core/scanner.js +394 -0
- package/src/core/version-detector.js +295 -0
- package/src/extractors/api.js +284 -0
- package/src/extractors/auth.js +853 -0
- package/src/extractors/authorization.js +785 -0
- package/src/extractors/caching.js +84 -0
- package/src/extractors/component.js +221 -0
- package/src/extractors/config.js +81 -0
- package/src/extractors/controller.js +273 -0
- package/src/extractors/coverage-snapshot.js +296 -0
- package/src/extractors/email.js +123 -0
- package/src/extractors/factory-registry.js +225 -0
- package/src/extractors/gemfile.js +440 -0
- package/src/extractors/helper.js +55 -0
- package/src/extractors/jobs.js +122 -0
- package/src/extractors/model.js +506 -0
- package/src/extractors/realtime.js +102 -0
- package/src/extractors/routes.js +251 -0
- package/src/extractors/schema.js +178 -0
- package/src/extractors/stimulus.js +149 -0
- package/src/extractors/storage.js +100 -0
- package/src/extractors/test-conventions.js +340 -0
- package/src/extractors/tier2.js +417 -0
- package/src/extractors/tier3.js +84 -0
- package/src/extractors/uploader.js +138 -0
- package/src/extractors/views.js +131 -0
- package/src/extractors/worker.js +62 -0
- package/src/git/diff-parser.js +132 -0
- package/src/providers/interface.js +12 -0
- package/src/providers/local-fs.js +318 -0
- package/src/server.js +71 -0
- package/src/tools/blast-radius-tools.js +129 -0
- package/src/tools/free-tools.js +44 -0
- package/src/tools/handlers/get-controller.js +93 -0
- package/src/tools/handlers/get-coverage-gaps.js +100 -0
- package/src/tools/handlers/get-deep-analysis.js +294 -0
- package/src/tools/handlers/get-domain-clusters.js +113 -0
- package/src/tools/handlers/get-factory-registry.js +43 -0
- package/src/tools/handlers/get-full-index.js +28 -0
- package/src/tools/handlers/get-model.js +108 -0
- package/src/tools/handlers/get-overview.js +153 -0
- package/src/tools/handlers/get-routes.js +18 -0
- package/src/tools/handlers/get-schema.js +40 -0
- package/src/tools/handlers/get-subgraph.js +82 -0
- package/src/tools/handlers/get-test-conventions.js +18 -0
- package/src/tools/handlers/get-well-tested-examples.js +51 -0
- package/src/tools/handlers/helpers.js +115 -0
- package/src/tools/handlers/index-project.js +36 -0
- package/src/tools/handlers/search-patterns.js +104 -0
- package/src/tools/index.js +34 -0
- package/src/tools/pro-tools.js +13 -0
- package/src/utils/file-reader.js +20 -0
- package/src/utils/inflector.js +223 -0
- package/src/utils/ruby-parser.js +115 -0
- package/src/utils/spec-style-detector.js +26 -0
- package/src/utils/token-counter.js +46 -0
- package/src/utils/yaml-parser.js +135 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemfile Extractor (#16)
|
|
3
|
+
* Parses Gemfile and Gemfile.lock, categorises gems.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { GEMFILE_PATTERNS } from '../core/patterns.js'
|
|
7
|
+
|
|
8
|
+
/** @type {Record<string, string[]>} */
|
|
9
|
+
const GEM_CATEGORIES = {
|
|
10
|
+
core: [
|
|
11
|
+
'rails',
|
|
12
|
+
'railties',
|
|
13
|
+
'activesupport',
|
|
14
|
+
'activerecord',
|
|
15
|
+
'actionpack',
|
|
16
|
+
'actionview',
|
|
17
|
+
'actionmailer',
|
|
18
|
+
'activejob',
|
|
19
|
+
'actioncable',
|
|
20
|
+
'activestorage',
|
|
21
|
+
'actiontext',
|
|
22
|
+
'actionmailbox',
|
|
23
|
+
'puma',
|
|
24
|
+
'unicorn',
|
|
25
|
+
'bootsnap',
|
|
26
|
+
'tzinfo-data',
|
|
27
|
+
'sprockets',
|
|
28
|
+
'sprockets-rails',
|
|
29
|
+
],
|
|
30
|
+
frontend: [
|
|
31
|
+
'importmap-rails',
|
|
32
|
+
'jsbundling-rails',
|
|
33
|
+
'cssbundling-rails',
|
|
34
|
+
'tailwindcss-rails',
|
|
35
|
+
'sass-rails',
|
|
36
|
+
'webpacker',
|
|
37
|
+
'shakapacker',
|
|
38
|
+
'turbo-rails',
|
|
39
|
+
'stimulus-rails',
|
|
40
|
+
'hotwire-rails',
|
|
41
|
+
'propshaft',
|
|
42
|
+
'dartsass-rails',
|
|
43
|
+
'vite_rails',
|
|
44
|
+
'esbuild',
|
|
45
|
+
'rollup',
|
|
46
|
+
],
|
|
47
|
+
auth: [
|
|
48
|
+
'devise',
|
|
49
|
+
'devise-jwt',
|
|
50
|
+
'devise_invitable',
|
|
51
|
+
'omniauth',
|
|
52
|
+
'omniauth-rails_csrf_protection',
|
|
53
|
+
'sorcery',
|
|
54
|
+
'clearance',
|
|
55
|
+
'rodauth-rails',
|
|
56
|
+
'doorkeeper',
|
|
57
|
+
'jwt',
|
|
58
|
+
'bcrypt',
|
|
59
|
+
'has_secure_password',
|
|
60
|
+
'warden',
|
|
61
|
+
'rack-attack',
|
|
62
|
+
],
|
|
63
|
+
authorization: [
|
|
64
|
+
'pundit',
|
|
65
|
+
'cancancan',
|
|
66
|
+
'action_policy',
|
|
67
|
+
'rolify',
|
|
68
|
+
'authority',
|
|
69
|
+
],
|
|
70
|
+
background: [
|
|
71
|
+
'sidekiq',
|
|
72
|
+
'sidekiq-pro',
|
|
73
|
+
'sidekiq-enterprise',
|
|
74
|
+
'resque',
|
|
75
|
+
'delayed_job',
|
|
76
|
+
'good_job',
|
|
77
|
+
'solid_queue',
|
|
78
|
+
'que',
|
|
79
|
+
'sneakers',
|
|
80
|
+
'shoryuken',
|
|
81
|
+
'mission_control-jobs',
|
|
82
|
+
],
|
|
83
|
+
caching: [
|
|
84
|
+
'redis',
|
|
85
|
+
'redis-rails',
|
|
86
|
+
'redis-actionpack',
|
|
87
|
+
'hiredis',
|
|
88
|
+
'solid_cache',
|
|
89
|
+
'dalli',
|
|
90
|
+
'identity_cache',
|
|
91
|
+
'readthis',
|
|
92
|
+
],
|
|
93
|
+
search: [
|
|
94
|
+
'searchkick',
|
|
95
|
+
'pg_search',
|
|
96
|
+
'meilisearch-rails',
|
|
97
|
+
'elasticsearch-rails',
|
|
98
|
+
'chewy',
|
|
99
|
+
'ransack',
|
|
100
|
+
'thinking-sphinx',
|
|
101
|
+
],
|
|
102
|
+
payments: [
|
|
103
|
+
'pay',
|
|
104
|
+
'stripe',
|
|
105
|
+
'stripe-rails',
|
|
106
|
+
'braintree',
|
|
107
|
+
'shopify_api',
|
|
108
|
+
'solidus',
|
|
109
|
+
'spree',
|
|
110
|
+
],
|
|
111
|
+
uploads: [
|
|
112
|
+
'image_processing',
|
|
113
|
+
'mini_magick',
|
|
114
|
+
'shrine',
|
|
115
|
+
'carrierwave',
|
|
116
|
+
'paperclip',
|
|
117
|
+
'aws-sdk-s3',
|
|
118
|
+
'google-cloud-storage',
|
|
119
|
+
'azure-storage-blob',
|
|
120
|
+
],
|
|
121
|
+
monitoring: [
|
|
122
|
+
'sentry-ruby',
|
|
123
|
+
'sentry-rails',
|
|
124
|
+
'newrelic_rpm',
|
|
125
|
+
'honeybadger',
|
|
126
|
+
'bugsnag',
|
|
127
|
+
'rollbar',
|
|
128
|
+
'airbrake',
|
|
129
|
+
'scout_apm',
|
|
130
|
+
'skylight',
|
|
131
|
+
'datadog',
|
|
132
|
+
'lograge',
|
|
133
|
+
'ahoy_matey',
|
|
134
|
+
],
|
|
135
|
+
deployment: [
|
|
136
|
+
'kamal',
|
|
137
|
+
'capistrano',
|
|
138
|
+
'capistrano-rails',
|
|
139
|
+
'capistrano-bundler',
|
|
140
|
+
'capistrano-rbenv',
|
|
141
|
+
'thruster',
|
|
142
|
+
'mina',
|
|
143
|
+
'sshkit',
|
|
144
|
+
],
|
|
145
|
+
code_quality: [
|
|
146
|
+
'rubocop',
|
|
147
|
+
'rubocop-rails',
|
|
148
|
+
'rubocop-rspec',
|
|
149
|
+
'rubocop-performance',
|
|
150
|
+
'rubocop-minitest',
|
|
151
|
+
'rubocop-rails-omakase',
|
|
152
|
+
'standard',
|
|
153
|
+
'erb_lint',
|
|
154
|
+
'brakeman',
|
|
155
|
+
'bundler-audit',
|
|
156
|
+
'overcommit',
|
|
157
|
+
'annotate',
|
|
158
|
+
'reek',
|
|
159
|
+
'flog',
|
|
160
|
+
'flay',
|
|
161
|
+
],
|
|
162
|
+
testing: [
|
|
163
|
+
'rspec-rails',
|
|
164
|
+
'factory_bot_rails',
|
|
165
|
+
'faker',
|
|
166
|
+
'capybara',
|
|
167
|
+
'selenium-webdriver',
|
|
168
|
+
'cuprite',
|
|
169
|
+
'capybara-playwright-driver',
|
|
170
|
+
'shoulda-matchers',
|
|
171
|
+
'simplecov',
|
|
172
|
+
'webmock',
|
|
173
|
+
'vcr',
|
|
174
|
+
'timecop',
|
|
175
|
+
'database_cleaner',
|
|
176
|
+
'parallel_tests',
|
|
177
|
+
'minitest',
|
|
178
|
+
'mocha',
|
|
179
|
+
'rspec-mocks',
|
|
180
|
+
],
|
|
181
|
+
data: [
|
|
182
|
+
'pg',
|
|
183
|
+
'mysql2',
|
|
184
|
+
'sqlite3',
|
|
185
|
+
'trilogy',
|
|
186
|
+
'activerecord-import',
|
|
187
|
+
'scenic',
|
|
188
|
+
'strong_migrations',
|
|
189
|
+
'active_record_doctor',
|
|
190
|
+
'paranoia',
|
|
191
|
+
'discard',
|
|
192
|
+
'acts_as_paranoid',
|
|
193
|
+
'paper_trail',
|
|
194
|
+
'audited',
|
|
195
|
+
'aasm',
|
|
196
|
+
'statesman',
|
|
197
|
+
'state_machines-activerecord',
|
|
198
|
+
'friendly_id',
|
|
199
|
+
'acts_as_list',
|
|
200
|
+
'acts_as_tree',
|
|
201
|
+
'ancestry',
|
|
202
|
+
'closure_tree',
|
|
203
|
+
],
|
|
204
|
+
admin: [
|
|
205
|
+
'activeadmin',
|
|
206
|
+
'administrate',
|
|
207
|
+
'avo',
|
|
208
|
+
'rails_admin',
|
|
209
|
+
'trestle',
|
|
210
|
+
'motor-admin',
|
|
211
|
+
],
|
|
212
|
+
api: [
|
|
213
|
+
'grape',
|
|
214
|
+
'graphql',
|
|
215
|
+
'graphql-ruby',
|
|
216
|
+
'graphiql-rails',
|
|
217
|
+
'jbuilder',
|
|
218
|
+
'jsonapi-serializer',
|
|
219
|
+
'active_model_serializers',
|
|
220
|
+
'blueprinter',
|
|
221
|
+
'alba',
|
|
222
|
+
'fast_jsonapi',
|
|
223
|
+
'rack-cors',
|
|
224
|
+
'versionist',
|
|
225
|
+
'apipie-rails',
|
|
226
|
+
'rswag',
|
|
227
|
+
],
|
|
228
|
+
realtime: ['actioncable', 'anycable-rails', 'hotwire-rails'],
|
|
229
|
+
mail: [
|
|
230
|
+
'letter_opener',
|
|
231
|
+
'letter_opener_web',
|
|
232
|
+
'premailer-rails',
|
|
233
|
+
'mailgun-ruby',
|
|
234
|
+
'postmark-rails',
|
|
235
|
+
'sendgrid-ruby',
|
|
236
|
+
'aws-sdk-ses',
|
|
237
|
+
],
|
|
238
|
+
pdf: [
|
|
239
|
+
'wicked_pdf',
|
|
240
|
+
'grover',
|
|
241
|
+
'prawn',
|
|
242
|
+
'prawn-table',
|
|
243
|
+
'hexapdf',
|
|
244
|
+
'wkhtmltopdf-binary',
|
|
245
|
+
],
|
|
246
|
+
spreadsheet: [
|
|
247
|
+
'caxlsx',
|
|
248
|
+
'axlsx_rails',
|
|
249
|
+
'roo',
|
|
250
|
+
'spreadsheet',
|
|
251
|
+
'rubyXL',
|
|
252
|
+
'creek',
|
|
253
|
+
'csv',
|
|
254
|
+
],
|
|
255
|
+
image: ['ruby-vips', 'fastimage'],
|
|
256
|
+
i18n: ['i18n-tasks', 'rails-i18n', 'mobility', 'globalize', 'i18n-js'],
|
|
257
|
+
multi_tenancy: ['acts_as_tenant', 'apartment', 'ros-apartment', 'milia'],
|
|
258
|
+
dev_tools: [
|
|
259
|
+
'pry',
|
|
260
|
+
'pry-rails',
|
|
261
|
+
'pry-byebug',
|
|
262
|
+
'byebug',
|
|
263
|
+
'debug',
|
|
264
|
+
'better_errors',
|
|
265
|
+
'binding_of_caller',
|
|
266
|
+
'web-console',
|
|
267
|
+
'rack-mini-profiler',
|
|
268
|
+
'bullet',
|
|
269
|
+
'prosopite',
|
|
270
|
+
'pghero',
|
|
271
|
+
'spring',
|
|
272
|
+
'listen',
|
|
273
|
+
'foreman',
|
|
274
|
+
],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Build reverse lookup: gem name → category */
|
|
278
|
+
const GEM_TO_CATEGORY = {}
|
|
279
|
+
for (const [category, gems] of Object.entries(GEM_CATEGORIES)) {
|
|
280
|
+
for (const gem of gems) {
|
|
281
|
+
GEM_TO_CATEGORY[gem] = category
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Categorize a gem name.
|
|
287
|
+
* @param {string} name
|
|
288
|
+
* @returns {string}
|
|
289
|
+
*/
|
|
290
|
+
function categorizeGem(name) {
|
|
291
|
+
return GEM_TO_CATEGORY[name] || 'other'
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Parse resolved versions from Gemfile.lock content.
|
|
296
|
+
* @param {string|null} lockContent
|
|
297
|
+
* @returns {Map<string, string>}
|
|
298
|
+
*/
|
|
299
|
+
function parseLockVersions(lockContent) {
|
|
300
|
+
const versions = new Map()
|
|
301
|
+
if (!lockContent) return versions
|
|
302
|
+
|
|
303
|
+
let inSpecs = false
|
|
304
|
+
for (const line of lockContent.split('\n')) {
|
|
305
|
+
const trimmed = line.trimEnd()
|
|
306
|
+
if (trimmed === ' specs:' || trimmed === ' specs:') {
|
|
307
|
+
inSpecs = true
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
if (inSpecs && /^\S/.test(trimmed)) {
|
|
311
|
+
inSpecs = false
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
if (inSpecs) {
|
|
315
|
+
// Lines like " rails (7.1.3)" or " activesupport (= 7.1.3)"
|
|
316
|
+
const match = trimmed.match(/^\s{4}(\S+)\s+\(([^)]+)\)/)
|
|
317
|
+
if (match) {
|
|
318
|
+
versions.set(match[1], match[2])
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return versions
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Parse Gemfile content into structured gem entries.
|
|
327
|
+
* @param {string|null} gemfileContent
|
|
328
|
+
* @param {Map<string, string>} lockVersions
|
|
329
|
+
* @returns {{ gems: Array<{name: string, version: string|null, resolved: string|null, category: string, group: string}>, source: string|null, rubyVersion: string|null, groups: string[] }}
|
|
330
|
+
*/
|
|
331
|
+
function parseGemfile(gemfileContent, lockVersions) {
|
|
332
|
+
const result = {
|
|
333
|
+
gems: [],
|
|
334
|
+
source: null,
|
|
335
|
+
rubyVersion: null,
|
|
336
|
+
groups: [],
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!gemfileContent) return result
|
|
340
|
+
|
|
341
|
+
// Extract source
|
|
342
|
+
const sourceMatch = gemfileContent.match(GEMFILE_PATTERNS.source)
|
|
343
|
+
if (sourceMatch) {
|
|
344
|
+
result.source = sourceMatch[1]
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Extract ruby version
|
|
348
|
+
const rubyMatch = gemfileContent.match(GEMFILE_PATTERNS.ruby)
|
|
349
|
+
if (rubyMatch) {
|
|
350
|
+
result.rubyVersion = rubyMatch[1]
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Parse line by line, tracking group context
|
|
354
|
+
const lines = gemfileContent.split('\n')
|
|
355
|
+
const groupStack = []
|
|
356
|
+
const seenGroups = new Set()
|
|
357
|
+
|
|
358
|
+
for (const line of lines) {
|
|
359
|
+
// Detect group blocks
|
|
360
|
+
const groupMatch = line.match(GEMFILE_PATTERNS.group)
|
|
361
|
+
if (groupMatch) {
|
|
362
|
+
// Parse group symbols: :development, :test or :development, :test
|
|
363
|
+
const groupSymbols = groupMatch[1].match(/:(\w+)/g)
|
|
364
|
+
if (groupSymbols) {
|
|
365
|
+
const groups = groupSymbols.map((g) => g.slice(1))
|
|
366
|
+
groupStack.push(groups)
|
|
367
|
+
for (const g of groups) seenGroups.add(g)
|
|
368
|
+
}
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Detect end of group block
|
|
373
|
+
if (/^\s*end\b/.test(line) && groupStack.length > 0) {
|
|
374
|
+
groupStack.pop()
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Parse gem declarations
|
|
379
|
+
const gemMatch = line.match(GEMFILE_PATTERNS.gem)
|
|
380
|
+
if (gemMatch) {
|
|
381
|
+
const name = gemMatch[1]
|
|
382
|
+
const version = gemMatch[2] || null
|
|
383
|
+
|
|
384
|
+
// Determine group from context or inline group option
|
|
385
|
+
let group = 'default'
|
|
386
|
+
if (groupStack.length > 0) {
|
|
387
|
+
group = groupStack[groupStack.length - 1].join(', ')
|
|
388
|
+
} else if (gemMatch[3]) {
|
|
389
|
+
// Check for inline group: option
|
|
390
|
+
const inlineGroup = gemMatch[3].match(
|
|
391
|
+
/group:\s*(?::(\w+)|\[([^\]]+)\])/,
|
|
392
|
+
)
|
|
393
|
+
if (inlineGroup) {
|
|
394
|
+
group =
|
|
395
|
+
inlineGroup[1] ||
|
|
396
|
+
inlineGroup[2]
|
|
397
|
+
.replace(/:/g, '')
|
|
398
|
+
.replace(/\s/g, '')
|
|
399
|
+
.split(',')
|
|
400
|
+
.join(', ')
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
result.gems.push({
|
|
405
|
+
name,
|
|
406
|
+
version,
|
|
407
|
+
resolved: lockVersions.get(name) || null,
|
|
408
|
+
category: categorizeGem(name),
|
|
409
|
+
group,
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
result.groups = [...seenGroups]
|
|
415
|
+
return result
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Extract Gemfile data from the project.
|
|
420
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
421
|
+
* @returns {{ gems: Array<{name: string, version: string|null, resolved: string|null, category: string, group: string}>, source: string|null, rubyVersion: string|null, groups: string[], byCategory: Record<string, Array<{name: string, version: string|null, resolved: string|null, category: string, group: string}>> }}
|
|
422
|
+
*/
|
|
423
|
+
export function extractGemfile(provider) {
|
|
424
|
+
const gemfileContent = provider.readFile('Gemfile')
|
|
425
|
+
const lockContent = provider.readFile('Gemfile.lock')
|
|
426
|
+
|
|
427
|
+
const lockVersions = parseLockVersions(lockContent)
|
|
428
|
+
const result = parseGemfile(gemfileContent, lockVersions)
|
|
429
|
+
|
|
430
|
+
// Build byCategory index
|
|
431
|
+
const byCategory = {}
|
|
432
|
+
for (const gem of result.gems) {
|
|
433
|
+
if (!byCategory[gem.category]) {
|
|
434
|
+
byCategory[gem.category] = []
|
|
435
|
+
}
|
|
436
|
+
byCategory[gem.category].push(gem)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return { ...result, byCategory }
|
|
440
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper Extractor (#7 — Views sub-type)
|
|
3
|
+
* Extracts helper module names, public methods, and controller associations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { HELPER_PATTERNS } from '../core/patterns.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract helper information from a single helper file.
|
|
10
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
11
|
+
* @param {string} filePath
|
|
12
|
+
* @returns {object|null}
|
|
13
|
+
*/
|
|
14
|
+
export function extractHelper(provider, filePath) {
|
|
15
|
+
const content = provider.readFile(filePath)
|
|
16
|
+
if (!content) return null
|
|
17
|
+
|
|
18
|
+
const moduleMatch = content.match(HELPER_PATTERNS.moduleDeclaration)
|
|
19
|
+
if (!moduleMatch) return null
|
|
20
|
+
|
|
21
|
+
const moduleName = moduleMatch[1]
|
|
22
|
+
|
|
23
|
+
// Derive controller by convention: PostsHelper → PostsController
|
|
24
|
+
const controller = moduleName.endsWith('Helper')
|
|
25
|
+
? moduleName.replace(/Helper$/, 'Controller')
|
|
26
|
+
: null
|
|
27
|
+
|
|
28
|
+
// Find where private section begins (if any)
|
|
29
|
+
const privateMatch = content.match(HELPER_PATTERNS.privateKeyword)
|
|
30
|
+
const privateIndex = privateMatch ? privateMatch.index : content.length
|
|
31
|
+
|
|
32
|
+
// Extract public methods (before private keyword)
|
|
33
|
+
const publicContent = content.slice(0, privateIndex)
|
|
34
|
+
const methods = []
|
|
35
|
+
const methodRe = new RegExp(HELPER_PATTERNS.methodDefinition.source, 'gm')
|
|
36
|
+
let m
|
|
37
|
+
while ((m = methodRe.exec(publicContent))) {
|
|
38
|
+
methods.push(m[1])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Extract included helpers
|
|
42
|
+
const includes = []
|
|
43
|
+
const includeRe = new RegExp(HELPER_PATTERNS.includeHelper.source, 'g')
|
|
44
|
+
while ((m = includeRe.exec(content))) {
|
|
45
|
+
includes.push(m[1])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
module: moduleName,
|
|
50
|
+
file: filePath,
|
|
51
|
+
controller,
|
|
52
|
+
methods,
|
|
53
|
+
includes,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jobs Extractor (#10)
|
|
3
|
+
* Extracts background job metadata from app/jobs/*.rb.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { JOB_PATTERNS } from '../core/patterns.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract a single job's metadata.
|
|
10
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
11
|
+
* @param {string} filePath
|
|
12
|
+
* @returns {object|null}
|
|
13
|
+
*/
|
|
14
|
+
export function extractJob(provider, filePath) {
|
|
15
|
+
const content = provider.readFile(filePath)
|
|
16
|
+
if (!content) return null
|
|
17
|
+
|
|
18
|
+
const classMatch = content.match(JOB_PATTERNS.classDeclaration)
|
|
19
|
+
if (!classMatch) return null
|
|
20
|
+
|
|
21
|
+
const superclass = classMatch[2]
|
|
22
|
+
if (
|
|
23
|
+
!superclass.includes('Job') &&
|
|
24
|
+
!superclass.includes('ApplicationJob') &&
|
|
25
|
+
superclass !== 'ActiveJob::Base'
|
|
26
|
+
) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = {
|
|
31
|
+
class: classMatch[1],
|
|
32
|
+
file: filePath,
|
|
33
|
+
superclass,
|
|
34
|
+
queue: 'default',
|
|
35
|
+
retry_on: [],
|
|
36
|
+
discard_on: [],
|
|
37
|
+
sidekiq_options: null,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Queue
|
|
41
|
+
const queueMatch = content.match(JOB_PATTERNS.queueAs)
|
|
42
|
+
if (queueMatch) {
|
|
43
|
+
result.queue = queueMatch[1]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Retry on
|
|
47
|
+
const retryRe = new RegExp(JOB_PATTERNS.retryOn.source, 'gm')
|
|
48
|
+
let m
|
|
49
|
+
while ((m = retryRe.exec(content))) {
|
|
50
|
+
result.retry_on.push({
|
|
51
|
+
exception: m[1],
|
|
52
|
+
options: m[2]?.trim() || null,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Discard on
|
|
57
|
+
const discardRe = new RegExp(JOB_PATTERNS.discardOn.source, 'gm')
|
|
58
|
+
while ((m = discardRe.exec(content))) {
|
|
59
|
+
result.discard_on.push(m[1])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sidekiq options
|
|
63
|
+
const sidekiqMatch = content.match(JOB_PATTERNS.sidekiqOptions)
|
|
64
|
+
if (sidekiqMatch) {
|
|
65
|
+
result.sidekiq_options = sidekiqMatch[1].trim()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract all jobs and detect adapter/recurring config.
|
|
73
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
74
|
+
* @param {Array<{path: string}>} entries
|
|
75
|
+
* @param {{gems?: object}} gemInfo
|
|
76
|
+
* @returns {object}
|
|
77
|
+
*/
|
|
78
|
+
export function extractJobs(provider, entries, gemInfo = {}) {
|
|
79
|
+
const gems = gemInfo.gems || {}
|
|
80
|
+
const result = {
|
|
81
|
+
adapter: null,
|
|
82
|
+
jobs: [],
|
|
83
|
+
queues_detected: new Set(),
|
|
84
|
+
recurring_jobs: null,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Detect adapter
|
|
88
|
+
if (gems.solid_queue) result.adapter = 'solid_queue'
|
|
89
|
+
else if (gems.sidekiq) result.adapter = 'sidekiq'
|
|
90
|
+
else if (gems.delayed_job) result.adapter = 'delayed_job'
|
|
91
|
+
else if (gems.resque) result.adapter = 'resque'
|
|
92
|
+
else if (gems.good_job) result.adapter = 'good_job'
|
|
93
|
+
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const job = extractJob(provider, entry.path)
|
|
96
|
+
if (job) {
|
|
97
|
+
result.jobs.push(job)
|
|
98
|
+
result.queues_detected.add(job.queue)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Recurring jobs from config/recurring.yml (Solid Queue)
|
|
103
|
+
const recurringContent = provider.readFile('config/recurring.yml')
|
|
104
|
+
if (recurringContent) {
|
|
105
|
+
const jobNames = []
|
|
106
|
+
const classRe = /class:\s*(\w+)/g
|
|
107
|
+
let m
|
|
108
|
+
while ((m = classRe.exec(recurringContent))) {
|
|
109
|
+
jobNames.push(m[1])
|
|
110
|
+
}
|
|
111
|
+
if (jobNames.length > 0) {
|
|
112
|
+
result.recurring_jobs = {
|
|
113
|
+
source: 'config/recurring.yml',
|
|
114
|
+
jobs: jobNames,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
result.queues_detected = [...result.queues_detected]
|
|
120
|
+
|
|
121
|
+
return result
|
|
122
|
+
}
|