brut 0.5.0 → 0.8.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.
Files changed (265) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +7 -0
  4. data/Dockerfile.dx +19 -0
  5. data/Gemfile.lock +1 -1
  6. data/README.md +19 -0
  7. data/assets/YouTubeThumb.pxd +0 -0
  8. data/bin/build +86 -0
  9. data/bin/ci +36 -0
  10. data/bin/docs +39 -9
  11. data/bin/publish +61 -0
  12. data/bin/setup +6 -0
  13. data/brut-css/bin/build +19 -0
  14. data/brut-css/bin/ci +19 -0
  15. data/brut-css/bin/docs +19 -0
  16. data/brut-css/bin/publish +21 -0
  17. data/brut-css/bin/setup +1 -0
  18. data/brut-css/package-lock.json +2 -2
  19. data/brut-css/package.json +1 -1
  20. data/brut-js/bin/build +15 -6
  21. data/brut-js/bin/docs +25 -0
  22. data/brut-js/bin/publish +21 -0
  23. data/brut-js/bin/setup +1 -0
  24. data/brut-js/dx +1 -0
  25. data/brut-js/package-lock.json +2 -2
  26. data/brut-js/package.json +1 -1
  27. data/brut.gemspec +2 -2
  28. data/brutrb.com/bin/setup +1 -0
  29. data/brutrb.com/getting-started.md +3 -0
  30. data/brutrb.com/overview.md +6 -0
  31. data/brutrb.com/tutorial.md +7 -3
  32. data/docs/404.html +2 -2
  33. data/docs/adrs.html +3 -3
  34. data/docs/ai.html +3 -3
  35. data/docs/assets/{app.D6BuVHo9.js → app.DyQLb4Ot.js} +1 -1
  36. data/docs/assets/chunks/@localSearchIndexroot.CmtZyrFA.js +1 -0
  37. data/docs/assets/chunks/{VPLocalSearchBox.BpvHMbx6.js → VPLocalSearchBox.T1iA-eJx.js} +1 -1
  38. data/docs/assets/chunks/{theme.wlAOvi2f.js → theme.ChwsbWjK.js} +2 -2
  39. data/docs/assets/{components.md.iLiv2E9X.js → components.md.DHh-NwKs.js} +3 -3
  40. data/docs/assets/{configuration.md.DmuAdsli.js → configuration.md.D8Wz3oJU.js} +1 -1
  41. data/docs/assets/{forms.md.D8aa_qI-.js → forms.md.BRE85eju.js} +1 -1
  42. data/docs/assets/{getting-started.md.DLplsDUd.js → getting-started.md.2ioiTe-B.js} +6 -3
  43. data/docs/assets/{getting-started.md.DLplsDUd.lean.js → getting-started.md.2ioiTe-B.lean.js} +1 -1
  44. data/docs/assets/overview.md.DlKiRRG_.js +1 -0
  45. data/docs/assets/overview.md.DlKiRRG_.lean.js +1 -0
  46. data/docs/assets/tutorial.md.BIb7XT6j.js +1 -0
  47. data/docs/assets/tutorial.md.BIb7XT6j.lean.js +1 -0
  48. data/docs/assets.html +3 -3
  49. data/docs/brut-js.html +3 -3
  50. data/docs/business-logic.html +3 -3
  51. data/docs/cli.html +3 -3
  52. data/docs/components.html +7 -7
  53. data/docs/configuration.html +5 -5
  54. data/docs/css.html +3 -3
  55. data/docs/custom-element-tests.html +3 -3
  56. data/docs/database-access.html +3 -3
  57. data/docs/database-schema.html +3 -3
  58. data/docs/deployment.html +3 -3
  59. data/docs/dev-environment.html +3 -3
  60. data/docs/dir-structure.html +3 -3
  61. data/docs/doc-conventions.html +3 -3
  62. data/docs/end-to-end-tests.html +3 -3
  63. data/docs/features.html +3 -3
  64. data/docs/flash-and-session.html +3 -3
  65. data/docs/form-constraints.html +3 -3
  66. data/docs/forms.html +5 -5
  67. data/docs/getting-started.html +9 -6
  68. data/docs/handlers.html +3 -3
  69. data/docs/hashmap.json +1 -1
  70. data/docs/hooks.html +3 -3
  71. data/docs/i18n.html +3 -3
  72. data/docs/index.html +3 -3
  73. data/docs/instrumentation.html +3 -3
  74. data/docs/javascript.html +3 -3
  75. data/docs/jobs.html +3 -3
  76. data/docs/keyword-injection.html +3 -3
  77. data/docs/layouts.html +3 -3
  78. data/docs/lsp.html +3 -3
  79. data/docs/markdown-examples.html +3 -3
  80. data/docs/middleware.html +3 -3
  81. data/docs/overview.html +5 -5
  82. data/docs/pages.html +3 -3
  83. data/docs/recipes/alternate-layouts.html +3 -3
  84. data/docs/recipes/authentication.html +3 -3
  85. data/docs/recipes/blank-layouts.html +3 -3
  86. data/docs/recipes/custom-flash.html +3 -3
  87. data/docs/recipes/indexed-forms.html +3 -3
  88. data/docs/recipes/migrations.html +3 -3
  89. data/docs/recipes/text-field-component.html +3 -3
  90. data/docs/roadmap.html +3 -3
  91. data/docs/routes.html +3 -3
  92. data/docs/security.html +3 -3
  93. data/docs/seed-data.html +3 -3
  94. data/docs/space-time-continuum.html +3 -3
  95. data/docs/tutorial.html +5 -5
  96. data/docs/unit-tests.html +3 -3
  97. data/docs/why.html +3 -3
  98. data/lib/brut/framework/mcp.rb +1 -1
  99. data/lib/brut/front_end/components/form_tag.rb +2 -2
  100. data/lib/brut/version.rb +1 -1
  101. data/mkbrut/.gitignore +16 -0
  102. data/mkbrut/CODE_OF_CONDUCT.txt +100 -0
  103. data/mkbrut/Gemfile +3 -0
  104. data/mkbrut/Gemfile.lock +19 -0
  105. data/mkbrut/LICENSE.txt +370 -0
  106. data/mkbrut/README.md +145 -0
  107. data/mkbrut/Rakefile +2 -0
  108. data/mkbrut/bin/build +36 -0
  109. data/mkbrut/bin/ci +19 -0
  110. data/mkbrut/bin/docs +19 -0
  111. data/mkbrut/bin/publish +129 -0
  112. data/mkbrut/bin/rake +16 -0
  113. data/mkbrut/bin/setup +30 -0
  114. data/mkbrut/brut-welcome.png +0 -0
  115. data/mkbrut/deploy/.dockerignore +2 -0
  116. data/mkbrut/deploy/Dockerfile +25 -0
  117. data/mkbrut/exe/mkbrut +5 -0
  118. data/mkbrut/lib/mkbrut/app.rb +79 -0
  119. data/mkbrut/lib/mkbrut/app_id.rb +8 -0
  120. data/mkbrut/lib/mkbrut/app_name.rb +29 -0
  121. data/mkbrut/lib/mkbrut/app_options.rb +36 -0
  122. data/mkbrut/lib/mkbrut/base.rb +57 -0
  123. data/mkbrut/lib/mkbrut/cli.rb +107 -0
  124. data/mkbrut/lib/mkbrut/erb_binding_delegate.rb +20 -0
  125. data/mkbrut/lib/mkbrut/internet_identifier.rb +32 -0
  126. data/mkbrut/lib/mkbrut/invalid_identifier.rb +4 -0
  127. data/mkbrut/lib/mkbrut/ops/add_css_import.rb +42 -0
  128. data/mkbrut/lib/mkbrut/ops/add_i18n_message.rb +74 -0
  129. data/mkbrut/lib/mkbrut/ops/add_method.rb +48 -0
  130. data/mkbrut/lib/mkbrut/ops/append_to_file.rb +20 -0
  131. data/mkbrut/lib/mkbrut/ops/base_op.rb +21 -0
  132. data/mkbrut/lib/mkbrut/ops/copy_file.rb +12 -0
  133. data/mkbrut/lib/mkbrut/ops/insert_code_in_method.rb +58 -0
  134. data/mkbrut/lib/mkbrut/ops/insert_route.rb +52 -0
  135. data/mkbrut/lib/mkbrut/ops/mkdir.rb +13 -0
  136. data/mkbrut/lib/mkbrut/ops/prism_parsing_op.rb +70 -0
  137. data/mkbrut/lib/mkbrut/ops/render_template.rb +26 -0
  138. data/mkbrut/lib/mkbrut/ops/skip_file.rb +10 -0
  139. data/mkbrut/lib/mkbrut/ops.rb +16 -0
  140. data/mkbrut/lib/mkbrut/organization.rb +5 -0
  141. data/mkbrut/lib/mkbrut/prefix.rb +26 -0
  142. data/mkbrut/lib/mkbrut/prefixed_io.rb +16 -0
  143. data/mkbrut/lib/mkbrut/segments/bare_bones.rb +185 -0
  144. data/mkbrut/lib/mkbrut/segments/demo.rb +121 -0
  145. data/mkbrut/lib/mkbrut/segments/heroku.rb +30 -0
  146. data/mkbrut/lib/mkbrut/segments/sidekiq.rb +3 -0
  147. data/mkbrut/lib/mkbrut/segments.rb +8 -0
  148. data/mkbrut/lib/mkbrut/version.rb +3 -0
  149. data/mkbrut/lib/mkbrut/versions.rb +13 -0
  150. data/mkbrut/lib/mkbrut.rb +18 -0
  151. data/mkbrut/mkbrut.gemspec +32 -0
  152. data/mkbrut/templates/Base/.dockerignore +25 -0
  153. data/mkbrut/templates/Base/.env.development.erb +60 -0
  154. data/mkbrut/templates/Base/.env.test.erb +8 -0
  155. data/mkbrut/templates/Base/.gitignore +31 -0
  156. data/mkbrut/templates/Base/.projections.json +59 -0
  157. data/mkbrut/templates/Base/Dockerfile.dx +205 -0
  158. data/mkbrut/templates/Base/Gemfile.erb +53 -0
  159. data/mkbrut/templates/Base/Procfile.development +5 -0
  160. data/mkbrut/templates/Base/Procfile.test +1 -0
  161. data/mkbrut/templates/Base/README.md +4 -0
  162. data/mkbrut/templates/Base/README.md.erb +40 -0
  163. data/mkbrut/templates/Base/app/bootstrap.rb +61 -0
  164. data/mkbrut/templates/Base/app/config/i18n/en/1_defaults.rb +128 -0
  165. data/mkbrut/templates/Base/app/config/i18n/en/2_app.rb +24 -0
  166. data/mkbrut/templates/Base/app/public/static/manifest.json.erb +33 -0
  167. data/mkbrut/templates/Base/app/src/app.rb.erb +37 -0
  168. data/mkbrut/templates/Base/app/src/back_end/data_models/app_data_model.rb +5 -0
  169. data/mkbrut/templates/Base/app/src/back_end/data_models/db.rb +19 -0
  170. data/mkbrut/templates/Base/app/src/back_end/data_models/migrations/20240101130000_citext.rb +6 -0
  171. data/mkbrut/templates/Base/app/src/back_end/data_models/seed/seed_data.rb +9 -0
  172. data/mkbrut/templates/Base/app/src/front_end/components/app_component.rb +8 -0
  173. data/mkbrut/templates/Base/app/src/front_end/components/custom_element_registration.rb.erb +7 -0
  174. data/mkbrut/templates/Base/app/src/front_end/css/index.css +2 -0
  175. data/mkbrut/templates/Base/app/src/front_end/css/svgs.css +12 -0
  176. data/mkbrut/templates/Base/app/src/front_end/forms/app_form.rb +4 -0
  177. data/mkbrut/templates/Base/app/src/front_end/handlers/app_handler.rb +4 -0
  178. data/mkbrut/templates/Base/app/src/front_end/images/LogoPylon.png +0 -0
  179. data/mkbrut/templates/Base/app/src/front_end/images/LogoTransit.png +0 -0
  180. data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-120x120.png +0 -0
  181. data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-152x152.png +0 -0
  182. data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-167x167.png +0 -0
  183. data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-180x180.png +0 -0
  184. data/mkbrut/templates/Base/app/src/front_end/images/favicon.ico +0 -0
  185. data/mkbrut/templates/Base/app/src/front_end/images/icon.png +0 -0
  186. data/mkbrut/templates/Base/app/src/front_end/images/mkicons.sh +6 -0
  187. data/mkbrut/templates/Base/app/src/front_end/js/index.js +6 -0
  188. data/mkbrut/templates/Base/app/src/front_end/layouts/default_layout.rb.erb +73 -0
  189. data/mkbrut/templates/Base/app/src/front_end/pages/app_page.rb +11 -0
  190. data/mkbrut/templates/Base/app/src/front_end/pages/home_page.rb +62 -0
  191. data/mkbrut/templates/Base/app/src/front_end/support/app_session.rb +6 -0
  192. data/mkbrut/templates/Base/app/src/front_end/svgs/README.md +5 -0
  193. data/mkbrut/templates/Base/app/src/front_end/svgs/comment-button.svg +59 -0
  194. data/mkbrut/templates/Base/bin/README.md.erb +5 -0
  195. data/mkbrut/templates/Base/bin/build-assets +7 -0
  196. data/mkbrut/templates/Base/bin/ci +39 -0
  197. data/mkbrut/templates/Base/bin/console +31 -0
  198. data/mkbrut/templates/Base/bin/db +9 -0
  199. data/mkbrut/templates/Base/bin/dbconsole +51 -0
  200. data/mkbrut/templates/Base/bin/dev +25 -0
  201. data/mkbrut/templates/Base/bin/release +26 -0
  202. data/mkbrut/templates/Base/bin/run +86 -0
  203. data/mkbrut/templates/Base/bin/scaffold +9 -0
  204. data/mkbrut/templates/Base/bin/setup +256 -0
  205. data/mkbrut/templates/Base/bin/startup-message +65 -0
  206. data/mkbrut/templates/Base/bin/test +9 -0
  207. data/mkbrut/templates/Base/bin/test-server +29 -0
  208. data/mkbrut/templates/Base/bin/watch-and-build-assets +37 -0
  209. data/mkbrut/templates/Base/config.ru +16 -0
  210. data/mkbrut/templates/Base/docker-compose.dx.yml +92 -0
  211. data/mkbrut/templates/Base/dx/README.md +28 -0
  212. data/mkbrut/templates/Base/dx/bash_customizations +12 -0
  213. data/mkbrut/templates/Base/dx/bash_customizations.local +8 -0
  214. data/mkbrut/templates/Base/dx/build +107 -0
  215. data/mkbrut/templates/Base/dx/docker-compose.env.erb +25 -0
  216. data/mkbrut/templates/Base/dx/dx.sh.lib +137 -0
  217. data/mkbrut/templates/Base/dx/exec +68 -0
  218. data/mkbrut/templates/Base/dx/prune +19 -0
  219. data/mkbrut/templates/Base/dx/show-help-in-app-container-then-wait.sh +38 -0
  220. data/mkbrut/templates/Base/dx/start +30 -0
  221. data/mkbrut/templates/Base/dx/stop +23 -0
  222. data/mkbrut/templates/Base/package.json.erb +37 -0
  223. data/mkbrut/templates/Base/puma.config.rb +53 -0
  224. data/mkbrut/templates/Base/specs/e2e/home_page.spec.rb.erb +23 -0
  225. data/mkbrut/templates/Base/specs/front_end/js/SpecHelper.js +24 -0
  226. data/mkbrut/templates/Base/specs/front_end/pages/home_page.spec.rb +22 -0
  227. data/mkbrut/templates/Base/specs/lint_factories.spec.rb +7 -0
  228. data/mkbrut/templates/Base/specs/spec_helper.rb +78 -0
  229. data/mkbrut/templates/Base/specs/support.rb +2 -0
  230. data/mkbrut/templates/segments/BareBones/app/src/front_end/handlers/trigger_exception_handler.rb +24 -0
  231. data/mkbrut/templates/segments/BareBones/app/src/front_end/js/Example.js.erb +49 -0
  232. data/mkbrut/templates/segments/BareBones/specs/front_end/handlers/trigger_exception_handler.spec.rb +41 -0
  233. data/mkbrut/templates/segments/BareBones/specs/front_end/js/Example.spec.js.erb +38 -0
  234. data/mkbrut/templates/segments/Demo/app/src/back_end/data_models/db/guestbook_message.rb +3 -0
  235. data/mkbrut/templates/segments/Demo/app/src/back_end/data_models/migrations/20250628194124_guestbook.rb +14 -0
  236. data/mkbrut/templates/segments/Demo/app/src/front_end/components/flash_component.rb +36 -0
  237. data/mkbrut/templates/segments/Demo/app/src/front_end/css/constraint-violations.css +18 -0
  238. data/mkbrut/templates/segments/Demo/app/src/front_end/css/fonts.css +19 -0
  239. data/mkbrut/templates/segments/Demo/app/src/front_end/fonts/monaspace-xenon.ttf +0 -0
  240. data/mkbrut/templates/segments/Demo/app/src/front_end/forms/guestbook_message_form.rb +4 -0
  241. data/mkbrut/templates/segments/Demo/app/src/front_end/handlers/guestbook_message_handler.rb +64 -0
  242. data/mkbrut/templates/segments/Demo/app/src/front_end/pages/guestbook_page/message_component.rb +41 -0
  243. data/mkbrut/templates/segments/Demo/app/src/front_end/pages/guestbook_page.rb +43 -0
  244. data/mkbrut/templates/segments/Demo/app/src/front_end/pages/new_guestbook_message_page.rb +64 -0
  245. data/mkbrut/templates/segments/Demo/specs/back_end/data_models/db/guestbook_message.spec.rb +5 -0
  246. data/mkbrut/templates/segments/Demo/specs/e2e/guest_message.spec.rb +54 -0
  247. data/mkbrut/templates/segments/Demo/specs/factories/db/guestbook_message.factory.rb +7 -0
  248. data/mkbrut/templates/segments/Demo/specs/front_end/components/flash_component.spec.rb +5 -0
  249. data/mkbrut/templates/segments/Demo/specs/front_end/handlers/guestbook_message_handler.spec.rb +122 -0
  250. data/mkbrut/templates/segments/Demo/specs/front_end/pages/guestbook_page/message_component.spec.rb +5 -0
  251. data/mkbrut/templates/segments/Demo/specs/front_end/pages/guestbook_page.spec.rb +52 -0
  252. data/mkbrut/templates/segments/Demo/specs/front_end/pages/new_guestbook_message_page.spec.rb +5 -0
  253. data/mkbrut/templates/segments/Heroku/bin/deploy +11 -0
  254. data/mkbrut/templates/segments/Heroku/deploy/Dockerfile +125 -0
  255. data/mkbrut/templates/segments/Heroku/deploy/docker-entrypoint +15 -0
  256. data/mkbrut/templates/segments/Heroku/deploy/heroku_config.rb +26 -0
  257. metadata +185 -21
  258. data/docs/assets/chunks/@localSearchIndexroot.COP2Bcmp.js +0 -1
  259. data/docs/assets/overview.md.iMnwLO4x.js +0 -1
  260. data/docs/assets/overview.md.iMnwLO4x.lean.js +0 -1
  261. data/docs/assets/tutorial.md.BYXj4cOu.js +0 -1
  262. data/docs/assets/tutorial.md.BYXj4cOu.lean.js +0 -1
  263. /data/docs/assets/{components.md.iLiv2E9X.lean.js → components.md.DHh-NwKs.lean.js} +0 -0
  264. /data/docs/assets/{configuration.md.DmuAdsli.lean.js → configuration.md.D8Wz3oJU.lean.js} +0 -0
  265. /data/docs/assets/{forms.md.D8aa_qI-.lean.js → forms.md.BRE85eju.lean.js} +0 -0
