@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,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 Extractor (#18-40)
|
|
3
|
+
* Lightweight extraction for secondary categories, primarily
|
|
4
|
+
* using Gemfile detection and file existence checks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { detectSpecStyle } from '../utils/spec-style-detector.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract Tier 2 information across categories #18-40.
|
|
11
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
12
|
+
* @param {Array<{path: string, category: string}>} entries
|
|
13
|
+
* @param {{gems?: object}} gemInfo
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
export function extractTier2(provider, entries, gemInfo = {}) {
|
|
17
|
+
const gems = gemInfo.gems || {}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
security: extractSecurity(provider, gems),
|
|
21
|
+
testing: extractTesting(provider, entries, gems),
|
|
22
|
+
code_quality: extractCodeQuality(provider, gems),
|
|
23
|
+
deployment: extractDeployment(provider, gems),
|
|
24
|
+
search: extractSearch(entries, gems),
|
|
25
|
+
payments: extractPayments(gems),
|
|
26
|
+
multi_tenancy: extractMultiTenancy(entries, gems),
|
|
27
|
+
admin: extractAdmin(entries, gems),
|
|
28
|
+
design_patterns: extractDesignPatterns(entries),
|
|
29
|
+
state_machines: extractStateMachines(gems),
|
|
30
|
+
i18n: extractI18n(provider, entries),
|
|
31
|
+
pdf: extractPdf(gems),
|
|
32
|
+
csv: extractCsv(gems),
|
|
33
|
+
webhooks: extractWebhooks(entries),
|
|
34
|
+
scheduled_tasks: extractScheduledTasks(provider, gems),
|
|
35
|
+
middleware: extractMiddleware(entries),
|
|
36
|
+
engines: extractEngines(entries),
|
|
37
|
+
credentials: extractCredentials(provider, gems),
|
|
38
|
+
http_clients: extractHttpClients(gems),
|
|
39
|
+
performance: extractPerformance(gems),
|
|
40
|
+
database_tooling: extractDatabaseTooling(gems),
|
|
41
|
+
rich_text: extractRichText(gems),
|
|
42
|
+
notifications: extractNotifications(entries, gems),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** #18 Security */
|
|
47
|
+
function extractSecurity(provider, gems) {
|
|
48
|
+
const result = {
|
|
49
|
+
csp: false,
|
|
50
|
+
cors: false,
|
|
51
|
+
force_ssl: false,
|
|
52
|
+
filter_parameters: false,
|
|
53
|
+
credentials_encrypted: false,
|
|
54
|
+
brakeman: !!gems.brakeman,
|
|
55
|
+
bundler_audit: !!gems['bundler-audit'],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
result.csp =
|
|
59
|
+
provider.readFile('config/initializers/content_security_policy.rb') !== null
|
|
60
|
+
result.cors = provider.readFile('config/initializers/cors.rb') !== null
|
|
61
|
+
|
|
62
|
+
const prodContent = provider.readFile('config/environments/production.rb')
|
|
63
|
+
if (prodContent && /config\.force_ssl\s*=\s*true/.test(prodContent)) {
|
|
64
|
+
result.force_ssl = true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const filterContent = provider.readFile(
|
|
68
|
+
'config/initializers/filter_parameter_logging.rb',
|
|
69
|
+
)
|
|
70
|
+
if (filterContent) result.filter_parameters = true
|
|
71
|
+
|
|
72
|
+
if (provider.readFile('config/credentials.yml.enc') !== null) {
|
|
73
|
+
result.credentials_encrypted = true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** #19 Testing */
|
|
80
|
+
function extractTesting(provider, entries, gems) {
|
|
81
|
+
const result = {
|
|
82
|
+
framework: null,
|
|
83
|
+
factories: !!gems.factory_bot_rails,
|
|
84
|
+
system_tests: !!gems.capybara,
|
|
85
|
+
coverage: !!gems.simplecov,
|
|
86
|
+
mocking: [],
|
|
87
|
+
parallel: !!gems.parallel_tests,
|
|
88
|
+
faker: !!gems.faker,
|
|
89
|
+
spec_style: detectSpecStyle(entries),
|
|
90
|
+
factories_dir: detectFactoriesDir(provider),
|
|
91
|
+
fixtures_dir: detectFixturesDir(provider),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (gems['rspec-rails']) {
|
|
95
|
+
result.framework = 'rspec'
|
|
96
|
+
} else if (entries.some((e) => e.path.startsWith('test/'))) {
|
|
97
|
+
result.framework = 'minitest'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (gems.webmock) result.mocking.push('webmock')
|
|
101
|
+
if (gems.vcr) result.mocking.push('vcr')
|
|
102
|
+
|
|
103
|
+
return result
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Detect the factories directory.
|
|
108
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
109
|
+
* @returns {string|null}
|
|
110
|
+
*/
|
|
111
|
+
function detectFactoriesDir(provider) {
|
|
112
|
+
if (provider.fileExists('spec/factories')) return 'spec/factories'
|
|
113
|
+
if (provider.fileExists('test/factories')) return 'test/factories'
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Detect the fixtures directory.
|
|
119
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
120
|
+
* @returns {string|null}
|
|
121
|
+
*/
|
|
122
|
+
function detectFixturesDir(provider) {
|
|
123
|
+
if (provider.fileExists('spec/fixtures')) return 'spec/fixtures'
|
|
124
|
+
if (provider.fileExists('test/fixtures')) return 'test/fixtures'
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** #20 Code Quality */
|
|
129
|
+
function extractCodeQuality(provider, gems) {
|
|
130
|
+
const result = {
|
|
131
|
+
rubocop: false,
|
|
132
|
+
rubocop_preset: null,
|
|
133
|
+
erb_lint: !!gems.erb_lint,
|
|
134
|
+
eslint: false,
|
|
135
|
+
brakeman: !!gems.brakeman,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (provider.readFile('.rubocop.yml') !== null) {
|
|
139
|
+
result.rubocop = true
|
|
140
|
+
if (gems['rubocop-rails-omakase']) result.rubocop_preset = 'omakase'
|
|
141
|
+
else if (gems.standard) result.rubocop_preset = 'standard'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ESLint detection
|
|
145
|
+
const eslintFiles = [
|
|
146
|
+
'.eslintrc.js',
|
|
147
|
+
'.eslintrc.json',
|
|
148
|
+
'.eslintrc.yml',
|
|
149
|
+
'eslint.config.js',
|
|
150
|
+
'eslint.config.mjs',
|
|
151
|
+
]
|
|
152
|
+
for (const f of eslintFiles) {
|
|
153
|
+
if (provider.readFile(f) !== null) {
|
|
154
|
+
result.eslint = true
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** #21 Deployment */
|
|
163
|
+
function extractDeployment(provider, gems) {
|
|
164
|
+
const result = {
|
|
165
|
+
kamal: false,
|
|
166
|
+
capistrano: false,
|
|
167
|
+
heroku: false,
|
|
168
|
+
docker: false,
|
|
169
|
+
ci: [],
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (provider.readFile('config/deploy.yml') !== null) result.kamal = true
|
|
173
|
+
if (provider.readFile('Capfile') !== null) result.capistrano = true
|
|
174
|
+
if (provider.readFile('Procfile') !== null) result.heroku = true
|
|
175
|
+
if (provider.readFile('Dockerfile') !== null) result.docker = true
|
|
176
|
+
|
|
177
|
+
// CI detection
|
|
178
|
+
if (
|
|
179
|
+
provider.readFile('.github/workflows') !== null ||
|
|
180
|
+
provider.readFile('.github') !== null
|
|
181
|
+
) {
|
|
182
|
+
// Check if any workflow files exist via entries
|
|
183
|
+
result.ci.push('github_actions')
|
|
184
|
+
}
|
|
185
|
+
if (provider.readFile('.circleci/config.yml') !== null)
|
|
186
|
+
result.ci.push('circleci')
|
|
187
|
+
if (provider.readFile('.gitlab-ci.yml') !== null) result.ci.push('gitlab')
|
|
188
|
+
|
|
189
|
+
return result
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** #22 Search */
|
|
193
|
+
function extractSearch(entries, gems) {
|
|
194
|
+
const result = {
|
|
195
|
+
engine: null,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (gems.searchkick) result.engine = 'searchkick'
|
|
199
|
+
else if (gems.pg_search) result.engine = 'pg_search'
|
|
200
|
+
else if (gems['meilisearch-rails']) result.engine = 'meilisearch'
|
|
201
|
+
else if (gems.chewy) result.engine = 'chewy'
|
|
202
|
+
else if (gems['elasticsearch-rails']) result.engine = 'elasticsearch'
|
|
203
|
+
|
|
204
|
+
return result
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** #23 Payments */
|
|
208
|
+
function extractPayments(gems) {
|
|
209
|
+
const result = { provider: null }
|
|
210
|
+
if (gems.pay) result.provider = 'pay'
|
|
211
|
+
else if (gems.stripe) result.provider = 'stripe'
|
|
212
|
+
return result
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** #24 Multi-tenancy */
|
|
216
|
+
function extractMultiTenancy(entries, gems) {
|
|
217
|
+
const result = { strategy: null }
|
|
218
|
+
if (gems.acts_as_tenant) result.strategy = 'acts_as_tenant'
|
|
219
|
+
else if (gems.apartment) result.strategy = 'apartment'
|
|
220
|
+
return result
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** #25 Admin */
|
|
224
|
+
function extractAdmin(entries, gems) {
|
|
225
|
+
const result = { framework: null }
|
|
226
|
+
if (gems.activeadmin) result.framework = 'activeadmin'
|
|
227
|
+
else if (gems.administrate) result.framework = 'administrate'
|
|
228
|
+
else if (gems.avo) result.framework = 'avo'
|
|
229
|
+
else if (gems.rails_admin) result.framework = 'rails_admin'
|
|
230
|
+
else if (entries.some((e) => e.path.startsWith('app/controllers/admin/'))) {
|
|
231
|
+
result.framework = 'custom'
|
|
232
|
+
}
|
|
233
|
+
return result
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** #26 Design Patterns */
|
|
237
|
+
function extractDesignPatterns(entries) {
|
|
238
|
+
const patterns = {}
|
|
239
|
+
const dirs = {
|
|
240
|
+
services: 'app/services/',
|
|
241
|
+
forms: 'app/forms/',
|
|
242
|
+
queries: 'app/queries/',
|
|
243
|
+
decorators: 'app/decorators/',
|
|
244
|
+
presenters: 'app/presenters/',
|
|
245
|
+
interactors: 'app/interactors/',
|
|
246
|
+
validators: 'app/validators/',
|
|
247
|
+
notifiers: 'app/notifiers/',
|
|
248
|
+
}
|
|
249
|
+
for (const [name, dir] of Object.entries(dirs)) {
|
|
250
|
+
const count = entries.filter((e) => e.path.startsWith(dir)).length
|
|
251
|
+
if (count > 0) patterns[name] = count
|
|
252
|
+
}
|
|
253
|
+
return patterns
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** #27 State Machines */
|
|
257
|
+
function extractStateMachines(gems) {
|
|
258
|
+
const result = { library: null }
|
|
259
|
+
if (gems.aasm) result.library = 'aasm'
|
|
260
|
+
else if (gems.statesman) result.library = 'statesman'
|
|
261
|
+
else if (gems['state_machines-activerecord'])
|
|
262
|
+
result.library = 'state_machines'
|
|
263
|
+
return result
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** #28 I18n */
|
|
267
|
+
function extractI18n(provider, entries) {
|
|
268
|
+
const result = {
|
|
269
|
+
default_locale: null,
|
|
270
|
+
locales: [],
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const appContent = provider.readFile('config/application.rb')
|
|
274
|
+
if (appContent) {
|
|
275
|
+
const dlMatch = appContent.match(
|
|
276
|
+
/config\.i18n\.default_locale\s*=\s*:(\w+)/,
|
|
277
|
+
)
|
|
278
|
+
if (dlMatch) result.default_locale = dlMatch[1]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const localeEntries = entries.filter(
|
|
282
|
+
(e) => e.path.startsWith('config/locales/') && e.path.endsWith('.yml'),
|
|
283
|
+
)
|
|
284
|
+
const localeSet = new Set()
|
|
285
|
+
for (const entry of localeEntries) {
|
|
286
|
+
// Extract locale from filename: en.yml, devise.en.yml, etc.
|
|
287
|
+
const match = entry.path.match(/\.?(\w{2}(?:-\w{2})?)\.yml$/)
|
|
288
|
+
if (match) localeSet.add(match[1])
|
|
289
|
+
}
|
|
290
|
+
result.locales = [...localeSet].sort()
|
|
291
|
+
|
|
292
|
+
return result
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** #29 PDF */
|
|
296
|
+
function extractPdf(gems) {
|
|
297
|
+
const result = { library: null }
|
|
298
|
+
if (gems.wicked_pdf) result.library = 'wicked_pdf'
|
|
299
|
+
else if (gems.prawn) result.library = 'prawn'
|
|
300
|
+
else if (gems.grover) result.library = 'grover'
|
|
301
|
+
return result
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** #30 CSV / Spreadsheet */
|
|
305
|
+
function extractCsv(gems) {
|
|
306
|
+
const result = { library: null }
|
|
307
|
+
if (gems.caxlsx || gems.axlsx_rails) result.library = 'caxlsx'
|
|
308
|
+
else if (gems.roo) result.library = 'roo'
|
|
309
|
+
return result
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** #31 Webhooks */
|
|
313
|
+
function extractWebhooks(entries) {
|
|
314
|
+
const controllers = entries.filter(
|
|
315
|
+
(e) => e.path.includes('webhook') && e.category === 'controller',
|
|
316
|
+
)
|
|
317
|
+
return { detected: controllers.length > 0, controllers: controllers.length }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** #32 Scheduled Tasks */
|
|
321
|
+
function extractScheduledTasks(provider, gems) {
|
|
322
|
+
const result = { scheduler: null }
|
|
323
|
+
if (gems.whenever) result.scheduler = 'whenever'
|
|
324
|
+
else if (gems['sidekiq-cron']) result.scheduler = 'sidekiq-cron'
|
|
325
|
+
else if (gems['sidekiq-scheduler']) result.scheduler = 'sidekiq-scheduler'
|
|
326
|
+
|
|
327
|
+
if (provider.readFile('config/recurring.yml') !== null) {
|
|
328
|
+
result.scheduler = result.scheduler || 'solid_queue'
|
|
329
|
+
result.recurring_jobs = true
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** #33 Middleware */
|
|
336
|
+
function extractMiddleware(entries) {
|
|
337
|
+
const custom = entries.filter((e) => e.path.startsWith('app/middleware/'))
|
|
338
|
+
return { custom_count: custom.length }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** #34 Engines */
|
|
342
|
+
function extractEngines(entries) {
|
|
343
|
+
const engineEntries = entries.filter(
|
|
344
|
+
(e) => e.path.startsWith('engines/') || e.path.startsWith('lib/engines/'),
|
|
345
|
+
)
|
|
346
|
+
return { count: engineEntries.length }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** #35 Credentials */
|
|
350
|
+
function extractCredentials(provider, gems) {
|
|
351
|
+
const result = {
|
|
352
|
+
encrypted: false,
|
|
353
|
+
per_environment: false,
|
|
354
|
+
dotenv: !!gems['dotenv-rails'],
|
|
355
|
+
legacy_secrets: false,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (provider.readFile('config/credentials.yml.enc') !== null) {
|
|
359
|
+
result.encrypted = true
|
|
360
|
+
}
|
|
361
|
+
if (provider.readFile('config/credentials/production.yml.enc') !== null) {
|
|
362
|
+
result.per_environment = true
|
|
363
|
+
}
|
|
364
|
+
if (provider.readFile('config/secrets.yml') !== null) {
|
|
365
|
+
result.legacy_secrets = true
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return result
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** #36 HTTP Clients */
|
|
372
|
+
function extractHttpClients(gems) {
|
|
373
|
+
const clients = []
|
|
374
|
+
if (gems.faraday) clients.push('faraday')
|
|
375
|
+
if (gems.httparty) clients.push('httparty')
|
|
376
|
+
return { clients }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** #37 Performance */
|
|
380
|
+
function extractPerformance(gems) {
|
|
381
|
+
const tools = []
|
|
382
|
+
if (gems.bullet) tools.push('bullet')
|
|
383
|
+
if (gems['rack-mini-profiler']) tools.push('rack-mini-profiler')
|
|
384
|
+
if (gems.pghero) tools.push('pghero')
|
|
385
|
+
if (gems.prosopite) tools.push('prosopite')
|
|
386
|
+
return { tools }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** #38 Database Tooling */
|
|
390
|
+
function extractDatabaseTooling(gems) {
|
|
391
|
+
const tools = []
|
|
392
|
+
if (gems.annotate) tools.push('annotate')
|
|
393
|
+
if (gems.strong_migrations) tools.push('strong_migrations')
|
|
394
|
+
if (gems.database_cleaner) tools.push('database_cleaner')
|
|
395
|
+
if (gems.active_record_doctor) tools.push('active_record_doctor')
|
|
396
|
+
return { tools }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** #39 Rich Text */
|
|
400
|
+
function extractRichText(gems) {
|
|
401
|
+
const result = { action_text: false, markdown: null }
|
|
402
|
+
if (gems.actiontext || gems['actiontext']) result.action_text = true
|
|
403
|
+
if (gems.redcarpet) result.markdown = 'redcarpet'
|
|
404
|
+
else if (gems.kramdown) result.markdown = 'kramdown'
|
|
405
|
+
else if (gems.commonmarker) result.markdown = 'commonmarker'
|
|
406
|
+
return result
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** #40 Notifications */
|
|
410
|
+
function extractNotifications(entries, gems) {
|
|
411
|
+
const result = { framework: null }
|
|
412
|
+
if (gems.noticed) result.framework = 'noticed'
|
|
413
|
+
else if (entries.some((e) => e.path.startsWith('app/notifications/'))) {
|
|
414
|
+
result.framework = 'custom'
|
|
415
|
+
}
|
|
416
|
+
return result
|
|
417
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 3 Extractor (#41-56)
|
|
3
|
+
* Detection-only extraction for tertiary categories,
|
|
4
|
+
* primarily Gemfile-based gem presence checks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detect the first matching gem from the given names.
|
|
9
|
+
* @param {object} gems - Gem lookup object
|
|
10
|
+
* @param {...string} names - Gem names to check in priority order
|
|
11
|
+
* @returns {{gem: string}|null}
|
|
12
|
+
*/
|
|
13
|
+
function detectGem(gems, ...names) {
|
|
14
|
+
for (const n of names) {
|
|
15
|
+
if (gems[n]) return { gem: n }
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect all matching gems from the given names.
|
|
22
|
+
* @param {object} gems - Gem lookup object
|
|
23
|
+
* @param {...string} names - Gem names to check
|
|
24
|
+
* @returns {{gems: string[]}|null}
|
|
25
|
+
*/
|
|
26
|
+
function detectGems(gems, ...names) {
|
|
27
|
+
const found = names.filter((n) => gems[n])
|
|
28
|
+
return found.length > 0 ? { gems: found } : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract Tier 3 detection across categories #41-56.
|
|
33
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
34
|
+
* @param {Array<{path: string, category: string}>} entries
|
|
35
|
+
* @param {{gems?: object}} gemInfo
|
|
36
|
+
* @returns {object}
|
|
37
|
+
*/
|
|
38
|
+
export function extractTier3(provider, entries, gemInfo = {}) {
|
|
39
|
+
const gems = gemInfo.gems || {}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
feature_flags: detectGem(gems, 'flipper', 'unleash'),
|
|
43
|
+
audit: detectGem(gems, 'paper_trail', 'audited', 'logidze'),
|
|
44
|
+
soft_delete: detectGem(gems, 'discard', 'paranoia'),
|
|
45
|
+
pagination: detectGem(gems, 'pagy', 'kaminari', 'will_paginate'),
|
|
46
|
+
friendly_urls: detectGem(gems, 'friendly_id'),
|
|
47
|
+
tagging: detectGem(gems, 'acts-as-taggable-on'),
|
|
48
|
+
seo: detectGems(gems, 'meta-tags', 'sitemap_generator'),
|
|
49
|
+
geolocation: detectGem(gems, 'geocoder', 'rgeo'),
|
|
50
|
+
sms_push: detectGems(gems, 'twilio-ruby', 'web-push'),
|
|
51
|
+
activity_tracking: detectGem(gems, 'public_activity'),
|
|
52
|
+
data_import_export: extractDataImportExport(entries),
|
|
53
|
+
event_sourcing: detectGem(gems, 'rails_event_store', 'sequent'),
|
|
54
|
+
dry_rb: detectGems(
|
|
55
|
+
gems,
|
|
56
|
+
'dry-validation',
|
|
57
|
+
'dry-monads',
|
|
58
|
+
'dry-types',
|
|
59
|
+
'dry-struct',
|
|
60
|
+
),
|
|
61
|
+
markdown: detectGem(gems, 'redcarpet', 'kramdown', 'commonmarker'),
|
|
62
|
+
rate_limiting: detectGem(gems, 'rack-attack'),
|
|
63
|
+
graphql: extractGraphql(entries, gems),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** #51 */
|
|
68
|
+
function extractDataImportExport(entries) {
|
|
69
|
+
const detected = entries.some(
|
|
70
|
+
(e) =>
|
|
71
|
+
/import|export/i.test(e.path) &&
|
|
72
|
+
(e.path.startsWith('app/services/') || e.path.startsWith('app/jobs/')),
|
|
73
|
+
)
|
|
74
|
+
return { detected }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** #56 */
|
|
78
|
+
function extractGraphql(entries, gems) {
|
|
79
|
+
if (!gems.graphql) return null
|
|
80
|
+
const schema = entries.some(
|
|
81
|
+
(e) => e.path.startsWith('app/graphql/') && e.path.includes('schema'),
|
|
82
|
+
)
|
|
83
|
+
return { gem: 'graphql', schema }
|
|
84
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uploader Extractor (#12 — Storage sub-type)
|
|
3
|
+
* Extracts CarrierWave / Shrine uploader metadata: class name, storage type,
|
|
4
|
+
* allowed extensions/content types, versions, and size constraints.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { UPLOADER_PATTERNS } from '../core/patterns.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract uploader information from a single uploader file.
|
|
11
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
12
|
+
* @param {string} filePath
|
|
13
|
+
* @returns {object|null}
|
|
14
|
+
*/
|
|
15
|
+
export function extractUploader(provider, filePath) {
|
|
16
|
+
const content = provider.readFile(filePath)
|
|
17
|
+
if (!content) return null
|
|
18
|
+
|
|
19
|
+
// Try CarrierWave first
|
|
20
|
+
const cwMatch = content.match(UPLOADER_PATTERNS.carrierWaveClass)
|
|
21
|
+
if (cwMatch) {
|
|
22
|
+
return extractCarrierWaveUploader(content, filePath, cwMatch[1])
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Try Shrine
|
|
26
|
+
const shrineMatch = content.match(UPLOADER_PATTERNS.shrineClass)
|
|
27
|
+
if (shrineMatch) {
|
|
28
|
+
return extractShrineUploader(content, filePath, shrineMatch[1])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract CarrierWave uploader metadata.
|
|
36
|
+
* @param {string} content
|
|
37
|
+
* @param {string} filePath
|
|
38
|
+
* @param {string} className
|
|
39
|
+
* @returns {object}
|
|
40
|
+
*/
|
|
41
|
+
function extractCarrierWaveUploader(content, filePath, className) {
|
|
42
|
+
const result = {
|
|
43
|
+
class: className,
|
|
44
|
+
file: filePath,
|
|
45
|
+
type: 'carrierwave',
|
|
46
|
+
storage: 'file',
|
|
47
|
+
extensions: [],
|
|
48
|
+
content_types: [],
|
|
49
|
+
versions: [],
|
|
50
|
+
store_dir: null,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Storage type
|
|
54
|
+
const storageMatch = content.match(UPLOADER_PATTERNS.storageType)
|
|
55
|
+
if (storageMatch) {
|
|
56
|
+
result.storage = storageMatch[1]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extension allowlist
|
|
60
|
+
const extMatch = content.match(UPLOADER_PATTERNS.extensionAllowlist)
|
|
61
|
+
if (extMatch) {
|
|
62
|
+
result.extensions = extMatch[1].trim().split(/\s+/)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Content type allowlist
|
|
66
|
+
const ctMatch = content.match(UPLOADER_PATTERNS.contentTypeAllowlist)
|
|
67
|
+
if (ctMatch) {
|
|
68
|
+
result.content_types = ctMatch[1].trim().split(/\s+/)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Versions
|
|
72
|
+
const versionRe = new RegExp(UPLOADER_PATTERNS.versionBlock.source, 'g')
|
|
73
|
+
let m
|
|
74
|
+
while ((m = versionRe.exec(content))) {
|
|
75
|
+
result.versions.push(m[1])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Store dir
|
|
79
|
+
const dirMatch = content.match(UPLOADER_PATTERNS.storeDir)
|
|
80
|
+
if (dirMatch) {
|
|
81
|
+
result.store_dir = dirMatch[1]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract Shrine uploader metadata.
|
|
89
|
+
* @param {string} content
|
|
90
|
+
* @param {string} filePath
|
|
91
|
+
* @param {string} className
|
|
92
|
+
* @returns {object}
|
|
93
|
+
*/
|
|
94
|
+
function extractShrineUploader(content, filePath, className) {
|
|
95
|
+
const result = {
|
|
96
|
+
class: className,
|
|
97
|
+
file: filePath,
|
|
98
|
+
type: 'shrine',
|
|
99
|
+
plugins: [],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pluginRe = new RegExp(UPLOADER_PATTERNS.shrinePlugin.source, 'g')
|
|
103
|
+
let m
|
|
104
|
+
while ((m = pluginRe.exec(content))) {
|
|
105
|
+
result.plugins.push(m[1])
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Scan models for CarrierWave mount_uploader declarations.
|
|
113
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
114
|
+
* @param {Object<string, {file?: string}>} modelExtractions
|
|
115
|
+
* @returns {Array<{model: string, attribute: string, uploader: string}>}
|
|
116
|
+
*/
|
|
117
|
+
export function detectMountedUploaders(provider, modelExtractions) {
|
|
118
|
+
const mounted = []
|
|
119
|
+
if (!modelExtractions) return mounted
|
|
120
|
+
|
|
121
|
+
for (const [modelName, model] of Object.entries(modelExtractions)) {
|
|
122
|
+
if (!model.file) continue
|
|
123
|
+
const content = provider.readFile(model.file)
|
|
124
|
+
if (!content) continue
|
|
125
|
+
|
|
126
|
+
const mountRe = new RegExp(UPLOADER_PATTERNS.mountUploader.source, 'g')
|
|
127
|
+
let m
|
|
128
|
+
while ((m = mountRe.exec(content))) {
|
|
129
|
+
mounted.push({
|
|
130
|
+
model: modelName,
|
|
131
|
+
attribute: m[1],
|
|
132
|
+
uploader: m[2],
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return mounted
|
|
138
|
+
}
|