shopify_theme_builder 0.4.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96de2b7ebf1688b16c967eb0a10fc193b0ba49064f0612a2a8ae7b73d8ff5ca4
4
- data.tar.gz: e8db2e1b3643c1f2b479a4e87c03c00e1c2c8e3354ec7914dca566eabab82746
3
+ metadata.gz: 7d2c29efc227a315501fc0e21d47b8cb268f8cbd7da2887754321e4ae02914b2
4
+ data.tar.gz: 51abf4c9808331d461b486325b2a29dc3804d2546befab0ab523530370f471d3
5
5
  SHA512:
6
- metadata.gz: 1bbc6a322bd5a4bb4156e5692c49fa7358b962da211453289a9e9ad7bb419d76b0e28508b4fa41ec6f6eb662436d3b79c16d4e5a6a2ae46d130cb79b9faae1b4
7
- data.tar.gz: ce8c3198cc5d190ca41d7e1e46f69a3c59fa3649735d74c11bcc42c37eefb950723484453e6fd909785273323c257cab39711eafbb4a5bec9da3e19d47be3ec1
6
+ metadata.gz: 06e8ea516c5e0fa0ea36769a21d7c4cfcfbd5c5cbd4c4cf1c62c60707e041e645e00a7e45fcbcdea0fb993f1eef34008682901934a9f6a62299084394ba252d0
7
+ data.tar.gz: 1df921934a3ba4cca6f783a4409e4480cba8adaba7363503a6fbec0faf528007f86e8a02a52ccf039f3992942f6766c0f8fd573ea5a81e6625e4d939fd3f3dd6
data/README.md CHANGED
@@ -84,6 +84,14 @@ Install the gem and add to the application's Gemfile by executing:
84
84
  bundle add shopify_theme_builder --group "development"
85
85
  ```
86
86
 
87
+ Run the gem's install command:
88
+
89
+ ```bash
90
+ bundle exec theme-builder install
91
+ ```
92
+
93
+ This will inject Tailwind CSS and Stimulus JS into your Shopify theme layout, and add an entry in the `Procfile.dev` to run the watcher if present.
94
+
87
95
  ## Usage
88
96
 
89
97
  To watch for changes in the default components folder and build the theme, run:
@@ -105,19 +113,23 @@ If you need help with all available options, or how to set them, run:
105
113
  bundle exec theme-builder help watch
106
114
  ```
107
115
 
108
- ## After Running the Watcher
116
+ ### Component generator
109
117
 
110
- The watcher will create a CSS file that can be included in your Shopify theme layout in this way:
118
+ This gem also provides a command to generate a new component with all the necessary files. To create a new component, run:
111
119
 