@@ -0,0 +1,74 @@
1
+ class MKBrut::Ops::AddI18nMessage < MKBrut::Ops::PrismParsingOp
2
+ def initialize(project_root:, hash:)
3
+ @file = project_root / "app" / "config" / "i18n" / "en" / "2_app.rb"
4
+ @hash = hash
5
+ end
6
+
7
+ def call
8
+ if dry_run?
9
+ puts "Would merge:\n#{@hash}\ninto #{@file}"
10
+ return
11
+ end
12
+ parse_file!
13
+
14
+ hash_node = @tree.value.statements.body.detect { it.is_a?(Prism::HashNode) }
15
+ if !hash_node
16
+ raise "'#{@file}' did not have a hash node, so we cannot insert a new i18n message"
17
+ end
18
+
19
+ # eval the source to get a real hash of the contents
20
+ start_offset = hash_node.location.start_offset
21
+ end_offset = hash_node.location.end_offset
22
+ original_code = @source[start_offset...end_offset]
23
+ original_hash = eval(original_code, binding, @file.to_s)
24
+
25
+ new_hash = deep_merge(original_hash,@hash)
26
+
27
+ formatted_hash = format_hash(new_hash)
28
+
29
+ new_source = @source.dup
30
+ new_source[start_offset...end_offset] = formatted_hash
31
+
32
+ File.open(@file, "w") do |file|
33
+ file.puts new_source
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def deep_merge(a, b)
40
+ a.merge(b) do |_key, old_val, new_val|
41
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
42
+ deep_merge(old_val, new_val)
43
+ else
44
+ new_val
45
+ end
46
+ end
47
+ end
48
+
49
+ # NASTY, but not currently sure a better what do it.
50
+ def format_hash(hash, trailing_comma = "", indent = "")
51
+ string = "{\n"
52
+ hash.each do |key, value|
53
+ key_code = if key.kind_of?(Symbol)
54
+ if key =~ /^[A-Za-z_][A-Za-z0-9_]*$/
55
+ "#{key}:"
56
+ else
57
+ "'#{key}':"
58
+ end
59
+ else
60
+ "#{key} =>"
61
+ end
62
+ value_code = case value
63
+ when String
64
+ then "\"#{value}\",\n"
65
+ when Hash
66
+ format_hash(value, ",", indent + " ")
67
+ end
68
+ string << "#{indent} #{key_code} #{value_code}"
69
+ end
70
+ string << "#{indent}}#{trailing_comma}\n"
71
+ string
72
+ end
73
+ end
74
+
@@ -0,0 +1,48 @@
1
+ class MKBrut::Ops::AddMethod < MKBrut::Ops::PrismParsingOp
2
+ def initialize(file:, class_name:, code:)
3
+ @file = file
4
+ @class_name = class_name
5
+ @code = code.gsub(/^\n\s*$/,"").gsub(/\n$/,"")
6
+ end
7
+
8
+ def call
9
+ if dry_run?
10
+ puts "Would add method:\n#{@code}\nto #{@class_name} in '#{@file}'"
11
+ return
12
+ end
13
+ class_node = find_class(class_name: @class_name, assumed_body: false)
14
+
15
+ insert_offset = nil
16
+ class_body_nodes = case class_node.body
17
+ when Prism::StatementsNode
18
+ class_node.body.body
19
+ when nil
20
+ []
21
+ else
22
+ [class_node.body]
23
+ end
24
+
25
+ class_body_nodes.each do |node|
26
+ if node.is_a?(Prism::CallNode) && node.name == "private"
27
+ insert_offset = node.location.start_offset
28
+ break
29
+ end
30
+ end
31
+
32
+ if insert_offset.nil?
33
+ # Use the final end of the class
34
+ insert_offset = class_node.location.end_offset - 3
35
+ end
36
+
37
+ class_start_line = class_node.location.start_line
38
+ class_indent = @source.lines[class_start_line - 1][/^\s*/] || ""
39
+ method_indent = class_indent + " "
40
+
41
+ indented_method_code = @code.lines.map { |line| method_indent + line }.join
42
+ insert_text = "\n" + indented_method_code + "\n"
43
+
44
+ updated_source = @source.dup.insert(insert_offset, insert_text)
45
+ File.write(@file, updated_source)
46
+ updated_source
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ class MKBrut::Ops::AppendToFile < MKBrut::Ops::BaseOp
2
+ def initialize(file:, content:)
3
+ @file = file
4
+ @content = content
5
+ end
6
+
7
+ def call
8
+ if dry_run?
9
+ puts "Would append to #{@file}:\n#{@content}\n"
10
+ return
11
+ end
12
+
13
+ contents = File.read(@file)
14
+ File.open(@file, "w") do |file|
15
+ file.puts contents
16
+ file.puts "\n"
17
+ file.puts @content
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ class MKBrut::Ops::BaseOp
2
+ @dry_run = false
3
+
4
+ def self.dry_run=(value)
5
+ MKBrut::Ops::BaseOp.instance_variable_set(:@dry_run, value)
6
+ end
7
+
8
+ def self.dry_run? = !!MKBrut::Ops::BaseOp.instance_variable_get(:@dry_run)
9
+ def dry_run? = self.class.dry_run?
10
+
11
+ def call = raise "Subclass must implement"
12
+
13
+ def self.fileutils_args
14
+ if self.dry_run?
15
+ { noop: true, verbose: true }
16
+ else
17
+ {}
18
+ end
19
+ end
20
+ def fileutils_args = self.class.fileutils_args
21
+ end
@@ -0,0 +1,12 @@
1
+ require "fileutils"
2
+
3
+ class MKBrut::Ops::CopyFile < MKBrut::Ops::BaseOp
4
+ def initialize(source, destination_root:)
5
+ @source = source
6
+ @destination_root = destination_root
7
+ end
8
+ def call
9
+ FileUtils.cp(@source, @destination_root / @source.basename, **fileutils_args)
10
+ end
11
+ def to_s = "Copy '#{@source}' to '#{@destination_root}'"
12
+ end
@@ -0,0 +1,58 @@
1
+ class MKBrut::Ops::InsertCodeInMethod < MKBrut::Ops::PrismParsingOp
2
+ def initialize(file:, class_name:, method_name:, code:, where: :end)
3
+ @file = file
4
+ @class_name = class_name
5
+ @method_name = method_name.to_sym
6
+ @code = code
7
+ @where = where
8
+ end
9
+
10
+ def call
11
+ method_node = find_method(class_name: @class_name, method_name: @method_name)
12
+
13
+ insertion_point = if @where == :start
14
+ insertion_point_for_code_at_start_of_method(method_node: method_node)
15
+ else
16
+ insertion_point_for_code_at_end_of_method(method_node: method_node)
17
+ end
18
+ indented_code = indent_code_for_method(method_node: method_node)
19
+
20
+ new_source = @source.dup.insert(insertion_point, indented_code)
21
+ File.write(@file, new_source)
22
+ end
23
+
24
+ private
25
+
26
+ def indent_code_for_method(method_node:)
27
+
28
+ method_start_line = method_node.location.start_line
29
+ spaces_before_def = @source.lines[method_start_line - 1][/^\s*/] || ""
30
+ spaces_for_code_in_method = spaces_before_def + " "
31
+
32
+ post_indent = if @where == :start
33
+ "\n#{spaces_before_def}"
34
+ else
35
+ ""
36
+ end
37
+
38
+
39
+ "\n" +
40
+ @code.split(/\n/).map { |line|
41
+ spaces_for_code_in_method + line
42
+ }.join("\n") + post_indent
43
+ end
44
+
45
+ def insertion_point_for_code_at_end_of_method(method_node:)
46
+ line_number_of_method_end = method_node.location.end_line - 1
47
+ length_of_method_end = @source.lines[line_number_of_method_end].length
48
+
49
+ method_node.location.end_offset - length_of_method_end
50
+ end
51
+
52
+ def insertion_point_for_code_at_start_of_method(method_node:)
53
+ line_number_of_method_start = method_node.location.start_line - 1
54
+ length_of_method_start = @source.lines[line_number_of_method_start].length
55
+
56
+ method_node.location.start_offset + length_of_method_start
57
+ end
58
+ end
@@ -0,0 +1,52 @@
1
+ class MKBrut::Ops::InsertRoute < MKBrut::Ops::PrismParsingOp
2
+ def initialize(project_root:, code:)
3
+ @file = project_root / "app" / "src" / "app.rb"
4
+ @code = code
5
+ end
6
+
7
+ def call
8
+ if dry_run?
9
+ puts "Would insert route:\n#{@code}\ninto #{@file}"
10
+ return
11
+ end
12
+ app_class_node = find_class(class_name: "App")
13
+
14
+ routes_block = find_routes_block(app_class_node)
15
+
16
+ if !routes_block
17
+ raise "'App' in '#{@file}' did not have a routes block, so we cannot insert a new route"
18
+ end
19
+
20
+ end_offset = routes_block.block.location.end_offset
21
+ indented_line = " #{@code}\n "
22
+ new_source = @source.dup.insert(end_offset - 3, indented_line)
23
+
24
+ File.open(@file, "w") do |file|
25
+ file.puts new_source
26
+ end
27
+ end
28
+
29
+ def find_routes_block(class_node)
30
+ statements = case class_node.body
31
+ when Prism::StatementsNode
32
+ class_node.body.body
33
+ when nil
34
+ []
35
+ else
36
+ [class_node.body]
37
+ end
38
+
39
+ statements.detect do |statement|
40
+ if statement.is_a?(Prism::CallNode)
41
+ if statement.name == :routes
42
+ statement.block
43
+ else
44
+ false
45
+ end
46
+ else
47
+ false
48
+ end
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,13 @@
1
+ require "fileutils"
2
+
3
+ class MKBrut::Ops::Mkdir < MKBrut::Ops::BaseOp
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ def call
9
+ FileUtils.mkdir_p(@path, **fileutils_args)
10
+ end
11
+
12
+ def to_s = "Make Dir '#{@path}'"
13
+ end
@@ -0,0 +1,70 @@
1
+ require "prism"
2
+ class MKBrut::Ops::PrismParsingOp < MKBrut::Ops::BaseOp
3
+ def initialize(file:)
4
+ @file = file
5
+ end
6
+
7
+ class ClassNotInSource < StandardError
8
+ def initialize(file:, class_name:)
9
+ super("Could not find the class '#{class_name}' inside '#{file}'")
10
+ end
11
+ end
12
+
13
+ class MethodNotInClass < StandardError
14
+ def initialize(file:, class_name:, method_name:)
15
+ super("Could not find the method '#{method_name}' in class '#{class_name}' inside '#{file}'")
16
+ end
17
+ end
18
+
19
+ class SourceNotParseable < StandardError
20
+ def initialize(tree_errors:, file:)
21
+ error_message = tree_errors.map { |error|
22
+ "#{error.message} (line #{error.location.start_line}, column #{error.location.start_column})"
23
+ }.join(", ")
24
+ super("Failed to parse file '#{file}': #{error_message}")
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def parse_file!
31
+ source = File.read(@file)
32
+ tree = Prism.parse(source)
33
+
34
+ if !tree.success?
35
+ raise SourceNotParseable.new(tree_errors: tree.errors, file: @file)
36
+ end
37
+ @tree = tree
38
+ @source = source
39
+ end
40
+
41
+ def find_class(class_name:, assumed_body: true)
42
+ if !@tree
43
+ parse_file!
44
+ end
45
+ class_node = @tree.value.statements.body.detect { |node|
46
+ node.is_a?(Prism::ClassNode) && node.constant_path.slice == class_name
47
+ }
48
+
49
+ if !class_node
50
+ raise ClassNotInSource.new(file: @file, class_name: class_name)
51
+ end
52
+
53
+ if !class_node.body.respond_to?(:body) && assumed_body
54
+ raise "The class '#{class_name}' in '#{file}' does not have any methods"
55
+ end
56
+ class_node
57
+ end
58
+
59
+ def find_method(class_name:, method_name:)
60
+ class_node = find_class(class_name:)
61
+ method_node = class_node.body.body.detect { |node|
62
+ node.is_a?(Prism::DefNode) && node.name == @method_name
63
+ }
64
+
65
+ if !method_node
66
+ raise MethodNotInClass.new(file: @file, class_name: class_name, method_name: @method_name)
67
+ end
68
+ method_node
69
+ end
70
+ end
@@ -0,0 +1,26 @@
1
+ require "erb"
2
+ class MKBrut::Ops::RenderTemplate < MKBrut::Ops::BaseOp
3
+
4
+ def initialize(source, destination_root:, erb_binding:)
5
+ @erb = source
6
+ @destination_file = destination_root / @erb.basename.sub_ext("")
7
+ @erb_binding = erb_binding
8
+ end
9
+
10
+ def call
11
+ if dry_run?
12
+ puts "Render '#{@destination_file}'"
13
+ return
14
+ end
15
+ template = File.read(@erb)
16
+ File.open(@destination_file, "w") do |file|
17
+ file.puts ERB.new(
18
+ template,
19
+ trim_mode: "-"
20
+ ).result(
21
+ @erb_binding.instance_eval { binding }
22
+ )
23
+ end
24
+ end
25
+ def to_s = "ERB '#{@erb}' to '#{@destination_file}'"
26
+ end
@@ -0,0 +1,10 @@
1
+ class MKBrut::Ops::SkipFile
2
+ def initialize(source)
3
+ @source = source
4
+ end
5
+
6
+ def call
7
+ end
8
+
9
+ def to_s = "Skip #{@source}"
10
+ end
@@ -0,0 +1,16 @@
1
+ module MKBrut
2
+ module Ops
3
+ autoload :BaseOp, "mkbrut/ops/base_op"
4
+ autoload :Mkdir, "mkbrut/ops/mkdir"
5
+ autoload :CopyFile, "mkbrut/ops/copy_file"
6
+ autoload :RenderTemplate, "mkbrut/ops/render_template"
7
+ autoload :SkipFile, "mkbrut/ops/skip_file"
8
+ autoload :InsertRoute, "mkbrut/ops/insert_route"
9
+ autoload :InsertCodeInMethod, "mkbrut/ops/insert_code_in_method"
10
+ autoload :AppendToFile, "mkbrut/ops/append_to_file"
11
+ autoload :PrismParsingOp, "mkbrut/ops/prism_parsing_op"
12
+ autoload :AddI18nMessage, "mkbrut/ops/add_i18n_message"
13
+ autoload :AddCSSImport, "mkbrut/ops/add_css_import"
14
+ autoload :AddMethod, "mkbrut/ops/add_method"
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ class MKBrut::Organization < MKBrut::InternetIdentifier
2
+ def initialize(value)
3
+ super(:organization, value)
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ module MKBrut
2
+ class Prefix
3
+ def self.from_app_id(app_id)
4
+ app_id = app_id.to_s
5
+ prefix = if app_id =~ /^[^-]+[a-z]-[a-z]/
6
+ app_id.split("-")[0..1].map { it[0] }.join("")
7
+ else
8
+ app_id[0..1]
9
+ end
10
+ self.new(prefix)
11
+ end
12
+
13
+ def initialize(identifier)
14
+ @identifier = identifier.to_s
15
+ if @identifier.length != 2
16
+ raise InvalidIdentifier, "prefix '#{@identifier}' must be 2 characters"
17
+ end
18
+ if @identifier !~ /^[a-z]+$/
19
+ raise InvalidIdentifier, "prefix must be only lower case ASCII letters"
20
+ end
21
+ end
22
+
23
+ def to_s = @identifier
24
+ alias to_str to_s
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ require "delegate"
2
+ class PrefixedIO < Delegator
3
+ def initialize(io, prefix)
4
+ @io = io
5
+ @prefix = "[ #{prefix} ] "
6
+ end
7
+
8
+ def __getobj__ = @io
9
+
10
+ def puts(*args)
11
+ args.each do |arg|
12
+ @io.puts(@prefix + arg.to_s)
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,185 @@
1
+ # The bare bones configuration on top of a blank Brut app.
2
+ class MKBrut::Segments::BareBones < MKBrut::Base
3
+
4
+ def self.friendly_name = "Bare bones framing"
5
+
6
+ def initialize(app_options:, current_dir:, templates_dir:)
7
+ @project_root = current_dir / app_options.app_name
8
+ @templates_dir = templates_dir / "segments" / "BareBones"
9
+ @erb_binding = ErbBindingDelegate.new(app_options)
10
+ end
11
+
12
+ def add!
13
+
14
+ operations = copy_files(@templates_dir, @project_root) +
15
+ other_operations(@project_root)
16
+
17
+ operations.each do |operation|
18
+ operation.call
19
+ end
20
+
21
+ end
22
+
23
+ def other_operations(project_root)
24
+ [
25
+ MKBrut::Ops::InsertRoute.new(
26
+ project_root: @project_root,
27
+ code: %{path "/trigger_exception", method: :get}
28
+ ),
29
+ MKBrut::Ops::InsertCodeInMethod.new(
30
+ file: @project_root / "app" / "src" / "app.rb",
31
+ class_name: "App",
32
+ method_name: "initialize",
33
+ code: %{
34
+ Brut.container.store(
35
+ "trigger_exception_key",
36
+ String,
37
+ "String used to prevent anyone from triggering exceptions in TriggerExceptionHandler"
38
+ ) do
39
+ ENV.fetch("TRIGGER_EXCEPTION_KEY")
40
+ end},
41
+ ),
42
+ InsertCustomElement.new(
43
+ project_root: @project_root,
44
+ element_class_name: "Example",
45
+ ),
46
+ MKBrut::Ops::InsertCodeInMethod.new(
47
+ file: @project_root / "app" / "src" / "front_end" / "pages" / "home_page.rb",
48
+ class_name: "HomePage",
49
+ method_name: "page_template",
50
+ code: %{
51
+ #{ @erb_binding.prefix }_example(
52
+ transform: "upper",
53
+ class: [ "pos-fixed",
54
+ "bottom-0",
55
+ "left-0",
56
+ "w-100",
57
+ "ff-sans",
58
+ "lh-title",
59
+ "tracked",
60
+ "f-5",
61
+ "f-6-ns",
62
+ "tc",
63
+ "pa-3",
64
+ "mt-3",
65
+ "db", ]
66
+ ) do
67
+ "We Like the Web"
68
+ end
69
+ }
70
+ ),
71
+ InsertEndToEndTestCode.new(
72
+ file: @project_root / "specs" / "e2e" / "home_page.spec.rb",
73
+ code: %{
74
+ example = page.locator("#{ @erb_binding.prefix }-example")
75
+ # The #{ @erb_binding.prefix }-example custom element will transform
76
+ # the text it contains. Since this is an end-to-end test
77
+ # the element should've done its thing and given us
78
+ # upper-case text.
79
+ expect(example).to have_text("WE LIKE THE WEB") }
80
+ ),
81
+ MKBrut::Ops::AppendToFile.new(
82
+ file: @project_root / ".env.development",
83
+ content: %{
84
+ # Key used to allow triggering an exception. This is required to prevent
85
+ # just anyone from triggering one.
86
+ TRIGGER_EXCEPTION_KEY=dev-trigger-exception
87
+ }
88
+ ),
89
+ MKBrut::Ops::AppendToFile.new(
90
+ file: @project_root / ".env.test",
91
+ content: "TRIGGER_EXCEPTION_KEY=test-trigger-exception"
92
+ ),
93
+
94
+ ]
95
+ end
96
+ class InsertCustomElement < MKBrut::Ops::BaseOp
97
+ def initialize(project_root:, element_class_name:)
98
+ @file = project_root / "app" / "src" / "front_end" / "js" / "index.js"
99
+ @element_class_name = element_class_name
100
+ end
101
+ def call
102
+ if dry_run?
103
+ puts "Would insert custom element '#{@element_class_name}' into #{@file}"
104
+ return
105
+ end
106
+ inserted = false
107
+ new_source = []
108
+ File.read(@file).split("\n").each do |line|
109
+ regexp = /^document\.addEventListener\(\"DOMContentLoaded\"/
110
+ if line.match?(regexp)
111
+ new_source << %{import #{@element_class_name} from "./#{@element_class_name}"}
112
+ new_source << line
113
+ new_source << %{ #{@element_class_name}.define()}
114
+ inserted = true
115
+ else
116
+ new_source << line
117
+ end
118
+ end
119
+ if !inserted
120
+ raise "Could not find a place to insert code in '#{@file}'. Trying to find a line that matches this regular expression:\n\n#{regexp.inspect}"
121
+ end
122
+ File.open(@file, "w") do |file|
123
+ file.puts new_source.join("\n")
124
+ end
125
+ end
126
+ end
127
+
128
+ class InsertEndToEndTestCode < MKBrut::Ops::PrismParsingOp
129
+ def initialize(file:, code:)
130
+ @file = file
131
+ @code = code
132
+ end
133
+ def call
134
+ if dry_run?
135
+ puts "Would insert end-to-end test code into #{@file}:\n\n#{@code}\n"
136
+ return
137
+ end
138
+ parse_file!
139
+
140
+ found_describe = false
141
+ first_it_block = nil
142
+
143
+ @tree.value.statements.body.each do |top|
144
+ if top.is_a?(Prism::CallNode) &&
145
+ top.name == :describe &&
146
+ top.block
147
+ found_describe = true
148
+
149
+ statements = top.block.body
150
+ if statements.respond_to?(:body)
151
+ statements.body.each do |statement|
152
+ if statement.is_a?(Prism::CallNode) &&
153
+ statement.name == :it &&
154
+ statement.block
155
+
156
+ first_it_block = statement
157
+ break
158
+
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ if !first_it_block
165
+ if found_describe
166
+ raise "Could not find an 'it' block inside the first 'describe' in '#{@file}'"
167
+ else
168
+ raise "Could not find a 'describe' block in '#{@file}'"
169
+ end
170
+ end
171
+
172
+ insertion_point = first_it_block.block.location.end_offset - 3
173
+
174
+ block_line = @source.lines[first_it_block.location.start_line - 1]
175
+ describe_indent = block_line[/^\s*/]
176
+ it_indent = describe_indent + " "
177
+
178
+ new_source = @source.dup.insert(insertion_point, "\n#{it_indent}#{@code}\n#{describe_indent}")
179
+ File.open(@file, "w") do |file|
180
+ file.puts new_source
181
+ end
182
+ end
183
+ end
184
+
185
+ end