mkbrut 0.1.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 (128) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +51 -0
  3. data/exe/mkbrut +5 -0
  4. data/lib/mkbrut/app.rb +67 -0
  5. data/lib/mkbrut/app_id.rb +8 -0
  6. data/lib/mkbrut/app_name.rb +29 -0
  7. data/lib/mkbrut/app_options.rb +29 -0
  8. data/lib/mkbrut/base.rb +57 -0
  9. data/lib/mkbrut/cli.rb +91 -0
  10. data/lib/mkbrut/erb_binding_delegate.rb +20 -0
  11. data/lib/mkbrut/internet_identifier.rb +32 -0
  12. data/lib/mkbrut/invalid_identifier.rb +4 -0
  13. data/lib/mkbrut/ops/add_css_import.rb +42 -0
  14. data/lib/mkbrut/ops/add_i18n_message.rb +74 -0
  15. data/lib/mkbrut/ops/add_method.rb +48 -0
  16. data/lib/mkbrut/ops/append_to_file.rb +17 -0
  17. data/lib/mkbrut/ops/base_op.rb +21 -0
  18. data/lib/mkbrut/ops/copy_file.rb +12 -0
  19. data/lib/mkbrut/ops/insert_code_in_method.rb +58 -0
  20. data/lib/mkbrut/ops/insert_route.rb +52 -0
  21. data/lib/mkbrut/ops/mkdir.rb +13 -0
  22. data/lib/mkbrut/ops/prism_parsing_op.rb +70 -0
  23. data/lib/mkbrut/ops/render_template.rb +26 -0
  24. data/lib/mkbrut/ops/skip_file.rb +10 -0
  25. data/lib/mkbrut/ops.rb +16 -0
  26. data/lib/mkbrut/organization.rb +5 -0
  27. data/lib/mkbrut/prefix.rb +26 -0
  28. data/lib/mkbrut/prefixed_io.rb +16 -0
  29. data/lib/mkbrut/segments/bare_bones.rb +184 -0
  30. data/lib/mkbrut/segments/demo.rb +117 -0
  31. data/lib/mkbrut/segments/sidekiq.rb +3 -0
  32. data/lib/mkbrut/segments.rb +7 -0
  33. data/lib/mkbrut/version.rb +3 -0
  34. data/lib/mkbrut/versions.rb +16 -0
  35. data/lib/mkbrut.rb +17 -0
  36. data/templates/Base/Dockerfile.dx +205 -0
  37. data/templates/Base/Gemfile.erb +53 -0
  38. data/templates/Base/Procfile.development +4 -0
  39. data/templates/Base/Procfile.test +1 -0
  40. data/templates/Base/README.md +4 -0
  41. data/templates/Base/README.md.erb +40 -0
  42. data/templates/Base/app/bootstrap.rb +61 -0
  43. data/templates/Base/app/config/i18n/en/1_defaults.rb +128 -0
  44. data/templates/Base/app/config/i18n/en/2_app.rb +24 -0
  45. data/templates/Base/app/public/static/manifest.json.erb +33 -0
  46. data/templates/Base/app/src/app.rb.erb +37 -0
  47. data/templates/Base/app/src/back_end/data_models/app_data_model.rb +5 -0
  48. data/templates/Base/app/src/back_end/data_models/db.rb +19 -0
  49. data/templates/Base/app/src/back_end/data_models/seed/seed_data.rb +9 -0
  50. data/templates/Base/app/src/front_end/components/app_component.rb +8 -0
  51. data/templates/Base/app/src/front_end/components/custom_element_registration.rb.erb +7 -0
  52. data/templates/Base/app/src/front_end/css/fonts.css +19 -0
  53. data/templates/Base/app/src/front_end/css/index.css +3 -0
  54. data/templates/Base/app/src/front_end/css/svgs.css +12 -0
  55. data/templates/Base/app/src/front_end/fonts/monaspace-xenon.ttf +0 -0
  56. data/templates/Base/app/src/front_end/forms/app_form.rb +4 -0
  57. data/templates/Base/app/src/front_end/handlers/app_handler.rb +4 -0
  58. data/templates/Base/app/src/front_end/images/apple-touch-icon-120x120.png +0 -0
  59. data/templates/Base/app/src/front_end/images/apple-touch-icon-152x152.png +0 -0
  60. data/templates/Base/app/src/front_end/images/apple-touch-icon-167x167.png +0 -0
  61. data/templates/Base/app/src/front_end/images/apple-touch-icon-180x180.png +0 -0
  62. data/templates/Base/app/src/front_end/images/favicon.ico +0 -0
  63. data/templates/Base/app/src/front_end/images/icon.png +0 -0
  64. data/templates/Base/app/src/front_end/images/mkicons.sh +6 -0
  65. data/templates/Base/app/src/front_end/js/index.js +6 -0
  66. data/templates/Base/app/src/front_end/layouts/default_layout.rb.erb +76 -0
  67. data/templates/Base/app/src/front_end/pages/app_page.rb +11 -0
  68. data/templates/Base/app/src/front_end/pages/home_page.rb.erb +54 -0
  69. data/templates/Base/app/src/front_end/support/app_session.rb +6 -0
  70. data/templates/Base/app/src/front_end/svgs/README.md +5 -0
  71. data/templates/Base/app/src/front_end/svgs/comment-button.svg +59 -0
  72. data/templates/Base/bin/README.md.erb +5 -0
  73. data/templates/Base/bin/build-assets +7 -0
  74. data/templates/Base/bin/ci +39 -0
  75. data/templates/Base/bin/console +31 -0
  76. data/templates/Base/bin/db +9 -0
  77. data/templates/Base/bin/dbconsole +51 -0
  78. data/templates/Base/bin/dev +25 -0
  79. data/templates/Base/bin/release +26 -0
  80. data/templates/Base/bin/run +86 -0
  81. data/templates/Base/bin/scaffold +9 -0
  82. data/templates/Base/bin/setup +256 -0
  83. data/templates/Base/bin/test +9 -0
  84. data/templates/Base/bin/test-server +29 -0
  85. data/templates/Base/bin/watch-and-build-assets +37 -0
  86. data/templates/Base/config.ru +16 -0
  87. data/templates/Base/docker-compose.dx.yml +85 -0
  88. data/templates/Base/dx/README.md +28 -0
  89. data/templates/Base/dx/bash_customizations +12 -0
  90. data/templates/Base/dx/bash_customizations.local +4 -0
  91. data/templates/Base/dx/build +101 -0
  92. data/templates/Base/dx/docker-compose.env.erb +25 -0
  93. data/templates/Base/dx/dx.sh.lib +137 -0
  94. data/templates/Base/dx/exec +56 -0
  95. data/templates/Base/dx/prune +19 -0
  96. data/templates/Base/dx/show-help-in-app-container-then-wait.sh +38 -0
  97. data/templates/Base/dx/start +30 -0
  98. data/templates/Base/dx/stop +23 -0
  99. data/templates/Base/package.json.erb +37 -0
  100. data/templates/Base/puma.config.rb +53 -0
  101. data/templates/Base/specs/e2e/home_page.spec.rb.erb +23 -0
  102. data/templates/Base/specs/front_end/js/SpecHelper.js +24 -0
  103. data/templates/Base/specs/front_end/pages/home_page.spec.rb +22 -0
  104. data/templates/Base/specs/lint_factories.spec.rb +7 -0
  105. data/templates/Base/specs/spec_helper.rb +78 -0
  106. data/templates/Base/specs/support.rb +2 -0
  107. data/templates/segments/BareBones/app/src/front_end/handlers/trigger_exception_handler.rb +24 -0
  108. data/templates/segments/BareBones/app/src/front_end/js/Example.js.erb +49 -0
  109. data/templates/segments/BareBones/specs/front_end/handlers/trigger_exception_handler.spec.rb +41 -0
  110. data/templates/segments/BareBones/specs/front_end/js/Example.spec.js.erb +38 -0
  111. data/templates/segments/Demo/app/src/back_end/data_models/db/guestbook_message.rb +3 -0
  112. data/templates/segments/Demo/app/src/back_end/data_models/migrations/20250628194124_guestbook.rb +15 -0
  113. data/templates/segments/Demo/app/src/front_end/components/flash_component.rb +36 -0
  114. data/templates/segments/Demo/app/src/front_end/css/constraint-violations.css +18 -0
  115. data/templates/segments/Demo/app/src/front_end/forms/guestbook_message_form.rb +4 -0
  116. data/templates/segments/Demo/app/src/front_end/handlers/guestbook_message_handler.rb +64 -0
  117. data/templates/segments/Demo/app/src/front_end/pages/guestbook_page/message_component.rb +41 -0
  118. data/templates/segments/Demo/app/src/front_end/pages/guestbook_page.rb +43 -0
  119. data/templates/segments/Demo/app/src/front_end/pages/new_guestbook_message_page.rb +64 -0
  120. data/templates/segments/Demo/specs/back_end/data_models/db/guestbook_message.spec.rb +5 -0
  121. data/templates/segments/Demo/specs/e2e/guest_message.spec.rb +54 -0
  122. data/templates/segments/Demo/specs/factories/db/guestbook_message.rb +7 -0
  123. data/templates/segments/Demo/specs/front_end/components/flash_component.spec.rb +5 -0
  124. data/templates/segments/Demo/specs/front_end/handlers/guestbook_message_handler.spec.rb +122 -0
  125. data/templates/segments/Demo/specs/front_end/pages/guestbook_page/message_component.spec.rb +5 -0
  126. data/templates/segments/Demo/specs/front_end/pages/guestbook_page.spec.rb +52 -0
  127. data/templates/segments/Demo/specs/front_end/pages/new_guestbook_message_page.spec.rb +5 -0
  128. metadata +195 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8d93e08e9f662468dd47a79c11c2afa36144905bfdf52e8fe54d209f99940557
