ariadne_view_components 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +68 -0
  4. data/app/assets/javascripts/ariadne_view_components.js +2 -0
  5. data/app/assets/javascripts/ariadne_view_components.js.map +1 -0
  6. data/app/assets/stylesheets/application.tailwind.css +3 -0
  7. data/app/components/ariadne/ariadne.ts +14 -0
  8. data/app/components/ariadne/base_button.rb +60 -0
  9. data/app/components/ariadne/base_component.rb +155 -0
  10. data/app/components/ariadne/button_component.html.erb +4 -0
  11. data/app/components/ariadne/button_component.rb +158 -0
  12. data/app/components/ariadne/clipboard_copy_component.html.erb +8 -0
  13. data/app/components/ariadne/clipboard_copy_component.rb +50 -0
  14. data/app/components/ariadne/clipboard_copy_component.ts +19 -0
  15. data/app/components/ariadne/component.rb +123 -0
  16. data/app/components/ariadne/content.rb +12 -0
  17. data/app/components/ariadne/counter_component.rb +100 -0
  18. data/app/components/ariadne/flash_component.html.erb +31 -0
  19. data/app/components/ariadne/flash_component.rb +125 -0
  20. data/app/components/ariadne/heading_component.rb +49 -0
  21. data/app/components/ariadne/heroicon_component.html.erb +7 -0
  22. data/app/components/ariadne/heroicon_component.rb +116 -0
  23. data/app/components/ariadne/image_component.rb +51 -0
  24. data/app/components/ariadne/text.rb +25 -0
  25. data/app/components/ariadne/tooltip_component.rb +105 -0
  26. data/app/lib/ariadne/audited/dsl.rb +32 -0
  27. data/app/lib/ariadne/class_name_helper.rb +22 -0
  28. data/app/lib/ariadne/fetch_or_fallback_helper.rb +100 -0
  29. data/app/lib/ariadne/icon_helper.rb +47 -0
  30. data/app/lib/ariadne/join_style_arguments_helper.rb +14 -0
  31. data/app/lib/ariadne/logger_helper.rb +23 -0
  32. data/app/lib/ariadne/status/dsl.rb +41 -0
  33. data/app/lib/ariadne/tab_nav_helper.rb +35 -0
  34. data/app/lib/ariadne/tabbed_component_helper.rb +39 -0
  35. data/app/lib/ariadne/test_selector_helper.rb +20 -0
  36. data/app/lib/ariadne/underline_nav_helper.rb +44 -0
  37. data/app/lib/ariadne/view_helper.rb +22 -0
  38. data/lib/ariadne/classify/utilities.rb +199 -0
  39. data/lib/ariadne/classify/utilities.yml +1817 -0
  40. data/lib/ariadne/classify/validation.rb +18 -0
  41. data/lib/ariadne/classify.rb +210 -0
  42. data/lib/ariadne/view_components/constants.rb +53 -0
  43. data/lib/ariadne/view_components/engine.rb +30 -0
  44. data/lib/ariadne/view_components/linters.rb +3 -0
  45. data/lib/ariadne/view_components/statuses.rb +14 -0
  46. data/lib/ariadne/view_components/version.rb +7 -0
  47. data/lib/ariadne/view_components.rb +59 -0
  48. data/lib/rubocop/config/default.yml +14 -0
  49. data/lib/rubocop/cop/ariadne/ariadne_heroicon.rb +252 -0
  50. data/lib/rubocop/cop/ariadne/base_cop.rb +26 -0
  51. data/lib/rubocop/cop/ariadne/component_name_migration.rb +35 -0
  52. data/lib/rubocop/cop/ariadne/no_tag_memoize.rb +43 -0
  53. data/lib/rubocop/cop/ariadne/system_argument_instead_of_class.rb +57 -0
  54. data/lib/rubocop/cop/ariadne.rb +3 -0
  55. data/lib/tasks/ariadne_view_components.rake +47 -0
  56. data/lib/tasks/coverage.rake +19 -0
  57. data/lib/tasks/custom_utilities.yml +310 -0
  58. data/lib/tasks/docs.rake +525 -0
  59. data/lib/tasks/helpers/ast_processor.rb +44 -0
  60. data/lib/tasks/helpers/ast_traverser.rb +77 -0
  61. data/lib/tasks/static.rake +15 -0
  62. data/lib/tasks/tailwind.rake +31 -0
  63. data/lib/tasks/utilities.rake +121 -0
  64. data/lib/yard/docs_helper.rb +83 -0
  65. data/lib/yard/renders_many_handler.rb +19 -0
  66. data/lib/yard/renders_one_handler.rb +19 -0
  67. data/static/arguments.yml +251 -0
  68. data/static/assets/view-components.svg +18 -0
  69. data/static/audited_at.json +14 -0
  70. data/static/classes.yml +89 -0
  71. data/static/constants.json +243 -0
  72. data/static/statuses.json +14 -0
  73. data/static/tailwindcss.yml +727 -0
  74. metadata +193 -0
