Module: BulletTrain::Themes::Application

Defined in:
lib/tasks/application.rb

Class Method Summary collapse

Class Method Details

.ask(string) ⇒ Object



309
310
311
312
# File 'lib/tasks/application.rb', line 309

def self.ask(string)
  puts string.blue
  $stdin.gets.strip
end

.clean_theme(theme_name, args) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/tasks/application.rb', line 230

def self.clean_theme(theme_name, args)
  light_base_path = `bundle show --paths bullet_train-themes-light`.chomp
  tailwind_base_path = `bundle show --paths bullet_train-themes-tailwind_css`.chomp
  theme_base_path = `bundle show --paths bullet_train-themes`.chomp

  directory_content = `find . | grep 'app/.*#{args[:theme]}'`.lines.map(&:chomp)
  directory_content = directory_content.reject { |content| content.match?("app/assets/builds/") }
  files = directory_content.select { |file| file.match?(/(\.erb)|(\.rb)|(\.css)|(\.js)$/) }

  # Files that exist outside of "./app/" that we need to check.
  files += [
    "tailwind.#{args[:theme]}.config.js",
    "tailwind.mailer.#{args[:theme]}.config.js",
  ]

  # This file doesn't exist under "app/" in its original gem, so we handle it differently.
  # Also, don't remove this file from the starter repository in case
  # the developer has any ejected files that have been customized.
  files.delete("./app/lib/bullet_train/themes/#{args[:theme]}.rb")

  files.each do |file|
    original_theme_path = nil

    # Remove the current directory syntax for concatenation with the gem base path.
    file.gsub!("./", "")

    [light_base_path, tailwind_base_path, theme_base_path].each do |theme_path|
      # Views exist under "base" when the gem is "bullet_train-themes".
      theme_gem_name = theme_path.scan(/(.*themes-)(.*$)/).flatten.pop || "base"
      original_theme_path = file.gsub(args[:theme], theme_gem_name)

      if File.exist?("#{theme_path}/#{original_theme_path}")
        original_theme_path = "#{theme_path}/#{original_theme_path}"
        break
      end
    end

    ejected_file_content = File.read(file)

    # These are the only files where we replace the theme name inside of them when ejecting,
    # so we revert the contents and check if the file has been changed or not.
    transformed_files = [
      "app/views/themes/foo/layouts/_head.html.erb",
      "app/assets/stylesheets/foo.tailwind.css",
      "tailwind.mailer.#{args[:theme]}.config.js"
    ]
    ejected_file_content.gsub!(/#{args[:theme]}/i, theme_name) if transformed_files.include?(file)

    if ejected_file_content == File.read(original_theme_path)
      puts "No changes in `#{file}` since being ejected. Removing."
      `rm #{file}`
    end
  end

  # Delete all leftover directories with empty content.
  [
    "./app/assets/stylesheets/",
    "./app/views/themes/"
  ].each do |remaining_directory|
    puts "Cleaning out directory: #{remaining_directory}"
    remaining_directory_content = Dir.glob(remaining_directory + "**/*")
    remaining_directories = remaining_directory_content.select { |content| File.directory?(content) }
    remaining_directories.reverse_each { |dir| Dir.rmdir dir if Dir.empty?(dir) }
    FileUtils.rmdir(remaining_directory) if Dir.empty?(remaining_directory)
  end

  # These are files from the starter repository that need to be set back to the original theme.
  [
    "Procfile.dev",
    "app/helpers/application_helper.rb",
    "package.json",
    "test/system/resolver_system_test.rb"
  ].each do |file|
    puts "Reverting changes in #{file}."
    new_lines = File.open(file).readlines.join.gsub(/#{args[:theme]}/i, theme_name)
    File.write(file, new_lines)
  end
end

.eject_theme(theme_name, ejected_theme_name) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/tasks/application.rb', line 6

def self.eject_theme(theme_name, ejected_theme_name)
  theme_parts = theme_name.humanize.split.map { |str| str.capitalize }
  constantized_theme = theme_parts.join
  humanized_theme = theme_parts.join(" ")

  theme_base_path = `bundle show --paths bullet_train-themes-#{theme_name}`.chomp
  puts "Ejecting from #{humanized_theme} theme in `#{theme_base_path}`."

  puts "Ejecting Tailwind configuration into `./tailwind.#{ejected_theme_name}.config.js`."
  `cp #{theme_base_path}/tailwind.#{theme_name}.config.js #{Rails.root}/tailwind.#{ejected_theme_name}.config.js`

  puts "Ejecting Tailwind mailer configuration into `./tailwind.mailer.#{ejected_theme_name}.config.js`."
  `cp #{theme_base_path}/tailwind.mailer.#{theme_name}.config.js #{Rails.root}/tailwind.mailer.#{ejected_theme_name}.config.js`
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/#{theme_name}/#{ejected_theme_name}/g" #{Rails.root}/tailwind.mailer.#{ejected_theme_name}.config.js)

  puts "Ejecting stylesheets into `./app/assets/stylesheets/#{ejected_theme_name}`."
  `mkdir #{Rails.root}/app/assets/stylesheets`
  `cp -R #{theme_base_path}/app/assets/stylesheets/#{theme_name} #{Rails.root}/app/assets/stylesheets/#{ejected_theme_name}`
  `cp -R #{theme_base_path}/app/assets/stylesheets/#{theme_name}.tailwind.css #{Rails.root}/app/assets/stylesheets/#{ejected_theme_name}.tailwind.css`
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/light/#{ejected_theme_name}/g" #{Rails.root}/app/assets/stylesheets/#{ejected_theme_name}.tailwind.css)

  puts "Ejecting JavaScript into `./app/javascript/application.#{ejected_theme_name}.js`."
  `cp #{theme_base_path}/app/javascript/application.#{theme_name}.js #{Rails.root}/app/javascript/application.#{ejected_theme_name}.js`

  `mkdir #{Rails.root}/app/views/themes`

  new_files = {}
  {
    "bullet_train-themes" => "base",
    "bullet_train-themes-tailwind_css" => "tailwind_css",
    "bullet_train-themes-light" => "light"
  }.each do |gem, theme_name|
    gem_path = `bundle show --paths #{gem}`.chomp
    showcase_partials = Dir.glob("#{gem_path}/app/views/showcase/**/*.html.erb")

    `find #{gem_path}/app/views/themes`.lines.map(&:chomp).each do |file_or_directory|
      target_file_or_directory = file_or_directory.gsub(gem_path, "").gsub("/#{theme_name}", "/#{ejected_theme_name}")
      target_file_or_directory = Rails.root.to_s + target_file_or_directory

      if File.directory?(file_or_directory)
        puts "Creating `#{target_file_or_directory}`."
        `mkdir #{target_file_or_directory}`
      else
        puts "Copying `#{target_file_or_directory}`."
        `cp #{file_or_directory} #{target_file_or_directory}`
        gem_with_version = gem_path.split("/").last
        new_files[target_file_or_directory] = file_or_directory.split(/(?=#{gem_with_version})/).last
      end

      # Look for showcase preview.
      file_name = target_file_or_directory.split("/").last
      showcase_preview = showcase_partials.find { _1.end_with?(file_name) }
      if showcase_preview
        puts "Ejecting showcase preview for #{target_file_or_directory}"
        partial_relative_path = showcase_preview.scan(/(?=app\/views\/showcase).*/).last
        directory = partial_relative_path.split("/")[0..-2].join("/")
        FileUtils.mkdir_p(directory)
        FileUtils.touch(partial_relative_path)
        `cp #{showcase_preview} #{partial_relative_path}`
        new_files[partial_relative_path] = "#{gem_path.scan(/#{gem}.*/).pop}/#{partial_relative_path}"
      end
    end
  end

  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/#{theme_name}/#{ejected_theme_name}/g" #{Rails.root}/app/views/themes/#{ejected_theme_name}/layouts/_head.html.erb)
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/#{theme_name}/#{ejected_theme_name}/g" #{Rails.root}/app/views/themes/#{ejected_theme_name}/layouts/_mailer.html.erb)

  puts "Cutting local `Procfile.dev` over from `#{theme_name}` to `#{ejected_theme_name}`."
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/#{theme_name}/#{ejected_theme_name}/g" #{Rails.root}/Procfile.dev)

  puts "Cutting local `package.json` over from `#{theme_name}` to `#{ejected_theme_name}`."
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/#{theme_name}/#{ejected_theme_name}/g" #{Rails.root}/package.json)

  puts "Cutting `test/system/resolver_system_test.rb` over from `#{theme_name}` to `#{ejected_theme_name}`."
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/light/#{ejected_theme_name}/g" #{Rails.root}/test/system/resolver_system_test.rb)

  # Stub out the class that represents this theme and establishes its inheritance structure.
  target_path = "#{Rails.root}/app/lib/bullet_train/themes/#{ejected_theme_name}.rb"
  puts "Stubbing out a class that represents this theme in `.#{target_path}`."
  `mkdir -p #{Rails.root}/app/lib/bullet_train/themes`
  `cp #{theme_base_path}/lib/bullet_train/themes/#{theme_name}.rb #{target_path}`
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/module #{constantized_theme}/module #{ejected_theme_name.titlecase}/g" #{target_path})
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/TailwindCss/#{constantized_theme}/g" #{target_path})
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/#{theme_name}/#{ejected_theme_name}/g" #{target_path})

  theme_file = Pathname.new(target_path)
  msmn = Masamune::AbstractSyntaxTree.new(theme_file.readlines.join)
  data_to_skip =
    msmn.method_calls(token_value: "require") +
    msmn.method_calls(token_value: "mattr_accessor") +
    msmn.comments.select { |comment| comment.token_value.match?("TODO") }
  lines_to_skip = data_to_skip.map { |data| data.line_number - 1 }
  new_lines = theme_file.readlines.select.with_index do |line, idx|
    !lines_to_skip.include?(idx) || line.match?("mattr_accessor :colors")
  end
  theme_file.write new_lines.join

  # We add the comment to the ejected files here so the sed calls don't
  # overwrite package names like `bullet_train-themes-light`.
  new_files.each do |key, value|
    file = Pathname.new(key)
    lines = file.readlines

    new_lines = case key.split(".").last
    when "rb", "yml"
      lines.unshift("# Ejected from #{value}\n\n")
    when "erb"
      lines.unshift("<% # Ejected from #{value} %>\n\n")
    end
    file.write(new_lines.join)
  end

  `standardrb --fix #{target_path}`

  puts "Cutting local project over from `#{theme_name}` to `#{ejected_theme_name}` in `app/helpers/application_helper.rb`."
  %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/:#{theme_name}/:#{ejected_theme_name}/g" #{Rails.root}/app/helpers/application_helper.rb)

  puts "You must restart `bin/dev` at this point, because of the changes to `Procfile.dev` and `package.json`."
end

.install_theme(theme_name) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/tasks/application.rb', line 213

def self.install_theme(theme_name)
  helper = Pathname.new("./app/helpers/application_helper.rb")
  msmn = Masamune::AbstractSyntaxTree.new(helper.readlines.join)
  current_theme_def = msmn.method_definitions(token_value: "current_theme").pop
  current_theme = msmn.symbols.find { |node| node.line_number > current_theme_def.line_number }.token_value
  helper.write msmn.replace(type: :symbol, old_token_value: current_theme, new_token_value: theme_name)

  [Pathname.new("./Procfile.dev"), Pathname.new("./package.json")].each do |file|
    changed = file.read.gsub! current_theme, theme_name
    if changed
      file.write changed
    end
  end

  puts "Finished installing `#{theme_name}`.".blue
end

.release_theme(original_theme_name, args) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/tasks/application.rb', line 126

def self.release_theme(original_theme_name, args)
  # We only want developers publishing gems off of `bullet_train-themes-light`, so if the task looks
  # something like `rake bullet_train:themes:foo:release[bar]`, we prevent them from moving any further here.
  if original_theme_name != "light"
    puts "You can only release new themes based off of Bullet Train's Light theme. Please eject a new theme from there, and publish your gem once you've finished making changes.".red
    exit 1
  elsif original_theme_name.nil?
    puts "Please run the command with the name of the theme you want to release.".red
    puts "For example: > rake bullet_train:themes:light:release[foo]"
  end

  puts "Preparing to release your custom theme: ".blue + args[:theme_name]
  puts ""
  puts "Before we make a new Ruby gem for your theme, you'll have to set up a GitHub repository first.".blue
  puts "Hit <Return> and we'll open a browser to GitHub where you can create a new repository.".blue
  puts "Make sure you name the repository ".blue + "bullet_train-themes-#{args[:theme_name]}"
  puts ""
  puts "When you're done, copy the SSH path from the new repository and return here.".blue
  ask "We'll ask you to paste it to us in the next step."
  `#{(Gem::Platform.local.os == "linux") ? "xdg-open" : "open"} https://github.com/new`

  ssh_path = ask "OK, what was the SSH path? (It should look like `git@github.com:your-account/your-new-repo.git`.)"
  puts ""
  puts "Great, you're all set.".blue
  puts "We'll take it from here, so sit back and enjoy the ride 🚄️".blue
  puts ""
  puts "Creating a Ruby gem for ".blue + "#{args[:theme_name]}..."

  Dir.mkdir("local") unless Dir.exist?("./local")
  if Dir.exist?("./local/bullet_train-themes-#{args[:theme_name]}")
    raise "You already have a repository named `bullet_train-themes-#{args[:theme_name]}` in `./local`.\n" \
      "Make sure you delete it first to create an entirely new gem."
  end

  # Pull `bullet_train-themes-light` only from `bullet_train-core` into the new theme directory.
  # https://www.git-scm.com/docs/git-sparse-checkout
  `mkdir ./local/bullet_train-themes-#{args[:theme_name]}`
  `cd ./local/bullet_train-themes-#{args[:theme_name]} && git init && git remote add bullet-train-core git@github.com:bullet-train-co/bullet_train-core.git`
  `cd ./local/bullet_train-themes-#{args[:theme_name]} && git config core.sparseCheckout true && echo "bullet_train-themes-light/**/*" >> .git/info/sparse-checkout`
  `cd ./local/bullet_train-themes-#{args[:theme_name]} && git pull bullet-train-core main && git remote rm bullet-train-core`
  `cd ./local/bullet_train-themes-#{args[:theme_name]} && mv bullet_train-themes-light/* . && mv bullet_train-themes-light/.* .`
  `cd ./local/bullet_train-themes-#{args[:theme_name]} && rmdir bullet_train-themes-light/`
  `cd ./local/bullet_train-themes-#{args[:theme_name]} && git config core.sparseCheckout false`

  BulletTrain::Themes::Light::CustomThemeFileReplacer.new(original_theme_name, args[:theme_name]).replace_theme

  work_tree_flag = "--work-tree=local/bullet_train-themes-#{args[:theme_name]}"
  git_dir_flag = "--git-dir=local/bullet_train-themes-#{args[:theme_name]}/.git"
  path = "./local/bullet_train-themes-#{args[:theme_name]}"

  # Set up the proper remote.
  `git #{work_tree_flag} #{git_dir_flag} remote add origin #{ssh_path}`
  `git #{work_tree_flag} #{git_dir_flag} add .`
  `git #{work_tree_flag} #{git_dir_flag} commit -m "Add initial files"`
  `git #{work_tree_flag} #{git_dir_flag} branch -m main`

  # Build the gem.
  `(cd #{path} && gem build bullet_train-themes-#{args[:theme_name]}.gemspec)`
  `git #{work_tree_flag} #{git_dir_flag} add .`
  `git #{work_tree_flag} #{git_dir_flag} commit -m "Build gem"`

  # Commit the deleted files on the main application.
  `git add .`
  `git commit -m "Remove #{args[:theme_name]} files from application"`

  # Push the gem's source code, but not the last commit in the main application.
  `git #{work_tree_flag} #{git_dir_flag} push -u origin main`

  puts ""
  puts ""
  puts "You're all set! Copy and paste the following commands to publish your gem:".blue
  puts "cd ./local/bullet_train-themes-#{args[:theme_name]}"
  puts "gem push bullet_train-themes-#{args[:theme_name]}-1.0.gem && cd ../../"
  puts ""
  puts "You may have to wait for some time until the gem can be downloaded via the Gemfile.".blue
  puts "After a few minutes, run the following command in your main application:".blue
  puts "bundle add bullet_train-themes-#{args[:theme_name]}"
  puts ""
  puts "Then you'll be ready to use your custom gem in your Bullet Train application.".blue
  puts ""
  puts "Please note that we have deleted the new theme from your main application.".blue
  puts "run `git log -1` for details."
  puts ""
  puts "Use `rake bullet_train:themes:light:install` to revert to the original theme,".blue
  puts "or run `rake bullet_train:themes:#{args[:theme_name]}:install` whenever you want to use your new theme.".blue
end