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,38 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ # Ideally, the message below is shown after everything starts up. We can't
6
+ # achieve this using healthchecks because the interval for a healtcheck is
7
+ # also an initial delay, and we don't really want to do healthchecks on
8
+ # our DB or Redis every 2 seconds. So, we sleep just a bit to let
9
+ # the other containers start up and vomit out their output first.
10
+ sleep 2
11
+ # Output some helpful messaging when invoking `dx/start` (which itself is
12
+ # a convenience script for `docker compose up`.
13
+ #
14
+ # Adding this to work around the mild inconvenience of the `app` container's
15
+ # entrypoint generating no output.
16
+ #
17
+ cat <<-'PROMPT'
18
+
19
+
20
+
21
+ 🎉 Dev Environment Initialized! 🎉
22
+
23
+ ℹ️ To use this environment, open a new terminal and run
24
+
25
+ dx/exec bash
26
+
27
+ 🕹 Use `ctrl-c` to exit.
28
+
29
+
30
+
31
+ PROMPT
32
+
33
+ # Using `sleep infinity` instead of `tail -f /dev/null`. This may be a
34
+ # performance improvement based on the conversation on a semi-related
35
+ # StackOverflow page.
36
+ #
37
+ # @see https://stackoverflow.com/a/41655546
38
+ sleep infinity
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
6
+
7
+ . "${SCRIPT_DIR}/dx.sh.lib"
8
+ require_command "docker"
9
+ load_docker_compose_env
10
+
11
+ usage_on_help "Starts all services, including a container in which to run your app" "" "" "" "${@}"
12
+
13
+ log "🚀" "Starting docker-compose.dx.yml"
14
+
15
+ BUILD=--build
16
+ if [ "${1}" == "--no-build" ]; then
17
+ BUILD=
18
+ fi
19
+
20
+ docker \
21
+ compose \
22
+ --file docker-compose.dx.yml \
23
+ --project-name "${PROJECT_NAME}" \
24
+ --env-file "${ENV_FILE}" \
25
+ up \
26
+ "${BUILD}" \
27
+ --timestamps \
28
+ --force-recreate
29
+
30
+ # vim: ft=bash
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
6
+
7
+ . "${SCRIPT_DIR}/dx.sh.lib"
8
+ require_command "docker"
9
+ load_docker_compose_env
10
+
11
+ usage_on_help "Stops all services, the container in which to run your app and removes any volumes" "" "" "" "${@}"
12
+
13
+ log "🚀" "Stopping docker-compose.dx.yml"
14
+
15
+ docker \
16
+ compose \
17
+ --file docker-compose.dx.yml \
18
+ --project-name "${PROJECT_NAME}" \
19
+ --env-file "${ENV_FILE}" \
20
+ down \
21
+ --volumes
22
+
23
+ # vim: ft=bash
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "<%= app_name %>",
3
+ "type": "module",
4
+ "license": "UNLICENSED",
5
+ "dependencies": {
6
+ "brut-css": "<%= versions.brut_css_version_specifier %>",
7
+ "brut-js": "<%= versions.brut_js_version_specifier %>"
8
+ },
9
+ "devDependencies": {
10
+ "chokidar-cli": "^3.0.0",
11
+ "esbuild": "^0.20.2",
12
+ "jsdom": "^25.0.1",
13
+ "mocha": "^10.7.3",
14
+ "playwright": "1.50.1",
15
+ "typescript": "^5.8.3",
16
+ "typescript-language-server": "^4.3.4",
17
+ "vscode-langservers-extracted": "^4.10.0"
18
+ },
19
+ "notes": {
20
+ "note": "This section provides documentation on what the various information in here is for",
21
+ "type": "Needs to be 'module' so that the web components unit tests work properly",
22
+ "dependencies": {
23
+ "brut-css": "Basic CSS library to get you started. See Brut's docs for how to use or remove",
24
+ "brut-js": "Autonomous custom elements and utilities. See Brut's docs for how to use and why you should not remove"
25
+ },
26
+ "devDependencies": {
27
+ "chokidar-cli": "Used to watch files and and rebuild stuff on changes",
28
+ "esbuild": "Bundles JS and CSS - you may need this in dependencies depending on how you deploy",
29
+ "jsdom": "Used for unit testing custom elements",
30
+ "mocha": "Used for unit testing custom elements",
31
+ "playwright": "Used for end-to-end testing. This version is locked and must be the same as the one in the Gemfile for playwright-ruby-client.",
32
+ "typescript": "Needed for the JS and CSS LSP servers",
33
+ "typescript-language-server": "Provides a JS LSP server",
34
+ "vscode-langservers-extracted": "Despite the name, provides a CSS LSP server"
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,53 @@
1
+ # Mostly based on Heroku's guidance:
2
+ # https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server
3
+
4
+ # Ensures the logger is setup and availbale as early as possible
5
+ require "semantic_logger"
6
+
7
+ # workers should be set to the # of cores. Heroku
8
+ # recommends the following, with 2 as a default if you don't have a good
9
+ # reason to change it.
10
+ workers Integer(ENV["WEB_CONCURRENCY"] || 2)
11
+
12
+ # Heroku recommens 5 as a default, with the ability
13
+ # to change it as needed.
14
+ threads_count = Integer(ENV["PUMA_MAX_THREADS"] || 5)
15
+
16
+ # This actually sets the min and max # of threads.
17
+ # Heroku recommends setting them both to the thread_count
18
+ # above.
19
+ threads threads_count, threads_count
20
+
21
+ # Indicate that the app should be loaded before any
22
+ # forking or thread creation. Despite the bang,
23
+ # this doesn't actually do anything - it just sets configuration
24
+ preload_app!
25
+
26
+ # Support IPv6 by binding to host `::` instead of `0.0.0.0`
27
+ port(ENV["PORT"] || 3000, "::")
28
+
29
+ # Turn off keepalive support for better long tails response time with Router 2.0
30
+ # Remove this line when https://github.com/puma/puma/issues/3487 is closed, and the fix is released
31
+ if respond_to?(:enable_keep_alives)
32
+ enable_keep_alives(false)
33
+ end
34
+
35
+ # Commenting this out as a) we don't default DefaultRackup
36
+ # and b) I don't exactly know what it means or does.
37
+ # rackup DefaultRackup if defined?(DefaultRackup)
38
+
39
+ # Set the environment based on RACK_ENV
40
+ environment ENV["RACK_ENV"]
41
+
42
+ before_fork do
43
+ # Per http://sequel.jeremyevans.net/rdoc/files/doc/fork_safety_rdoc.html
44
+ # we must disconnect before forking. Sequel will reconnect as needed.
45
+ Sequel::DATABASES.each(&:disconnect)
46
+ end
47
+
48
+ on_worker_boot do
49
+ # Per https://logger.rocketjob.io/forking.html we want to reopen
50
+ # this to avoid issues
51
+ SemanticLogger.reopen
52
+ end
53
+
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe "The home page works" do
4
+ it "shows a welcome message" do
5
+ # Instead of hard-coding URLs, you can use the
6
+ # page classes to navigate to their routing.
7
+ page.goto(HomePage.routing)
8
+
9
+ # The Brut::FrontEnd::Components::PageIdentifier component
10
+ # renders a <meta> tag that Brut's `be_page_for` will look for.
11
+ # This is a useful check that you ended up on the page you meant to.
12
+ expect(page).to be_page_for(HomePage)
13
+
14
+ # In Playwright, elements are "located" asynchronously. What is returned
15
+ # here is an object that will look for an <h1> only when an expectation
16
+ # is made on it.
17
+ h1 = page.locator("h1")
18
+
19
+ # This will actually locate the <h1> (waiting for some time for it to
20
+ # show up if it's not there), and then assert its text content.
21
+ expect(h1).to have_text("Welcome to Brut")
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ import { createTestBasedOnHTML } from "brut-js/testing"
2
+
3
+ import path from "node:path"
4
+ import fs from "node:fs"
5
+
6
+ const __dirname = import.meta.dirname
7
+
8
+ const appRoot = path.resolve(__dirname,"..","..","..","app")
9
+ const publicRoot = path.resolve(appRoot,"public")
10
+ const assetMetadataFilePath = path.resolve(appRoot,"config","asset_metadata.json")
11
+ const assetMetadata = JSON.parse(fs.readFileSync(assetMetadataFilePath))
12
+
13
+ const withHTML = (html) => {
14
+ return createTestBasedOnHTML({
15
+ html,
16
+ assetMetadata,
17
+ publicRoot
18
+ })
19
+ }
20
+
21
+ export {
22
+ withHTML,
23
+ }
24
+
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe HomePage do
4
+ it "should show the H1" do
5
+ # Page specs should evaluate the generated HTML, so
6
+ # the generate_and_parse help will accept an instaniated
7
+ # page (or components), generate its HTML, then use Nokogiri
8
+ # to parse it, return the result.
9
+ result = generate_and_parse(described_class.new)
10
+
11
+ # e! is provided by Brut::SpecSupport::EnhancedNode which
12
+ # delegates everything to the underlying Nokogiri::XML::Node
13
+ # while adding a few methods. e! requires that exactly
14
+ # one element match the given CSS selector, then returns it.
15
+ #
16
+ # Thus, this expectation will fail if:
17
+ # * there is no <h1> (and the error message would indcate this)
18
+ # * there is is more than one <h1> (and the error message would indcate this)
19
+ # * the only <h1>'s text is not exactly "Welcome to Brut"
20
+ expect(result.e!("h1").text).to eq("Welcome to Brut")
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ require "spec_helper"
2
+ RSpec.describe "factories" do
3
+ it "should be possible to create them all" do
4
+ FactoryBot.lint traits: true
5
+ end
6
+ end
7
+
@@ -0,0 +1,78 @@
1
+ ENV["RACK_ENV"] = "test"
2
+ require_relative "../app/bootstrap"
3
+ Bootstrap.new.bootstrap!
4
+
5
+ require "brut/spec_support"
6
+
7
+ require "nokogiri"
8
+ require "playwright"
9
+ require "playwright/test"
10
+ require "confidence_check/for_rspec"
11
+ require "with_clues"
12
+
13
+ require_relative "support"
14
+
15
+ RSpec.configure do |config|
16
+ # This configuration has two main parts.
17
+ #
18
+ # The first part is here: a call to Brut::SpecSupport::RSpecSetup,
19
+ # which will set configuration values required for Brut's spec
20
+ # helpers and other APIs. In generally, you do not want to change
21
+ # this configuration.
22
+ rspec_setup = Brut::SpecSupport::RSpecSetup.new(rspec_config: config)
23
+ rspec_setup.setup!
24
+
25
+ # The second part is here and is RSpec configuration that you may want
26
+ # to change. Changing the configuration below here should not break
27
+ # any of Brut's internal behavior around tests. The values
28
+ # and configuration options set are a recommended default, but you
29
+ # can certainly change it to suit your needs.
30
+
31
+
32
+ # Confidence Check allows you to wrap test expectations
33
+ # with confidence_check as a way to indicate those expectations are
34
+ # checking that the test is setup properly and not testing
35
+ # the behavior of the code under test
36
+ config.include ConfidenceCheck::ForRSpec
37
+
38
+ # With Clues allows you to wrap expectations in
39
+ # a with_clues block that will provide more details
40
+ # about the failure and make it easier to diagnose
41
+ # why a test failed.
42
+ config.include WithClues::Method
43
+ config.include FactoryBot::Syntax::Methods
44
+
45
+ config.expect_with :rspec do |expectations|
46
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
47
+ end
48
+
49
+ config.mock_with :rspec do |mocks|
50
+ mocks.verify_partial_doubles = true
51
+ end
52
+
53
+ # Not that you should be using shared contexts, but if you are, please
54
+ # see https://rubydoc.info/gems/rspec-core/3.13.5/RSpec/Core/Configuration#shared_context_metadata_behavior-instance_method
55
+ config.shared_context_metadata_behavior = :apply_to_host_groups
56
+
57
+ config.filter_run_when_matching :focus
58
+
59
+ # Can't find docs on how this path is resolved, so disabling
60
+ # config.example_status_persistence_file_path = "spec/examples.txt"
61
+
62
+ config.disable_monkey_patching!
63
+
64
+ config.warnings = ENV.fetch("RSPEC_WARNINGS","false") == "true"
65
+
66
+ if config.files_to_run.one?
67
+ config.default_formatter = "doc"
68
+ end
69
+
70
+ if ENV["RSPEC_PROFILE_EXAMPLES"]
71
+ config.profile_examples = ENV["RSPEC_PROFILE_EXAMPLES"].to_i
72
+ end
73
+
74
+ config.order = :random
75
+
76
+ Kernel.srand config.seed
77
+ end
78
+
@@ -0,0 +1,2 @@
1
+ module Support
2
+ end
@@ -0,0 +1,24 @@
1
+ # Allows manually triggering an exception so you can see
2
+ # the exception handling mechanism execute without waiting
3
+ # for a real exception to happen.
4
+ class TriggerExceptionHandler < AppHandler
5
+ # These arguments are query string parameters that
6
+ # can be given to the URL connected to this handler.
7
+ # See https://brutrb.com/keyword-injection.html for more details.
8
+ def initialize(message: "no message provided", status: nil, key: nil)
9
+ @message = message
10
+ @status = status
11
+ @key = key
12
+ end
13
+
14
+ def handle
15
+ if @key != Brut.container.trigger_exception_key
16
+ http_status(404)
17
+ elsif @status
18
+ http_status(@status)
19
+ else
20
+ raise @message
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,49 @@
1
+ import { BaseCustomElement } from "brut-js"
2
+
3
+ // This is very similar to a vanilla on-spec
4
+ // implementation of an autonomous custom element.
5
+ // The only difference is BaseCustomElement (which
6
+ // extends HTMLElement) provides a few convenience
7
+ // methods to make it easier to build your own custom
8
+ // elements.
9
+ class Example extends BaseCustomElement {
10
+ // tagName allows BaseCustomElement's define() method to
11
+ // define your custom element
12
+ static tagName = "<%= prefix %>-example"
13
+
14
+ static observedAttributes = [
15
+ "transform",
16
+ "show-warnings", // recognized by BaseCustomElement to allow
17
+ // for debugguing in the browser console,
18
+ // but clean consoles in production.
19
+ ]
20
+
21
+ #transform = "upper"
22
+
23
+ // Called by BaseCustomElement's attributeChangedCallback.
24
+ transformChangedCallback({newValue}) {
25
+ this.#transform = newValue
26
+ }
27
+
28
+ // Called by connectCallback and attributeChangedCallback, this
29
+ // method should make whatever changes are necessary based on the current
30
+ // state of the element.
31
+ update() {
32
+ const content = this.textContent
33
+ if (this.#transform == "upper") {
34
+ this.textContent = content.toLocaleUpperCase()
35
+ }
36
+ else if (this.#transform == "lower") {
37
+ this.textContent = content.toLocaleLowerCase()
38
+ }
39
+ else {
40
+ // Example of debugging. if show-warnings is not set, this message
41
+ // is not shown in the console. If show-warnings IS set (including
42
+ // when you override it in production using the browser's devtools)
43
+ // this message WILL be shown
44
+ this.logger.info("We only support upper or lower, but got %s",this.#transform)
45
+ }
46
+ }
47
+ }
48
+
49
+ export default Example
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe TriggerExceptionHandler do
4
+ describe "#handle!" do
5
+ context "key is correct" do
6
+ context "http status given" do
7
+ it "returns that status" do
8
+ handler = described_class.new(status: 401, key: "test-trigger-exception")
9
+ result = handler.handle!
10
+ expect(result).to have_returned_http_status(401)
11
+ end
12
+ end
13
+ context "no http status given" do
14
+ context "message provided" do
15
+ it "raises with that message" do
16
+ handler = described_class.new(message: "test message", key: "test-trigger-exception")
17
+ expect {
18
+ handler.handle!
19
+ }.to raise_error("test message")
20
+ end
21
+ end
22
+ context "no message provided" do
23
+ it "raises with a default message" do
24
+ handler = described_class.new(key: "test-trigger-exception")
25
+ expect {
26
+ handler.handle!
27
+ }.to raise_error("no message provided")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ context "key is incorrect" do
33
+ it "returns 404" do
34
+ handler = described_class.new(status: 401, message: "test message")
35
+ result = handler.handle!
36
+ expect(result).to have_returned_http_status(404)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,38 @@
1
+ import { withHTML } from "./SpecHelper.js"
2
+
3
+ // Autonomouse custom element support is very basic.
4
+ // In this spec, we use `withHTML` which was defined in SpecHelper.js,
5
+ // which ultimately uses `createTestBasedOnHTML` from BrutJS.
6
+ // This will configure JSDom with the HTML you provide, then execute
7
+ // your test as if that HTML is the document.
8
+ describe("<<%= prefix %>-example>", () => {
9
+
10
+ // The example custom element will transform its contents to
11
+ // either upper or lower case. In this test, we assert that, by
12
+ // default, it transforms to upper case. To do that,
13
+ // create an HTML document you can see below. The element
14
+ // will have been connected (connectedCallback will have been
15
+ // called) by the time the test executes. Thus,
16
+ // document.querySelector(...) will find the element and its
17
+ // .textContent will have already been transformed.
18
+ withHTML(`
19
+ <<%= prefix %>-example>This is some Text</<%= prefix %>-example>
20
+ `).test("upper case by default", ({document,assert}) => {
21
+ const element = document.querySelector("<%= prefix %>-example")
22
+ assert.equal(element.textContent,"THIS IS SOME TEXT")
23
+ })
24
+
25
+ withHTML(`
26
+ <<%= prefix %>-example transform="lower">This is some Text</<%= prefix %>-example>
27
+ `).test("lower case when asked", ({document,assert}) => {
28
+ const element = document.querySelector("<%= prefix %>-example")
29
+ assert.equal(element.textContent,"this is some text")
30
+ // Remember that setAttribute is synchronous, so by the time
31
+ // its done executing, attributeChangedCallback will have been called,
32
+ // and thus the element's `update()` method will have been called and
33
+ // its textContent transformed.
34
+ element.setAttribute("transform","upper")
35
+ assert.equal(element.textContent,"THIS IS SOME TEXT")
36
+ })
37
+ })
38
+
@@ -0,0 +1,3 @@
1
+ class DB::GuestbookMessage < AppDataModel
2
+ has_external_id "gm"
3
+ end
@@ -0,0 +1,14 @@
1
+ Sequel.migration do
2
+ up do
3
+ create_table :guestbook_messages,
4
+ comment: "Messages people have left in the guestbook",
5
+ external_id: true do
6
+
7
+ column :name, :text
8
+ column :message, :text
9
+ column :ip_address, :inet
10
+
11
+ key [ :ip_address ]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ class FlashComponent < AppComponent
2
+ def initialize(flash:)
3
+ @flash = flash
4
+ end
5
+
6
+ CLASSES = [
7
+ "f-1",
8
+ "fw-bold",
9
+ "w-50",
10
+ "mv-3",
11
+ "shadow-3",
12
+ "pa-3",
13
+ "ba",
14
+ "br-3",
15
+ "tc"
16
+ ]
17
+ COLORS = [
18
+ "%{color}-300",
19
+ "bg-%{color}-800",
20
+ "bc-%{color}-700",
21
+ ]
22
+
23
+ def view_template
24
+ if @flash.notice?
25
+ div(role: "status",
26
+ class: CLASSES + COLORS.map { it % { color: "blue" } }) do
27
+ t(@flash.notice)
28
+ end
29
+ elsif @flash.alert?
30
+ div(role: "alert",
31
+ class: CLASSES + COLORS.map { it % { color: "orange" } }) do
32
+ t(@flash.alert)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ /* Hide <brut-cv> tags so that we don't see
2
+ * client-side errors before the form is submitted */
3
+ brut-cv {
4
+ display: none;
5
+ color: var(--red-400);
6
+ }
7
+
8
+ /* Server-side errors are always visible */
9
+ brut-cv[server-side] {
10
+ display: block;
11
+ }
12
+
13
+ /* Only when a form submission is attempted do
14
+ * we show the <brut-cv>s that are not server-side,
15
+ * and thus client-side */
16
+ brut-form[submitted-invalid] brut-cv {
17
+ display: block;
18
+ }
@@ -0,0 +1,19 @@
1
+ /* Example of font-management. */
2
+ @font-face {
3
+ font-family: "Monaspace Xenon";
4
+ /** url is relative to this file. When bin/build-assets runs,
5
+ * esbuild will see this and modify it to work with the bundle
6
+ * that is ultimately produced */
7
+ src: url("../fonts/monaspace-xenon.ttf") format("truetype");
8
+ font-display: swap;
9
+ }
10
+
11
+ :root {
12
+ /* Sets the mono font for BrutCSS */
13
+ --ff-mono: 'Monaspace Xenon', monospace;
14
+ }
15
+
16
+ code {
17
+ /* Uses the mono font for <code> elements */
18
+ font-family: var(--ff-mono);
19
+ }
@@ -0,0 +1,4 @@
1
+ class GuestbookMessageForm < AppForm
2
+ input :name, minlength: 2
3
+ input :message, minlength: 10
4
+ end
@@ -0,0 +1,64 @@
1
+ class GuestbookMessageHandler < AppHandler
2
+ def initialize(form:, flash:, rack_request_ip:, session:)
3
+ @form = form
4
+ @flash = flash
5
+ @rack_request_ip = rack_request_ip
6
+ @session = session
7
+ end
8
+ def handle
9
+ # If client-side constraint violation checking was skipped,
10
+ # but there ARE such violations, the form will be able to check
11
+ # that server-side and return true for #constraint_violations?
12
+ if @form.valid?
13
+ save_message
14
+ end
15
+
16
+ if @form.constraint_violations?
17
+ @flash.alert = :guestbook_not_saved
18
+ return NewGuestbookMessagePage.new(form: @form,
19
+ session: @session)
20
+ else
21
+ @flash.notice = :guestbook_saved
22
+ redirect_to(
23
+ GuestbookPage,
24
+ anchor: @session.guestbook_message&.external_id
25
+ )
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def save_message
32
+ # While we don't recommend business logic be placed
33
+ # inside handlers, we're leaving it here to avoid
34
+ # anchoring you on any particular way of managing
35
+ # the logic in your app.
36
+ if @form.message.split(/\s+/).length < 2
37
+ @form.server_side_constraint_violation(
38
+ input_name: "message",
39
+ key: :not_enough_words
40
+ )
41
+ else
42
+ if DB::GuestbookMessage.find(ip_address: @rack_request_ip)
43
+ @form.server_side_constraint_violation(
44
+ input_name: "name",
45
+ key: :already_posted
46
+ )
47
+ else
48
+ begin
49
+ guestbook_message = DB::GuestbookMessage.create(
50
+ name: @form.name,
51
+ message: @form.message,
52
+ ip_address: @rack_request_ip,
53
+ )
54
+ @session.signed_guestbook(guestbook_message)
55
+ rescue Sequel::UniqueConstraintViolation => ex
56
+ @form.server_side_constraint_violation(
57
+ input_name: "name",
58
+ key: :already_posted
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end