Module: BulletTrain::Themes::Application

Defined in:
lib/tasks/application.rb

Class Method Summary collapse

Class Method Details

.ask(string) ⇒ Object



320
321
322
323
# File 'lib/tasks/application.rb', line 320

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

.clean_theme(theme_name, args) ⇒ Object



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
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/tasks/application.rb', line 241

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



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
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/tasks/application.rb', line 14

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}`."
  Rails.root.join("app/assets/stylesheets").mkpath
  `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`

  puts "Ejecting postcss import aliases configuration into `./postcss-import-config.js`."
  `cp #{theme_base_path}/postcss-import-config.js #{Rails.root}/postcss-import-config.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

.eject_theme_main_css(theme_name) ⇒ Object



6
7
8
9
10
11
12
# File 'lib/tasks/application.rb', line 6

def self.eject_theme_main_css(theme_name)
  theme_base_path = `bundle show --paths bullet_train-themes-#{theme_name}`.chomp

  puts "Ejecting app/assets/stylesheets/#{theme_name}.tailwind.css."
  Rails.root.join("app/assets/stylesheets").mkpath
  `cp -R #{theme_base_path}/app/assets/stylesheets/#{theme_name}.tailwind.css #{Rails.root}/app/assets/stylesheets/#{theme_name}.tailwind.css`
end

.install_theme(theme_name) ⇒ Object



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/tasks/application.rb', line 224

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



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
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/tasks/application.rb', line 137

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