112
- ```liquid
113
- {{ 'tailwind-output.css' | asset_url | stylesheet_tag }}
120
+ ```bash
121
+ bundle exec theme-builder generate
114
122
  ```
115
123
 
116
- And a JavaScript file that can be included in your Shopify theme layout in this way:
124
+ You can customize the component type, name and folder by providing additional options:
125
+ - `--type`: Specify the component type (`section`, `block`, or `snippet`).
126
+ - `--name`: Specify the component name.
127
+ - `--folder`: Specify the components folder (default is `_components`).
117
128
 
118
- ```liquid
119
- <script type="module" src="{{ 'controllers.js' | asset_url }}"></script>
120
- ```
129
+ ## Tested With
130
+
131
+ - [Skeleton Theme](https://github.com/Shopify/skeleton-theme)
132
+ - [Horizon Theme](https://github.com/Shopify/horizon)
121
133
 
122
134
  ## Development
123
135
 
@@ -129,8 +141,9 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
129
141
 
130
142
  - [x] Run the tailwind build process automatically.
131
143
  - [x] Add Stimulus JS support.
132
- - [ ] Create a command to build an example component with all the files.
133
- - [ ] Decompile existing Shopify files into components structure (?).
144
+ - [x] Create a command to build an example component with all the files.
145
+ - [ ] Investigate if it's possible to use import maps instead of a single file for Stimulus controllers.
146
+ - [ ] Decompile existing Shopify files into components structure (Is it really needed?).
134
147
 
135
148
  ## Contributing
136
149
 
@@ -0,0 +1,3 @@
1
+ <div data-controller="<%= name.parameterize %>">
2
+ <h1><%= name %> <%= type %></h1>
3
+ </div>
@@ -0,0 +1 @@
1
+ this is a comment
@@ -0,0 +1,5 @@
1
+ Stimulus.register("<%= name.parameterize %>", class extends Controller {
2
+ connect() {
3
+ console.log("Hello, Stimulus!", this.element)
4
+ }
5
+ })
@@ -0,0 +1,7 @@
1
+ <%= name %> snippet
2
+
3
+ @param {string} foo - A string value.
4
+ @param {string} [bar] - An optional string value.
5
+
6
+ @example
7
+ {% render '<%= name.parameterize %>', foo: 'Hello', bar: 'World' %}
@@ -0,0 +1 @@
1
+ console.log("Hello from the <%= name %> <%= type %>")
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "<%= name %>",
3
+ "tag": "<%= type == "section" ? "section" : "div" %>",
4
+ "class": "<%= name.parameterize %>",
5
+ "limit": 1,
6
+ "settings": [
7
+ {
8
+ "type": "text",
9
+ "id": "title",
10
+ "label": "Title"
11
+ }
12
+ ],
13
+ "max_blocks": 5,
14
+ "blocks": [{ "type": "@theme" }, { "type": "@app" }],
15
+ "presets": [
16
+ {
17
+ "name": "<%= name %>",
18
+ "settings": {
19
+ "title": "<%= name %>"
20
+ },
21
+ "blocks": []
22
+ }
23
+ ],
24
+ "locales": {
25
+ "en": {
26
+ "title": "<%= name %>"
27
+ }
28
+ },
29
+ "enabled_on": {
30
+ "templates": ["*"],
31
+ "groups": ["*"]
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ .<%= name.parameterize %> {
2
+ background-color: red;
3
+ }
@@ -12,8 +12,8 @@ module ShopifyThemeBuilder
12
12
  def build
13
13
  puts "Processing #{@files_to_process.count} files..."
14
14
 
15
- @files_to_process.each do |file|
16
- @processed_files << ShopifyThemeBuilder::LiquidProcessor.new(file).process
15
+ @files_to_process.each do |file, event|
16
+ @processed_files << ShopifyThemeBuilder::LiquidProcessor.new(file:, event:).process
17
17
  end
18
18
 
19
19
  puts "Built #{@processed_files.count} files." if @processed_files.any?
@@ -1,10 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/string/inflections"
3
4
  require "thor"
4
5
 
5
6
  module ShopifyThemeBuilder
6
7
  # CommandLine class to handle CLI commands using Thor.
7
8
  class CommandLine < Thor
9
+ include Thor::Actions
10
+
11
+ SUPPORTED_TYPES = %w[section block snippet].freeze
12
+
13
+ attr_reader :type, :name
14
+
15
+ def self.source_root
16
+ File.expand_path("../..", __dir__)
17
+ end
18
+
8
19
  desc "watch", "Watch for changes in the components folder and build the theme accordingly"
9
20
  method_option :folders, type: :array, default: ["_components"], desc: "Folders to watch for changes"
10
21
  method_option :tailwind_input_file, type: :string, default: "./assets/tailwind.css", desc: "Tailwind CSS input file"
@@ -22,5 +33,162 @@ module ShopifyThemeBuilder
22
33
  stimulus_output_file: options[:stimulus_output_file]
23
34
  )
24
35
  end
36
+
37
+ desc "install", "Set up your Shopify theme with Tailwind CSS, Stimulus JS, and the file watcher"
38
+ def install
39
+ add_tailwind_to_theme
40
+ add_stimulus_to_theme
41
+ add_watcher_to_procfile
42
+ end
43
+
44
+ desc "generate", "Generate an example component structure"
45
+ method_option :type, type: :string, desc: "Type of component to generate ('section', 'block' or 'snippet')"
46
+ method_option :name, type: :string, desc: "Name of the component to generate"
47
+ method_option :folder, type: :string, desc: "Folder to generate the component in"
48
+ def generate
49
+ @type = options[:type]
50
+ @name = options[:name]
51
+ folder = options[:folder]
52
+
53
+ until ShopifyThemeBuilder::CommandLine::SUPPORTED_TYPES.include?(@type)
54
+ unless @type.nil?
55
+ say_error "Error: Unsupported type '#{@type}'.\
56
+ Supported types are: #{ShopifyThemeBuilder::CommandLine::SUPPORTED_TYPES.join(", ")}"
57
+ end
58
+
59
+ @type = ask(
60
+ "Enter component type (section, block or snippet):",
61
+ limited_to: ShopifyThemeBuilder::CommandLine::SUPPORTED_TYPES
62
+ )
63
+ end
64
+
65
+ @name = ask("Enter component name (E.g., Slideshow):") if @name.nil?
66
+
67
+ folder = ask("Enter folder to generate the component in:", default: "_components") if folder.nil?
68
+
69
+ directory "generators", "#{folder}/#{@name.parameterize(separator: "_")}", exclude_pattern:
70
+ end
71
+
72
+ private
73
+
74
+ def exclude_pattern
75
+ return "doc.txt" unless @type == "snippet"
76
+
77
+ "schema.json"
78
+ end
79
+
80
+ def add_tailwind_to_theme
81
+ theme_file_path =
82
+ if File.exist?("snippets/stylesheets.liquid")
83
+ "snippets/stylesheets.liquid"
84
+ elsif File.exist?("layout/theme.liquid")
85
+ "layout/theme.liquid"
86
+ end
87
+
88
+ unless theme_file_path
89
+ say_error "Error: Could not find a theme file to inject Tailwind CSS.", :red
90
+ return
91
+ end
92
+
93
+ theme_file = File.read(theme_file_path)
94
+
95
+ if theme_file.include?("tailwind-output.css")
96
+ say "Tailwind CSS already included in #{theme_file_path}. Skipping injection.", :blue
97
+ return
98
+ end
99
+
100
+ injection_tag = "{{ 'tailwind-output.css' | asset_url | stylesheet_tag }}"
101
+
102
+ if theme_file_path == "snippets/stylesheets.liquid"
103
+ add_tailwind_to_snippet(theme_file_path, theme_file, injection_tag)
104
+ else
105
+ add_tailwind_to_layout(theme_file_path, theme_file, injection_tag)
106
+ end
107
+ end
108
+
109
+ def add_tailwind_to_snippet(theme_file_path, theme_file, injection_tag)
110
+ File.write(theme_file_path, "#{theme_file.chomp}\n#{injection_tag}\n")
111
+ say "Injected Tailwind CSS tag into #{theme_file_path}.", :green
112
+ end
113
+
114
+ def add_tailwind_to_layout(theme_file_path, theme_file, injection_tag)
115
+ stylesheet_tag_regex =
116
+ /(\{\{\s*['"][^'"]+['"]\s*\|\s*asset_url\s*\|\s*stylesheet_tag(?:\s*:\s*((?!\}\}).)*)?\s*\}\})/
117
+ if theme_file.match?(stylesheet_tag_regex)
118
+ updated_content = theme_file.sub(
119
+ stylesheet_tag_regex,
120
+ "\\1\n#{injection_tag}"
121
+ )
122
+ File.write(theme_file_path, updated_content)
123
+ say "Injected Tailwind CSS tag into #{theme_file_path}.", :green
124
+ else
125
+ say_error "Error: Could not find a way to inject Tailwind CSS. Please manually add the CSS tag.",
126
+ :red
127
+ end
128
+ end
129
+
130
+ def add_stimulus_to_theme
131
+ theme_file_path =
132
+ if File.exist?("snippets/scripts.liquid")
133
+ "snippets/scripts.liquid"
134
+ elsif File.exist?("layout/theme.liquid")
135
+ "layout/theme.liquid"
136
+ end
137
+
138
+ unless theme_file_path
139
+ say_error "Error: Could not find a theme file to inject Stimulus JS.", :red
140
+ return
141
+ end
142
+
143
+ theme_file = File.read(theme_file_path)
144
+
145
+ if theme_file.include?("controllers.js")
146
+ say "Stimulus JS already included in #{theme_file_path}. Skipping injection.", :blue
147
+ return
148
+ end
149
+
150
+ injection_tag = "<script type=\"module\" defer=\"defer\" src=\"{{ 'controllers.js' | asset_url }}\"></script>"
151
+
152
+ if theme_file_path == "snippets/scripts.liquid"
153
+ add_stimulus_to_snippet(theme_file_path, theme_file, injection_tag)
154
+ else
155
+ add_stimulus_to_layout(theme_file_path, theme_file, injection_tag)
156
+ end
157
+ end
158
+
159
+ def add_stimulus_to_snippet(theme_file_path, theme_file, injection_tag)
160
+ File.write(theme_file_path, "#{theme_file.chomp}\n#{injection_tag}\n")
161
+ say "Injected Stimulus JS tag into #{theme_file_path}.", :green
162
+ end
163
+
164
+ def add_stimulus_to_layout(theme_file_path, theme_file, injection_tag)
165
+ script_tag_regex = %r{(\s*)</body>}
166
+ if theme_file.match?(script_tag_regex)
167
+ updated_content = theme_file.sub(
168
+ script_tag_regex,
169
+ "\\1\n#{injection_tag}\n</body>"
170
+ )
171
+ File.write(theme_file_path, updated_content)
172
+ say "Injected Stimulus JS tag into #{theme_file_path}.", :green
173
+ else
174
+ say_error "Error: Could not find a way to inject Stimulus JS. Please manually add the JS tag.",
175
+ :red
176
+ end
177
+ end
178
+
179
+ def add_watcher_to_procfile
180
+ procfile_path = "Procfile.dev"
181
+ return unless File.exist?(procfile_path)
182
+
183
+ procfile_content = File.read(procfile_path)
184
+
185
+ if procfile_content.include?("theme-builder watch")
186
+ say "Watcher command already present in #{procfile_path}. Skipping addition.", :blue
187
+ return
188
+ end
189
+
190
+ File.write(procfile_path, "#{procfile_content.chomp}\ntheme-builder: bundle exec theme-builder watch\n")
191
+ say "Added watcher command to #{procfile_path}.", :green
192
+ end
25
193
  end
26
194
  end
@@ -24,17 +24,21 @@ module ShopifyThemeBuilder
24
24
  liquid: LIQUID_FILE_TYPES.map { |type| "#{type}.liquid" },
25
25
  schema: "schema.json",
26
26
  stylesheet: "style.css",
27
- javascript: "index.js"
27
+ javascript: "index.js",
28
+ stimulus: "controller.js"
28
29
  }.freeze
29
30
  SUPPORTED_VALUES = SUPPORTED_FILES.values.flatten.freeze
30
31
 
31
- def initialize(file)
32
- @file = file
32
+ def initialize(file:, event:)
33
+ @file = file.gsub("#{Dir.pwd}/", "")
34
+ @event = event
33
35
  @contents = +""
34
36
  @logger = Logger.new($stdout)
35
37
  end
36
38
 
37
39
  def process
40
+ return if component_deleted?
41
+
38
42
  return unless processable?
39
43
 
40
44
  compile_content
@@ -46,9 +50,28 @@ module ShopifyThemeBuilder
46
50
 
47
51
  private
48
52
 
53
+ # If a liquid file is deleted, we want to delete the compiled file as well,
54
+ # and skip further processing.
55
+ def component_deleted?
56
+ return false unless File.extname(@file) == ".liquid" && @event == :deleted
57
+
58
+ file_to_remove = compiled_filename(file_type: File.basename(@file, ".liquid"))
59
+
60
+ if File.exist?(file_to_remove)
61
+ File.delete(file_to_remove)
62
+ @logger.info "Deleted compiled file: #{file_to_remove}"
63
+ end
64
+
65
+ true
66
+ end
67
+
49
68
  # Returns true if the file is processable, false otherwise.
50
69
  def processable?
51
- supported? && correct_liquid_file? && correct_filename?
70
+ !directory? && supported? && correct_liquid_file? && correct_filename?
71
+ end
72
+
73
+ def directory?
74
+ File.directory?(@file)
52
75
  end
53
76
 
54
77
  # Checks if the file is in the list of supported files.
@@ -112,7 +135,7 @@ module ShopifyThemeBuilder
112
135
 
113
136
  # Returns the compiled filename based on the component name and liquid file type.
114
137
  # Example: _folder_to_watch/button/section.liquid -> sections/button.liquid
115
- def compiled_filename
138
+ def compiled_filename(file_type: liquid_file_type)
116
139
  filename_arr = file_dir.split(File::SEPARATOR) # Split the directory path into an array.
117
140
  filename_arr = filename_arr.drop(1) # Remove the base components folder from the path. E.g., _components
118
141
  filename_arr -= LIQUID_FILE_TYPES # Remove liquid file types from the path. E.g., section, snippet, block
@@ -121,12 +144,12 @@ module ShopifyThemeBuilder
121
144
 
122
145
  return nil if filename.empty?
123
146
 
124
- "#{liquid_file_type}s/#{filename}.liquid"
147
+ "#{file_type}s/#{filename}.liquid"
125
148
  end
126
149
 
127
150
  # Compiles the content by aggregating various related files.
128
151
  def compile_content
129
- SUPPORTED_FILES.each_key do |key|
152
+ SUPPORTED_FILES.except(:stimulus).each_key do |key|
130
153
  @contents << formatted_content(key)
131
154
  end
132
155
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShopifyThemeBuilder
4
- VERSION = "0.4.0"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -19,6 +19,7 @@ module ShopifyThemeBuilder
19
19
  @tailwind_output_file = tailwind_output_file
20
20
  @skip_tailwind = skip_tailwind
21
21
  @stimulus_output_file = stimulus_output_file
22
+ @stimulus_controller_file = ShopifyThemeBuilder::LiquidProcessor::SUPPORTED_FILES[:stimulus]
22
23
  end
23
24
 
24
25
  def watch
@@ -49,24 +50,26 @@ module ShopifyThemeBuilder
49
50
  def initial_build
50
51
  puts "Doing an initial build..."
51
52
 
53
+ files_to_process = {}
54
+
52
55
  @folders_to_watch.each do |folder|
53
- Builder.new(files_to_process: Dir.glob("#{folder}/**/*.*")).build
56
+ Dir.glob("#{folder}/**/*.*").each do |file|
57
+ files_to_process[file] = :updated
58
+ end
54
59
  end
60
+
61
+ Builder.new(files_to_process: files_to_process).build
55
62
  end
56
63
 
57
64
  def watch_folders
58
65
  puts "Watching for changes in '#{@folders_to_watch.join(", ")}' folders..."
59
66
 
60
67
  Filewatcher.new(@folders_to_watch).watch do |changes|
61
- changes.each_key do |filename|
62
- relative_filename = filename.gsub("#{Dir.pwd}/", "")
63
-
64
- Builder.new(files_to_process: [relative_filename]).build if relative_filename.start_with?(*@folders_to_watch)
65
- end
68
+ Builder.new(files_to_process: changes).build
66
69
 
67
70
  run_tailwind
68
71
 
69
- run_stimulus if changes.keys.any? { |f| f.end_with?("controller.js") }
72
+ run_stimulus if changes.keys.any? { |f| f.end_with?(@stimulus_controller_file) }
70
73
  end
71
74
  end
72
75
 
@@ -100,7 +103,7 @@ module ShopifyThemeBuilder
100
103
  end
101
104
 
102
105
  def run_stimulus
103
- controllers_files = @folders_to_watch.map { Dir.glob("#{_1}/**/controller.js") }.flatten
106
+ controllers_files = @folders_to_watch.map { Dir.glob("#{_1}/**/#{@stimulus_controller_file}") }.flatten
104
107
 
105
108
  return if controllers_files.empty?
106
109
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify_theme_builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Massimiliano Lattanzio
@@ -11,33 +11,39 @@ cert_chain: []
11
11
  date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: filewatcher
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.1'
19
+ version: '6.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9.0'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '2.1'
29
+ version: '6.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9.0'
27
33
  - !ruby/object:Gem::Dependency
28
- name: logger
34
+ name: filewatcher
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
37
  - - "~>"
32
38
  - !ruby/object:Gem::Version
33
- version: '1.7'
39
+ version: '2.1'
34
40
  type: :runtime
35
41
  prerelease: false
36
42
  version_requirements: !ruby/object:Gem::Requirement
37
43
  requirements:
38
44
  - - "~>"
39
45
  - !ruby/object:Gem::Version
40
- version: '1.7'
46
+ version: '2.1'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: tailwindcss-ruby
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -84,6 +90,13 @@ files:
84
90
  - README.md
85
91
  - Rakefile
86
92
  - exe/theme-builder
93
+ - generators/%type%.liquid.tt
94
+ - generators/comment.txt
95
+ - generators/controller.js.tt
96
+ - generators/doc.txt.tt
97
+ - generators/index.js.tt
98
+ - generators/schema.json.tt
99
+ - generators/style.css.tt
87
100
  - lib/shopify_theme_builder.rb
88
101
  - lib/shopify_theme_builder/builder.rb
89
102
  - lib/shopify_theme_builder/command_line.rb
@@ -116,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
129
  - !ruby/object:Gem::Version
117
130
  version: '0'
118
131
  requirements: []
119
- rubygems_version: 3.7.2
132
+ rubygems_version: 3.6.9
120
133
  specification_version: 4
121
134
  summary: An opinionated builder for Shopify themes using nested folders, Tailwind
122
135
  CSS, and Stimulus.