@@ -0,0 +1,525 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector"
4
+ require "fileutils"
5
+
6
+ namespace :docs do
7
+ desc "Rebuilds docs on change; run via the Procfile"
8
+ task :livereload do
9
+ require "listen"
10
+
11
+ Rake::Task["docs:build"].execute
12
+
13
+ puts "Listening for changes to documentation..."
14
+
15
+ listener = Listen.to("app") do |modified, added, removed|
16
+ puts "modified absolute path: #{modified}"
17
+ puts "added absolute path: #{added}"
18
+ puts "removed absolute path: #{removed}"
19
+
20
+ unless modified.length.zero?
21
+ changed = modified.dup.uniq
22
+ while (path = changed.shift)
23
+ puts "Reloading #{path}"
24
+ # reload constants (in case they changed)
25
+ load(path)
26
+ end
27
+ end
28
+
29
+ Rake::Task["docs:build"].execute
30
+ end
31
+ listener.start # not blocking
32
+ sleep
33
+ end
34
+
35
+ desc "Generate the documentation."
36
+ task :build do
37
+ registry = generate_yard_registry
38
+
39
+ puts "Converting YARD documentation to Markdown files."
40
+
41
+ # Rails controller for rendering arbitrary ERB
42
+ view_context = ApplicationController.new.tap { |c| c.request = ActionDispatch::TestRequest.create }.view_context
43
+ components = [
44
+ # FIXME: these need to be filled in
45
+ Ariadne::BaseButton,
46
+ Ariadne::ButtonComponent,
47
+ # Ariadne::Alpha::Layout,
48
+ # Ariadne::HellipButton,
49
+ # Ariadne::BorderBox::Header,
50
+ # Ariadne::IconButton,
51
+ # Ariadne::Beta::AutoComplete,
52
+ # Ariadne::Beta::AutoComplete::Item,
53
+ # Ariadne::Beta::Avatar,
54
+ # Ariadne::Beta::AvatarStack,
55
+ # Ariadne::Beta::Blankslate,
56
+ # Ariadne::BorderBoxComponent,
57
+ # Ariadne::BoxComponent,
58
+ # Ariadne::Beta::Breadcrumbs,
59
+ # Ariadne::ButtonGroup,
60
+ # Ariadne::Alpha::ButtonMarketing,
61
+ Ariadne::ClipboardCopyComponent,
62
+ # Ariadne::CloseButton,
63
+ Ariadne::CounterComponent,
64
+ # Ariadne::DetailsComponent,
65
+ # Ariadne::Dropdown,
66
+ # Ariadne::DropdownMenuComponent,
67
+ Ariadne::FlashComponent,
68
+ # Ariadne::FlexComponent,
69
+ # Ariadne::FlexItemComponent,
70
+ Ariadne::HeadingComponent,
71
+ # Ariadne::HiddenTextExpander,
72
+ # Ariadne::LabelComponent,
73
+ # Ariadne::LayoutComponent,
74
+ # Ariadne::LinkComponent,
75
+ # Ariadne::Markdown,
76
+ # Ariadne::MenuComponent,
77
+ # Ariadne::Navigation::TabComponent,
78
+ Ariadne::HeroiconComponent,
79
+ # Ariadne::LocalTime,
80
+ Ariadne::ImageComponent,
81
+ # Ariadne::ImageCrop,
82
+ # Ariadne::PopoverComponent,
83
+ # Ariadne::ProgressBarComponent,
84
+ # Ariadne::StateComponent,
85
+ # Ariadne::SpinnerComponent,
86
+ # Ariadne::SubheadComponent,
87
+ # Ariadne::TabContainerComponent,
88
+ Ariadne::Text,
89
+ # Ariadne::TimeAgoComponent,
90
+ # Ariadne::TimelineItemComponent,
91
+ # Ariadne::TooltipComponent,
92
+ # Ariadne::Truncate,
93
+ # Ariadne::Beta::Truncate,
94
+ # Ariadne::Alpha::UnderlineNav,
95
+ # Ariadne::Alpha::UnderlinePanels,
96
+ # Ariadne::Alpha::TabNav,
97
+ # Ariadne::Alpha::TabPanels,
98
+ Ariadne::TooltipComponent,
99
+ ]
100
+
101
+ js_components = [
102
+ # Ariadne::Dropdown,
103
+ # Ariadne::LocalTime,
104
+ # Ariadne::ImageCrop,
105
+ # Ariadne::Beta::AutoComplete,
106
+ Ariadne::ClipboardCopyComponent,
107
+ # Ariadne::TabContainerComponent,
108
+ # Ariadne::TimeAgoComponent,
109
+ # Ariadne::Alpha::UnderlinePanels,
110
+ # Ariadne::Alpha::TabPanels,
111
+ # Ariadne::TooltipComponent,
112
+ # Ariadne::ButtonComponent,
113
+ # Ariadne::LinkComponent,
114
+ ]
115
+
116
+ all_components = Ariadne::Component.descendants - [Ariadne::BaseComponent, Ariadne::Content] # TODO: why is `Ariadne::Content` not picked up?
117
+ components_needing_docs = all_components - components
118
+
119
+ args_for_components = []
120
+ classes_found_in_examples = []
121
+
122
+ errors = []
123
+
124
+ # Deletes docs before regenerating them, guaranteeing that we don't keep stale docs.
125
+ components_content_glob = File.join("docs", "content", "components", "**", "*.md")
126
+ FileUtils.rm_rf(components_content_glob)
127
+
128
+ components.sort_by(&:name).each do |component|
129
+ documentation = registry.get(component.name)
130
+
131
+ data = docs_metadata(component)
132
+
133
+ path = Pathname.new(data[:path])
134
+ path.dirname.mkpath unless path.dirname.exist?
135
+ File.open(path, "w") do |f|
136
+ f.puts("---")
137
+ f.puts("title: #{data[:title]}")
138
+ f.puts("componentId: #{data[:component_id]}")
139
+ f.puts("status: #{data[:status]}")
140
+ f.puts("source: #{data[:source]}")
141
+ f.puts("lookbook: #{data[:lookbook]}")
142
+ f.puts("---")
143
+ f.puts
144
+ f.puts("import Example from '#{data[:example_path]}'")
145
+
146
+ initialize_method = documentation.meths.find(&:constructor?)
147
+
148
+ if js_components.include?(component)
149
+ f.puts("import RequiresJSFlash from '#{data[:require_js_path]}'")
150
+ f.puts
151
+ f.puts("<RequiresJSFlash />")
152
+ end
153
+
154
+ f.puts
155
+ f.puts("<!-- Warning: AUTO-GENERATED file, do not edit. Add code comments to your Ruby instead <3 -->")
156
+ f.puts
157
+ f.puts(view_context.render(inline: documentation.base_docstring))
158
+
159
+ if documentation.tags(:deprecated).any?
160
+ f.puts
161
+ f.puts("## Deprecation")
162
+ documentation.tags(:deprecated).each do |tag|
163
+ f.puts
164
+ f.puts view_context.render(inline: tag.text)
165
+ end
166
+ end
167
+
168
+ if documentation.tags(:accessibility).any?
169
+ f.puts
170
+ f.puts("## Accessibility")
171
+ documentation.tags(:accessibility).each do |tag|
172
+ f.puts
173
+ f.puts view_context.render(inline: tag.text)
174
+ end
175
+ end
176
+
177
+ params = initialize_method.tags(:param)
178
+
179
+ errors << { component.name => { arguments: "No argument documentation found" } } unless params.any?
180
+
181
+ f.puts
182
+ f.puts("## Arguments")
183
+ f.puts
184
+ f.puts("| Name | Type | Default | Description |")
185
+ f.puts("| :- | :- | :- | :- |")
186
+
187
+ documented_params = params.map(&:name)
188
+ component_params = component.instance_method(:initialize).parameters.map { |p| p.last.to_s }
189
+
190
+ if (documented_params & component_params).size != component_params.size
191
+ err = { arguments: {} }
192
+ (component_params - documented_params).each do |arg|
193
+ err[:arguments][arg] = "Not documented"
194
+ end
195
+
196
+ errors << { component.name => err }
197
+ end
198
+
199
+ args = []
200
+ params.each do |tag|
201
+ default_value = pretty_default_value(tag, component)
202
+
203
+ args << {
204
+ "name" => tag.name,
205
+ "type" => tag.types.join(", "),
206
+ "default" => default_value,
207
+ "description" => view_context.render(inline: tag.text.squish),
208
+ }
209
+
210
+ f.puts("| `#{tag.name}` | `#{tag.types.join(", ")}` | #{default_value} | #{view_context.render(inline: tag.text.squish)} |")
211
+ end
212
+
213
+ component_args = {
214
+ "component" => data[:title],
215
+ "source" => data[:source],
216
+ "parameters" => args,
217
+ }
218
+
219
+ args_for_components << component_args
220
+
221
+ # Slots V2 docs
222
+ slot_v2_methods = documentation.meths.select { |x| x[:renders_one] || x[:renders_many] }
223
+
224
+ if slot_v2_methods.any?
225
+ f.puts
226
+ f.puts("## Slots")
227
+
228
+ slot_v2_methods.each do |slot_documentation|
229
+ f.puts
230
+ f.puts("### `#{slot_documentation.name.to_s.capitalize}`")
231
+
232
+ if slot_documentation.base_docstring.to_s.present?
233
+ f.puts
234
+ f.puts(view_context.render(inline: slot_documentation.base_docstring))
235
+ end
236
+
237
+ param_tags = slot_documentation.tags(:param)
238
+ if param_tags.any?
239
+ f.puts
240
+ f.puts("| Name | Type | Default | Description |")
241
+ f.puts("| :- | :- | :- | :- |")
242
+ end
243
+
244
+ param_tags.each do |tag|
245
+ f.puts("| `#{tag.name}` | `#{tag.types.join(", ")}` | #{pretty_default_value(tag, component)} | #{view_context.render(inline: tag.text)} |")
246
+ end
247
+ end
248
+ end
249
+
250
+ errors << { component.name => { example: "No examples found" } } unless initialize_method.tags(:example).any?
251
+
252
+ f.puts
253
+ f.puts("## Examples")
254
+
255
+ initialize_method.tags(:example).each do |tag|
256
+ name, description, code = parse_example_tag(tag)
257
+ f.puts
258
+ f.puts("### #{name}")
259
+ if description
260
+ f.puts
261
+ f.puts(view_context.render(inline: description.squish))
262
+ end
263
+ f.puts
264
+ html = view_context.render(inline: code)
265
+ html.scan(/class="([^"]*)"/) do |classnames|
266
+ classes_found_in_examples.concat(classnames[0].split.reject { |c| c.starts_with?("heroicon", "js", "my-") }.map { ".#{_1}" })
267
+ end
268
+ f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
269
+ f.puts
270
+ f.puts("```erb")
271
+ f.puts(code.to_s)
272
+ f.puts("```")
273
+ end
274
+ end
275
+ end
276
+
277
+ unless errors.empty?
278
+ puts "==============================================="
279
+ puts "===================== ERRORS =================="
280
+ puts "===============================================\n\n"
281
+ puts JSON.pretty_generate(errors)
282
+ puts "\n\n==============================================="
283
+ puts "==============================================="
284
+ puts "==============================================="
285
+
286
+ raise
287
+ end
288
+
289
+ File.open("static/classes.yml", "w") do |f|
290
+ f.puts YAML.dump(classes_found_in_examples.sort.uniq)
291
+ end
292
+
293
+ File.open("static/arguments.yml", "w") do |f|
294
+ f.puts YAML.dump(args_for_components)
295
+ end
296
+
297
+ # Build system arguments docs from BaseComponent
298
+ documentation = registry.get(Ariadne::BaseComponent.name)
299
+ File.open("docs/content/system-arguments.md", "w") do |f|
300
+ f.puts("---")
301
+ f.puts("title: System arguments")
302
+ f.puts("---")
303
+ f.puts
304
+ f.puts("<!-- Warning: AUTO-GENERATED file, do not edit. Add code comments to your Ruby instead <3 -->")
305
+ f.puts
306
+ f.puts(documentation.base_docstring)
307
+ f.puts
308
+
309
+ initialize_method = documentation.meths.find(&:constructor?)
310
+
311
+ f.puts(view_context.render(inline: initialize_method.base_docstring))
312
+ end
313
+
314
+ # Copy over ADR docs and insert them into the nav
315
+ # puts "Copying ADRs..."
316
+ # Rake::Task["docs:build_adrs"].invoke
317
+
318
+ puts "Markdown compiled."
319
+
320
+ if components_needing_docs.any?
321
+ puts
322
+ puts "The following components needs docs. Care to contribute them? #{components_needing_docs.map(&:name).join(", ")}"
323
+ end
324
+ end
325
+
326
+ # task :build_adrs do
327
+ # adr_content_dir = File.join("docs", "content", "adr")
328
+
329
+ # FileUtils.rm_rf(File.join(adr_content_dir))
330
+ # FileUtils.mkdir(adr_content_dir)
331
+
332
+ # nav_entries = Dir[File.join("adr", "*.md")].sort.map do |orig_path|
333
+ # orig_file_name = File.basename(orig_path)
334
+ # url_name = orig_file_name.chomp(".md")
335
+
336
+ # file_contents = File.read(orig_path)
337
+ # file_contents = <<~CONTENTS.sub(/\n+\z/, "\n")
338
+ # <!-- Warning: AUTO-GENERATED file, do not edit. Make changes to the files in the adr/ directory instead. -->
339
+ # #{file_contents}
340
+ # CONTENTS
341
+
342
+ # title_match = /^# (.+)/.match(file_contents)
343
+ # title = title_match[1]
344
+
345
+ # # Don't include initial ADR for recording ADRs
346
+ # next nil if title == "Record architecture decisions"
347
+
348
+ # File.write(File.join(adr_content_dir, orig_file_name), file_contents)
349
+ # puts "Copied #{orig_path}"
350
+
351
+ # { "title" => title, "url" => "/adr/#{url_name}" }
352
+ # end
353
+
354
+ # nav_yaml_file = File.join("docs", "src", "@primer", "gatsby-theme-doctocat", "nav.yml")
355
+ # nav_yaml = YAML.load_file(nav_yaml_file)
356
+ # adr_entry = {
357
+ # "title" => "Architecture decisions",
358
+ # "children" => nav_entries.compact,
359
+ # }
360
+
361
+ # existing_index = nav_yaml.index { |entry| entry["title"] == "Architecture decisions" }
362
+ # if existing_index
363
+ # nav_yaml[existing_index] = adr_entry
364
+ # else
365
+ # nav_yaml << adr_entry
366
+ # end
367
+
368
+ # File.write(nav_yaml_file, YAML.dump(nav_yaml))
369
+ # end
370
+
371
+ desc "Generate previews from documentation examples"
372
+ task :preview do
373
+ registry = generate_yard_registry
374
+
375
+ FileUtils.rm_rf("lookbook/test/components/previews/ariadne/docs/")
376
+
377
+ components = Ariadne::Component.descendants
378
+
379
+ components.each do |component|
380
+ documentation = registry.get(component.name)
381
+ short_name = component.name.gsub(/Ariadne|::/, "")
382
+ initialize_method = documentation.meths.find(&:constructor?)
383
+
384
+ next unless initialize_method&.tags(:example)&.any?
385
+
386
+ yard_example_tags = initialize_method.tags(:example)
387
+
388
+ path = Pathname.new("lookbook/test/components/previews/ariadne/docs/#{short_name.underscore}_preview.rb")
389
+ FileUtils.mkdir_p("lookbook/test/components/previews/ariadne/docs") unless path.dirname.exist?
390
+
391
+ File.open(path, "w") do |f|
392
+ f.puts("module Ariadne")
393
+ f.puts(" module Docs")
394
+ f.puts(" class #{short_name}Preview < ViewComponent::Preview")
395
+
396
+ yard_example_tags.each_with_index do |tag, index|
397
+ name, _, code = parse_example_tag(tag)
398
+ method_name = name.split("|").first.downcase.parameterize.underscore
399
+ f.puts(" def #{method_name}; end")
400
+ f.puts unless index == yard_example_tags.size - 1
401
+ path = Pathname.new("lookbook/test/components/previews/ariadne/docs/#{short_name.underscore}_preview/#{method_name}.html.erb")
402
+ FileUtils.mkdir_p("lookbook/test/components/previews/ariadne/docs/#{short_name.underscore}_preview") unless path.dirname.exist?
403
+ File.open(path, "w") do |view_file|
404
+ view_file.puts(code.to_s)
405
+ end
406
+ end
407
+
408
+ f.puts(" end")
409
+ f.puts(" end")
410
+ f.puts("end")
411
+ end
412
+ end
413
+ end
414
+ end
415
+
416
+ def generate_yard_registry
417
+ require "action_dispatch"
418
+ require_relative "../../app/lib/ariadne/view_helper"
419
+ require File.expand_path("./../../lookbook/config/environment.rb", __dir__)
420
+
421
+ YARD::Registry.yardoc_file = ".yardoc"
422
+
423
+ require "./app/components/ariadne/component.rb"
424
+ require "ariadne/view_components"
425
+ require "yard/docs_helper"
426
+ require "view_component/base"
427
+ require "view_component/test_helpers"
428
+ include(ViewComponent::TestHelpers)
429
+ include(Ariadne::ViewHelper)
430
+ include(YARD::DocsHelper)
431
+
432
+ Dir["./app/components/ariadne/**/*.rb"].sort.each do |file|
433
+ puts file
434
+ require file
435
+ end
436
+
437
+ YARD::Rake::YardocTask.new
438
+
439
+ # Custom tags for yard
440
+ YARD::Tags::Library.define_tag("Accessibility", :accessibility)
441
+ YARD::Tags::Library.define_tag("Deprecation", :deprecation)
442
+ YARD::Tags::Library.define_tag("Parameter", :param, :with_types_name_and_default)
443
+
444
+ puts "Building YARD documentation."
445
+ Rake::Task["yard"].execute
446
+
447
+ registry = YARD::RegistryStore.new
448
+ registry.load!(".yardoc")
449
+ registry
450
+ end
451
+
452
+ def parse_example_tag(tag)
453
+ name = tag.name
454
+ description = nil
455
+ code = nil
456
+
457
+ if tag.text.include?("@description")
458
+ splitted = tag.text.split(/@description|@code/)
459
+ description = splitted.second.gsub(/^[ \t]{2}/, "").strip
460
+ code = splitted.last.gsub(/^[ \t]{2}/, "").strip
461
+ else
462
+ code = tag.text
463
+ end
464
+
465
+ [name, description, code]
466
+ end
467
+
468
+ def pretty_default_value(tag, component)
469
+ params = tag.object.parameters.find { |param| [tag.name.to_s, "#{tag.name}:"].include?(param[0]) }
470
+ default = tag.defaults&.first || params&.second
471
+
472
+ return "N/A" unless default
473
+
474
+ constant_name = "#{component.name}::#{default}"
475
+ constant_value = default.safe_constantize || constant_name.safe_constantize
476
+
477
+ return pretty_value(default) if constant_value.nil?
478
+
479
+ pretty_value(constant_value)
480
+ end
481
+
482
+ def docs_metadata(component)
483
+ (status_module, short_name) = status_module_and_short_name(component)
484
+ status_path = status_module.nil? ? "" : "/"
485
+ status = component.status.to_s
486
+
487
+ {
488
+ title: short_name,
489
+ component_id: short_name.underscore,
490
+ status: status.capitalize,
491
+ source: source_url(component),
492
+ lookbook: lookbook_url(component),
493
+ path: "docs/content/components/#{status_path}#{short_name.downcase}.md",
494
+ example_path: example_path(component),
495
+ require_js_path: require_js_path(component),
496
+ }
497
+ end
498
+
499
+ def source_url(component)
500
+ path = component.name.split("::").map(&:underscore).join("/")
501
+
502
+ "https://github.com/yettoapp/ariadne/ruby/view_components/tree/main/app/components/#{path}.rb"
503
+ end
504
+
505
+ def lookbook_url(component)
506
+ path = component.name.split("::").map { |n| n.underscore.dasherize }.join("-")
507
+
508
+ "https://ariadne.style/view-components/lookbook/?path=/component/#{path}"
509
+ end
510
+
511
+ def example_path(component)
512
+ example_path = "../../src/@primer/gatsby-theme-doctocat/components/example"
513
+ example_path = "../#{example_path}" if status_module?(component)
514
+ example_path
515
+ end
516
+
517
+ def require_js_path(component)
518
+ require_js_path = "../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash"
519
+ require_js_path = "../#{require_js_path}" if status_module?(component)
520
+ require_js_path
521
+ end
522
+
523
+ def status_module?(component)
524
+ (["Alpha", "Beta"] & component.name.split("::")).any?
525
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ast_traverser"
4
+
5
+ # :nodoc:
6
+ class AstProcessor
7
+ class << self
8
+ def increment(stats, component, arg_name, value)
9
+ stats[component][:arguments][arg_name][value] = 0 unless stats[component][:arguments][arg_name][value]
10
+ stats[component][:arguments][arg_name][value] += 1
11
+ end
12
+
13
+ def process_ast(ast, stats)
14
+ traverser = AstTraverser.new
15
+ traverser.walk(ast)
16
+
17
+ return if traverser.stats.empty?
18
+
19
+ traverser.stats.each do |component, component_info|
20
+ stats[component] ||= {
21
+ paths: [],
22
+ }
23
+
24
+ stats[component][:paths] << component_info[:path]
25
+ stats[component][:paths].uniq!
26
+ stats[component][:arguments] ||= {}
27
+
28
+ component_info[:arguments]&.each do |arg, value|
29
+ arg_name = arg.to_s
30
+ stats[component][:arguments][arg_name] ||= {}
31
+
32
+ # we want to count each class separately
33
+ if arg_name == "classes"
34
+ value.split.each do |val|
35
+ increment(stats, component, arg_name, val)
36
+ end
37
+ else
38
+ increment(stats, component, arg_name, value)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ariadne/view_components/statuses"
4
+ require_relative "../../../app/lib/ariadne/view_helper"
5
+
6
+ # :nodoc:
7
+ class AstTraverser
8
+ include RuboCop::AST::Traversal
9
+
10
+ attr_reader :stats
11
+
12
+ def initialize
13
+ @stats = {}
14
+ end
15
+
16
+ def on_send(node)
17
+ return super(node) unless component_node?(node)
18
+
19
+ name = component_name(node)
20
+ args = extract_arguments(node, name)
21
+
22
+ @stats[name] = { path: node.loc.expression.source_buffer.name }
23
+ @stats[name][:arguments] = args unless args.empty?
24
+
25
+ super(node) # recursively iterate over children
26
+ end
27
+
28
+ def view_helpers
29
+ @view_helpers ||= ::Ariadne::ViewHelper::HELPERS.keys.map { |key| "ariadne_#{key}".to_sym }
30
+ end
31
+
32
+ def component_node?(node)
33
+ view_helpers.include?(node.method_name) || (node.method_name == :new && !node.receiver.nil? && ::Ariadne::ViewComponents::STATUSES.key?(node.receiver.const_name))
34
+ end
35
+
36
+ def component_name(node)
37
+ return node.receiver.const_name if node.method_name == :new
38
+
39
+ helper_key = node.method_name.to_s.gsub("ariadne_", "").to_sym
40
+ Ariadne::ViewHelper::HELPERS[helper_key]
41
+ end
42
+
43
+ def extract_arguments(node, name)
44
+ args = node.arguments
45
+ res = {}
46
+
47
+ return res if args.empty?
48
+
49
+ kwargs = args.last
50
+ if kwargs.respond_to?(:pairs)
51
+ res = kwargs.pairs.each_with_object({}) do |pair, h|
52
+ h.merge!(extract_values(pair))
53
+ end
54
+ end
55
+
56
+ # Heroicon is the only component that accepts positional arguments.
57
+ res[:icon] = args.first.source if name == "Ariadne::HeroiconComponent" && args.size > 1
58
+
59
+ res
60
+ end
61
+
62
+ def extract_values(pair)
63
+ return { pair.key.value => pair.value.source } unless pair.value.type == :hash
64
+
65
+ flatten_pairs(pair, prefix: "#{pair.key.value}-")
66
+ end
67
+
68
+ def flatten_pairs(pair, prefix: "")
69
+ pair.value.pairs.each_with_object({}) do |value_pair, h|
70
+ if value_pair.value.type == :hash
71
+ h.merge!(flatten_pairs(value_pair, prefix: "#{prefix}#{value_pair.key.value}-"))
72
+ else
73
+ h["#{prefix}#{value_pair.key.value}"] = value_pair.value.source
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :static do
4
+ desc "Generate static JSON mappings of components for easier loading"
5
+ task :dump do
6
+ require File.expand_path("./../../lookbook/config/environment.rb", __dir__)
7
+ require "ariadne/view_components"
8
+ # Loads all components for `.descendants` to work properly
9
+ Dir["./app/components/ariadne/**/*.rb"].sort.each { |file| require file }
10
+
11
+ Ariadne::ViewComponents.dump(:statuses)
12
+ Ariadne::ViewComponents.dump(:constants)
13
+ Ariadne::ViewComponents.dump(:audited_at)
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # "Hacks, but they work" ™
4
+ if !defined?(Rails) || ENV.fetch("DOCS_BUILD", false).blank?
5
+
6
+ # Spoof the Rails environment
7
+ module Rails
8
+ def self.root
9
+ Pathname.new(File.join(File.dirname(__FILE__), "..", ".."))
10
+ end
11
+ end
12
+
13
+ rule "" do |t|
14
+ task_name = t.name
15
+ if task_name == "assets:precompile"
16
+ puts "Stubbing non-existent #{task_name}"
17
+ else
18
+ raise "No task named #{task_name}"
19
+ end
20
+ end
21
+ end
22
+
23
+ spec = Gem::Specification.find_by_name("tailwindcss-rails")
24
+ load "#{spec.gem_dir}/lib/tasks/build.rake"
25
+
26
+ Rake::Task["tailwindcss:build"].enhance do
27
+ if File.exist?("app/assets/builds/tailwind.css")
28
+ puts "Renaming tailwind.css to ariadne_view_components.css..."
29
+ File.rename("app/assets/builds/tailwind.css", "app/assets/builds/ariadne_view_components.css")
30
+ end
31
+ end