4
+ data.tar.gz: 5911070cfefab0d42f46d19c5676bdce3f6bc0c63d386adab09d1995778f274c
5
+ SHA512:
6
+ metadata.gz: efae7e39b7ebe46371308cd83098d26a0ac12c637e76988b37ccc43c4271b58d74a1575f52ee451e821a3b410b2dad268807bafac5d7797aa99d6a4c63705e88
7
+ data.tar.gz: 8ec79d1512d8552e72e86534f72cd9750c3fc236a3224a98b195b062f1fffa90ad56eaafd5f2d0b3a6aa877296dfe6674a212fb76bbd125a43ab1d475e81df1d
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # mkbrut - Create a new Brut App
2
+
3
+ `mkbrut` is how you go from zero to having a Brut app where you can start working.
4
+
5
+ ## Installation
6
+
7
+ TBD
8
+
9
+ ## Usage
10
+
11
+ ```
12
+ mkbrut my-new-app
13
+ ```
14
+
15
+ This will create a new Brut app, including a development environment. The app will
16
+ have some demo features to show you around the framework.
17
+
18
+ To get a bare bones new app, use `--no-demo`:
19
+
20
+ ```
21
+ mkbrut my-new-app --no-demo
22
+ ```
23
+
24
+ You can also customize some aspects of your app once start to have an opinion about
25
+ it:
26
+
27
+
28
+ ```
29
+ mkbrut --app-id=new-app \
30
+ --organization=cyberdyne \
31
+ --prefix=ap \
32
+ my-new-app
33
+ ```
34
+
35
+ * `--app-id` The identifier for your app, suitable for use as a hostname or other internet-safe identifier.
36
+ * `--organization` This is your organization name you might use on GitHub, DockerHub, or WhateverHub.
37
+ * `--prefix` The two-character prefix for all external IDs of your database tables that opt into external IDs as well as any autonomous custom elements you might make
38
+
39
+ ## Developing
40
+
41
+ `mkbrut` has a Docker-based dev environment:
42
+
43
+ 1. Install Docker
44
+ 2. `dx/build`
45
+ 3. `dx/start`
46
+ 4. Open a new terminal:
47
+ 1. `dx/exec bash`
48
+ 2. You are now inside a running Docker container:
49
+ 1. `bin/setup`
50
+ 2. `bundle exec exe/mkbrut -h`
51
+
data/exe/mkbrut ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "mkbrut"
4
+
5
+ exit MKBrut::CLI.new(args: ARGV).run
data/lib/mkbrut/app.rb ADDED
@@ -0,0 +1,67 @@
1
+ require "fileutils"
2
+ require "erb"
3
+ require "ostruct"
4
+
5
+ module MKBrut
6
+ class App
7
+ def initialize(current_dir:, app_options:, out:, err:)
8
+ @out = out
9
+ @app_options = app_options
10
+
11
+ @out.puts "Creating app with these options:\n"
12
+ @out.puts "App name: #{app_options.app_name}"
13
+ @out.puts "App ID: #{app_options.app_id}"
14
+ @out.puts "Prefix: #{app_options.prefix}"
15
+ @out.puts "Organization: #{app_options.organization}"
16
+ @out.puts "Include demo? #{app_options.demo}\n"
17
+
18
+ if app_options.dry_run?
19
+ @out.puts "Dry Run"
20
+ MKBrut::Ops::BaseOp.dry_run = true
21
+ end
22
+
23
+ templates_dir = Pathname(
24
+ Gem::Specification.find_by_name("mkbrut").gem_dir
25
+ ) / "templates"
26
+
27
+ @base = MKBrut::Base.new(
28
+ app_options:,
29
+ current_dir:,
30
+ templates_dir:
31
+ )
32
+ @segments = [
33
+ MKBrut::Segments::BareBones.new(
34
+ app_options:,
35
+ current_dir:,
36
+ templates_dir:,
37
+ )
38
+ ]
39
+ if app_options.demo?
40
+ @segments << MKBrut::Segments::Demo.new(
41
+ app_options:,
42
+ current_dir:,
43
+ templates_dir:
44
+ )
45
+ end
46
+ end
47
+
48
+ def create!
49
+ @out.puts "Creating Base app"
50
+ @base.create!
51
+ @segments.each do |segment|
52
+ @out.puts "Creating segment: #{segment.class.friendly_name}"
53
+ segment.add!
54
+ end
55
+ @out.puts "#{@app_options.app_name} was created\n\n"
56
+ @out.puts "Time to get building:"
57
+ @out.puts "1. cd #{@app_options.app_name}"
58
+ @out.puts "2. dx/build"
59
+ @out.puts "3. dx/start"
60
+ @out.puts "4. [ in another terminal ] dx/exec bash"
61
+ @out.puts "5. [ inside the Docker container ] bin/setup"
62
+ @out.puts "6. [ inside the Docker container ] bin/dev"
63
+ @out.puts "7. Visit http://localhost:6502 in your browser"
64
+ @out.puts "8. [ inside the Docker container ] bin/setup help # to see more commands"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,8 @@
1
+ class MKBrut::AppId < MKBrut::InternetIdentifier
2
+ def self.from_app_name(app_name)
3
+ self.new(app_name.to_s.gsub(/[^a-zA-Z0-9\-]/, "-").downcase)
4
+ end
5
+ def initialize(value)
6
+ super(:app_id, value)
7
+ end
8
+ end
@@ -0,0 +1,29 @@
1
+ module MKBrut
2
+ class AppName
3
+ def initialize(value)
4
+ identifier = value.to_s
5
+ if identifier.empty?
6
+ raise MKBrut::InvalidIdentifier, "app-name is required"
7
+ end
8
+
9
+ if identifier.length > 63
10
+ raise MKBrut::InvalidIdentifier, "app-name cannot be longer than 63 characters"
11
+ end
12
+
13
+ if identifier.start_with?("-") || identifier.end_with?("-")
14
+ raise MKBrut::InvalidIdentifier, "app-name cannot start or end with a hyphen"
15
+ end
16
+
17
+ if identifier.match?(/[^a-zA-Z\-_]/)
18
+ raise MKBrut::InvalidIdentifier, "app-name can only contain letters, hyphens, and underscores"
19
+ end
20
+ if identifier.match?(/__/) || identifier.match?(/--/)
21
+ raise MKBrut::InvalidIdentifier, "app-name can not have repeating underscores or hyphens"
22
+ end
23
+ @identifier = identifier.to_s.gsub(/_/,"-")
24
+ end
25
+
26
+ def to_s = @identifier
27
+ alias to_str to_s
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ class MKBrut::AppOptions
2
+ attr_reader :app_name, :app_id, :prefix, :organization, :demo, :versions
3
+
4
+ def initialize(
5
+ app_name:,
6
+ app_id: nil,
7
+ prefix: nil,
8
+ dry_run: nil,
9
+ organization: nil,
10
+ demo: false,
11
+ versions: nil,
12
+ **ignore
13
+ )
14
+ if app_name.nil?
15
+ raise ArgumentError, "app_name is required"
16
+ end
17
+
18
+ @app_name = app_name
19
+ @app_id = app_id || MKBrut::AppId.from_app_name(@app_name)
20
+ @prefix = prefix || MKBrut::Prefix.from_app_id(@app_id)
21
+ @organization = organization || @app_id
22
+ @dry_run = !!dry_run
23
+ @demo = !!demo
24
+ @versions = versions
25
+ end
26
+
27
+ def dry_run? = @dry_run
28
+ def demo? = @demo
29
+ end
@@ -0,0 +1,57 @@
1
+ require "pathname"
2
+ require "securerandom"
3
+ # Constructs the base of any Brut app.
4
+ class MKBrut::Base
5
+ include MKBrut
6
+
7
+ class ErbBinding < MKBrut::ErbBindingDelegate
8
+ def session_secret = SecureRandom.hex(64)
9
+ end
10
+
11
+ def initialize(app_options:, current_dir:, templates_dir:)
12
+ @project_root = current_dir / app_options.app_name
13
+ @templates_dir = templates_dir / "Base"
14
+ @erb_binding = ErbBinding.new(app_options)
15
+ end
16
+
17
+ def create!
18
+ if @project_root.exist?
19
+ raise "Project root #{@project_root} already exists"
20
+ end
21
+ operations = [ Ops::Mkdir.new(@project_root) ] +
22
+ copy_files(@templates_dir, @project_root)
23
+
24
+ operations.each do |operation|
25
+ operation.call
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def filenames_to_always_skip = [ "README.md", "mkicons.sh" ]
32
+
33
+ def copy_files(source_dir, destination_root)
34
+ operations = []
35
+ Dir.glob("#{source_dir}/*", flags: File::FNM_DOTMATCH).each do |template_file|
36
+ template_file = Pathname(template_file)
37
+ if [ ".", ".." ].include?(template_file.basename.to_s)
38
+ next
39
+ end
40
+ if template_file.directory?
41
+ operations << Ops::Mkdir.new(destination_root / template_file.basename)
42
+ operations += copy_files(template_file, destination_root / template_file.basename)
43
+ elsif template_file.extname == ".erb"
44
+ operations << Ops::RenderTemplate.new(
45
+ template_file,
46
+ destination_root:,
47
+ erb_binding: @erb_binding
48
+ )
49
+ elsif filenames_to_always_skip.include?(template_file.basename.to_s)
50
+ operations << Ops::SkipFile.new(template_file)
51
+ else
52
+ operations << Ops::CopyFile.new(template_file, destination_root:)
53
+ end
54
+ end
55
+ operations
56
+ end
57
+ end
data/lib/mkbrut/cli.rb ADDED
@@ -0,0 +1,91 @@
1
+ require "optparse"
2
+ require "pathname"
3
+
4
+ module MKBrut
5
+ class CLI
6
+ def initialize(args:, out: $stdout, err: $stderr)
7
+ @args = args
8
+ @out = out
9
+ @err = err
10
+ end
11
+
12
+ def run
13
+
14
+ app_options = parse_options(@args, MKBrut::Versions.new)
15
+ new_app = MKBrut::App.new(
16
+ current_dir: Pathname.pwd.expand_path,
17
+ app_options:,
18
+ out: PrefixedIO.new(@out, "mkbrut"),
19
+ err: @err
20
+ )
21
+ new_app.create!
22
+ 0
23
+ rescue => e
24
+ @err.puts "Error: #{e.message}"
25
+ if ENV["BRUT_CLI_RAISE_ON_ERROR"] == "true"
26
+ raise
27
+ end
28
+ 1
29
+ end
30
+
31
+ def show_help
32
+ @out.puts @option_parser
33
+ @out.puts
34
+ @out.puts "ARGUMENTS"
35
+ @out.puts
36
+ @out.puts " app-name - name for your app, which will be the folder where your app's files are created"
37
+ @out.puts
38
+ @out.puts "ENVIRONMENT VARIABLES"
39
+ @out.puts
40
+ @out.puts " BRUT_CLI_RAISE_ON_ERROR - if set to 'true', any error will raise an exception instead of printing to stderr"
41
+ @out.puts
42
+
43
+ end
44
+
45
+ private
46
+
47
+
48
+ def parse_options(args, versions)
49
+ options = {}
50
+ @option_parser = OptionParser.new do |opts|
51
+ opts.accept(MKBrut::Prefix) do |prefix|
52
+ MKBrut::Prefix.new(prefix)
53
+ end
54
+ opts.accept(MKBrut::AppId) do |prefix|
55
+ MKBrut::AppId.new(prefix)
56
+ end
57
+ opts.accept(MKBrut::Organization) do |prefix|
58
+ MKBrut::Organization.new(prefix)
59
+ end
60
+ opts.banner = "Usage: mkbrut [options] app-name\n\n Creates a new Brut-powered app\n\nOPTIONS\n\n"
61
+
62
+ opts.on("-a", "--app-id=ID", MKBrut::AppId,
63
+ "App identifier, which must be able to be used as a hostname or other Internet identifier. Derived from your app name, if omitted")
64
+
65
+ opts.on("-o", "--organization=ORG",MKBrut::Organization,
66
+ "Organization name, e.g. what you'd use for GitHub. Defaults to the app-id value")
67
+
68
+ opts.on("-e", "--prefix=PREFIX", MKBrut::Prefix,
69
+ "Two-character prefix for external IDs and autonomous custom elements. Derived from your app-id, if omitted.")
70
+
71
+ opts.on("--dry-run", "Only show what would happen, don't actually do anything")
72
+ opts.on("--[no-]demo", "Include, or not, additional files that demonstrate Brut's features (default is true for now")
73
+ opts.on("-h", "--help", "Show this help message") do
74
+ show_help
75
+ exit
76
+ end
77
+ end
78
+
79
+ @option_parser.parse!(args, into: options)
80
+ if !options.key?(:demo)
81
+ options[:demo] = true
82
+ end
83
+
84
+ options[:app_name] = MKBrut::AppName.new(args.first)
85
+ options[:app_id] = options[:'app-id']
86
+ options[:dry_run] = !!options[:'dry-run']
87
+ MKBrut::AppOptions.new(**options.merge(versions:))
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,20 @@
1
+ # This exists because ERB can't working with a SimpleDelegator or
2
+ # Delegate.
3
+ class MKBrut::ErbBindingDelegate
4
+ def initialize(app_options)
5
+ @app_options = app_options
6
+ end
7
+
8
+ # Not using Delegate because it won't work with ERB binding
9
+ def method_missing(syn,*args,&block)
10
+ if args.empty? && @app_options.respond_to?(syn)
11
+ @app_options.send(syn)
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ def respond_to_missing?(syn,include_all)
18
+ @app_options.respond_to?(syn)
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ module MKBrut
2
+ class InternetIdentifier
3
+ def initialize(name, value)
4
+ @name = name
5
+ @identifier = value.to_s
6
+ validate_identifier
7
+ end
8
+
9
+ def to_s = @identifier
10
+ alias to_str to_s
11
+
12
+ private
13
+
14
+ def validate_identifier
15
+ if @identifier.empty?
16
+ raise MKBrut::InvalidIdentifier, "#{@name} cannot be empty"
17
+ end
18
+
19
+ if @identifier.length > 63
20
+ raise MKBrut::InvalidIdentifier, "#{@name} cannot be longer than 63 characters"
21
+ end
22
+
23
+ if @identifier.start_with?("-") || @identifier.end_with?("-")
24
+ raise MKBrut::InvalidIdentifier, "#{@name} cannot start or end with a hyphen"
25
+ end
26
+
27
+ if @identifier.match?(/[^a-zA-Z0-9-]/)
28
+ raise MKBrut::InvalidIdentifier, "#{@name} can only contain letters, numbers, and hyphens"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ module MKBrut
2
+ class InvalidIdentifier < ArgumentError
3
+ end
4
+ end
@@ -0,0 +1,42 @@
1
+ class MKBrut::Ops::AddCSSImport < MKBrut::Ops::BaseOp
2
+ def initialize(project_root:, import:)
3
+ @file = project_root / "app" / "src" / "front_end" / "css" / "index.css"
4
+ @import = import
5
+ end
6
+
7
+ def call
8
+ if dry_run?
9
+ puts "Would add import '#{@import}'; to '#{@file}'"
10
+ return
11
+ end
12
+
13
+ contents = File.read(@file).split(/\n/)
14
+
15
+ inserted_import = false
16
+ previous_line_was_import = false
17
+ new_contents = []
18
+ contents.each do |line|
19
+ if line =~ /^\s*@import\s+["']/
20
+ previous_line_was_import = true
21
+ new_contents << line
22
+ else
23
+ if previous_line_was_import && !inserted_import
24
+ new_contents << "@import '#{@import}';"
25
+ inserted_import = true
26
+ end
27
+ previous_line_was_import = false
28
+ new_contents << line
29
+ end
30
+ end
31
+ if !inserted_import && previous_line_was_import
32
+ new_contents << "@import \"#{@import}\";"
33
+ inserted_import = true
34
+ end
35
+ if !inserted_import
36
+ raise "Did not find any other @imports in '#{@file}' - was expecting at least one to exist"
37
+ end
38
+ File.open(@file, "w") do |file|
39
+ file.puts new_contents.join("\n")
40
+ end
41
+ end
42
+ end
@@ -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,17 @@
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
+ File.open(@file, "a") do |file|
14
+ file.puts @content
15
+ end
16
+ end
17
+ 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