dev_shopify_theme 0.0.21

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6b4bc2a2f214aebb3d716fb73dcbc16c75cece67
4
+ data.tar.gz: 45aa9e57e59faf45ed4264db7b6fba01ff61a32b
5
+ SHA512:
6
+ metadata.gz: 3d2abfe0baa1921a294cbf470b5e11f0a4f15f70d3b72be3a9c43a18154e40c6380eb4e12b8b851941ad65b32ddc81cd3cc643482fd732efa0c15a48f5adaa28
7
+ data.tar.gz: a29ee36ab3d79f27eb4e135bcd7d5897b63044931733ff82c4c9297b672e13f13ead52e88580edfc47eb334455f5fd60dc5992b6abf3b6d354739ad88f5b6eb8
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .project
@@ -0,0 +1,4 @@
1
+ langauge: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
@@ -0,0 +1,22 @@
1
+ # v0.0.21
2
+
3
+ * Adds Locale support
4
+ * Running `theme replace` no longer deletes unwatched files if they are found on Shopify
5
+
6
+ # v0.0.20
7
+
8
+ * Fixes issue with unknown types raising exceptions -- csaunders
9
+
10
+ # v0.0.19
11
+
12
+ * Fixes issue with incorrectly detecting binary assets -- csaunders
13
+ * Properly handle unknown event types --
14
+
15
+ # v0.0.12
16
+
17
+ * Fixes issue where app tried uploading directories
18
+
19
+ # v0.0.11
20
+
21
+ * Locks JSON api version which fixed an issue with downloading large binary files -- Tyler Ball
22
+ * Respect API limits when downloading theme assets from Shopify API -- Chris Saunders
@@ -0,0 +1,6 @@
1
+ - If the issue doesn't exist, open one to start a discussion
2
+ - If the issue is fine (it probably is) start working on your patch
3
+ - Create a Pull Request
4
+ - Be sure to update CHANGELOG.md to include the details of your fix
5
+ - Go through code review
6
+ - Get it merged
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in shopify_theme.gemspec
4
+ gemspec
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Shopify
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,140 @@
1
+ # Edit your Shopify theme locally
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/shopify_theme.svg)](http://badge.fury.io/rb/shopify_theme)
4
+
5
+ The Shopify theme gem is a command line tool that lets you make live changes to themes on your Shopify store. If the command line is scary, check out the [Desktop Theme Editor app](http://apps.shopify.com/desktop-theme-editor).
6
+
7
+ It will watch your local folders for any changes in your theme (including adding and removing files) and will update your .myshopify.com store to the latest changes.
8
+
9
+ ![Shopify theme gem](https://dl.dropboxusercontent.com/u/669627/terminalreadme.png)
10
+
11
+ You do not need to make changes to your default theme. You can leverage the theme preview feature of Shopify
12
+ that allows you to view what an unpublished theme looks like on your Shopify store. This means you don't need to
13
+ sign up for extra accounts and keep your shop up to date. You will however have a blue bar that shows up that you can
14
+ remove via the web inspector in Chrome or Safari.
15
+
16
+ # Requirements
17
+
18
+ This gem works with OS X or Windows with Ruby 1.9.
19
+
20
+ First time installing Ruby on windows? Try [Rubyinstaller](http://http://rubyinstaller.org/).
21
+
22
+ # Installation
23
+
24
+ To install the shopify_theme gem use 'gem install' (you might have use 'sudo gem install')
25
+
26
+ ```
27
+ gem install shopify_theme [optional_theme_id]
28
+ ```
29
+
30
+ to update to the latest version
31
+
32
+ ```
33
+ gem update shopify_theme
34
+ ```
35
+
36
+ # Usage
37
+
38
+ Generate the config file. Go get a valid api_key and password for your store head to `https://[your store].myshopify.com/admin/apps/private` and generate a private application. By default it adds the main theme, if you want to edit one of your other themes, add the `theme_id`.
39
+
40
+ ```
41
+ theme configure api_key password store_url
42
+ ```
43
+
44
+ Example of config.yml. Notice store has no http or https declaration. You can
45
+ use `:whitelist_files:` to specify files for upload. The `assets/`, `config/`,
46
+ `layout/`, `snippets/`, `templates/` and `locales/`directories are included by
47
+ default.
48
+
49
+ You can also use `:ignore_files:` to exclude files from getting uploaded, for
50
+ example your `config/settings.html` or other configuration driven items
51
+
52
+ ```yaml
53
+ ---
54
+ :api_key: 7a8da86d3dd730b67a357dedabaac5d6
55
+ :password: 552338ce0d3aba7fc501dcf99bc57a81
56
+ :store: little-plastics.myshopify.com
57
+ :theme_id:
58
+ :whitelist_files:
59
+ - directoryToUpload/
60
+ - importantFile.txt
61
+ :ignore_files:
62
+ - config/settings.html
63
+ ```
64
+
65
+ Download all the theme files
66
+
67
+ ```
68
+ theme download
69
+ ```
70
+
71
+ Upload a theme file
72
+
73
+ ```
74
+ theme upload assets/layout.liquid
75
+ ```
76
+
77
+ Remove a theme file
78
+
79
+ ```
80
+ theme remove assets/layout.liquid
81
+ ```
82
+
83
+ Completely replace shop theme assets with the local assets
84
+
85
+ ```
86
+ theme replace
87
+ ```
88
+
89
+ Watch the theme directory and upload any files as they change
90
+
91
+ ```
92
+ theme watch
93
+ ```
94
+
95
+ Open the store in the default browser
96
+
97
+ ```
98
+ theme open
99
+ ```
100
+
101
+ Bootstrap a new theme with [Timber](http://www.shopify.com/timber)
102
+
103
+ ```
104
+ # use latest stable
105
+ theme bootstrap api_key password shop_name theme_name
106
+
107
+ # use latest build
108
+ theme bootstrap api_key password shop_name theme_name master
109
+ ```
110
+
111
+ # Common Problems
112
+
113
+ ## SSL Certificates won't verify on Windows
114
+
115
+ If you are experiencing SSL validation errors, it is most likely because your installation does not have any valid
116
+ certificates. This can be taken care of by [downloading a certificate file](http://curl.haxx.se/ca/cacert.pem) and
117
+ [setting a the SSL_CERT_FILE environment variable on your system](http://www.computerhope.com/issues/ch000549.htm).
118
+ For more details check out the following [gist](https://gist.github.com/fnichol/867550).
119
+
120
+ [See the following issue for more details](https://github.com/Shopify/shopify_theme/issues/103)
121
+
122
+ ## How do I edit a theme that isn't my shops main theme?
123
+
124
+ This can be done by setting the `theme_id` field in `config.yml` which was created when you
125
+ ran `theme configure`. Your file should look like the following:
126
+
127
+ ```yaml
128
+ ---
129
+ :api_key: 7a8da86d3dd730b67a357dedabaac5d6
130
+ :password: 552338ce0d3aba7fc501dcf99bc57a81
131
+ :store: little-plastics.myshopify.com
132
+ :theme_id: 0987654321
133
+ ```
134
+
135
+ ## Where can I find my Theme Id?
136
+
137
+ Currently the best way to find the id of the theme you want to edit is to go to the theme in your
138
+ shops admin and grab it from the url.
139
+
140
+ ![themes/THEME_ID/settings](doc/how_to_find_theme_id.png)
@@ -0,0 +1,14 @@
1
+ require 'bundler'
2
+ require 'bundler/gem_tasks'
3
+ require 'rake/testtask'
4
+
5
+ task :default => [:spec]
6
+
7
+ Rake::TestTask.new 'spec' do |t|
8
+ ENV['test'] = 'true'
9
+ t.libs = ['lib', 'spec']
10
+ t.ruby_opts << '-rubygems'
11
+ t.verbose = true
12
+ t.test_files = FileList['spec/**/*_spec.rb']
13
+ end
14
+
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This allows shopify_theme to run easily from a git checkout without install.
4
+ # Hat tip to chriseppstein of Compass fame for the code for this
5
+ def fallback_load_path(path)
6
+ retried = false
7
+ begin
8
+ yield
9
+ rescue LoadError
10
+ unless retried
11
+ $: << path
12
+ retried = true
13
+ retry
14
+ end
15
+ raise
16
+ end
17
+ end
18
+
19
+ fallback_load_path(File.join(File.dirname(__FILE__), '..', 'lib')) do
20
+ require 'shopify_theme'
21
+ require 'shopify_theme/cli'
22
+ end
23
+
24
+ ShopifyTheme::Cli.start(ARGV)
@@ -0,0 +1,155 @@
1
+ require 'httparty'
2
+ module ShopifyTheme
3
+ include HTTParty
4
+ @@current_api_call_count = 0
5
+ @@total_api_calls = 40
6
+
7
+ NOOPParser = Proc.new {|data, format| {} }
8
+ TIMER_RESET = 10
9
+ PERMIT_LOWER_LIMIT = 3
10
+ TIMBER_ZIP = "https://github.com/Shopify/Timber/archive/%s.zip"
11
+ LAST_KNOWN_STABLE = "v1.1.0"
12
+
13
+ def self.test?
14
+ ENV['test']
15
+ end
16
+
17
+ def self.manage_timer(response)
18
+ return unless response.headers['x-shopify-shop-api-call-limit']
19
+ @@current_api_call_count, @@total_api_calls = response.headers['x-shopify-shop-api-call-limit'].split('/')
20
+ @@current_timer = Time.now if @current_timer.nil?
21
+ end
22
+
23
+ def self.critical_permits?
24
+ @@total_api_calls.to_i - @@current_api_call_count.to_i < PERMIT_LOWER_LIMIT
25
+ end
26
+
27
+ def self.passed_api_refresh?
28
+ delta_seconds > TIMER_RESET
29
+ end
30
+
31
+ def self.delta_seconds
32
+ Time.now.to_i - @@current_timer.to_i
33
+ end
34
+
35
+ def self.needs_sleep?
36
+ critical_permits? && !passed_api_refresh?
37
+ end
38
+
39
+ def self.sleep
40
+ if needs_sleep?
41
+ Kernel.sleep(TIMER_RESET - delta_seconds)
42
+ @current_timer = nil
43
+ end
44
+ end
45
+
46
+ def self.api_usage
47
+ "[API Limit: #{@@current_api_call_count || "??"}/#{@@total_api_calls || "??"}]"
48
+ end
49
+
50
+
51
+ def self.asset_list
52
+ # HTTParty parser chokes on assest listing, have it noop
53
+ # and then use a rel JSON parser.
54
+ response = shopify.get(path, :parser => NOOPParser)
55
+ manage_timer(response)
56
+ assets = JSON.parse(response.body)["assets"].collect {|a| a['key'] }
57
+ # Remove any .css files if a .css.liquid file exists
58
+ assets.reject{|a| assets.include?("#{a}.liquid") }
59
+ end
60
+
61
+ def self.get_asset(asset)
62
+ response = shopify.get(path, :query =>{:asset => {:key => asset}}, :parser => NOOPParser)
63
+ manage_timer(response)
64
+
65
+ # HTTParty json parsing is broken?
66
+ asset = response.code == 200 ? JSON.parse(response.body)["asset"] : {}
67
+ asset['response'] = response
68
+ asset
69
+ end
70
+
71
+ def self.send_asset(data)
72
+ response = shopify.put(path, :body =>{:asset => data})
73
+ manage_timer(response)
74
+ response
75
+ end
76
+
77
+ def self.delete_asset(asset)
78
+ response = shopify.delete(path, :body =>{:asset => {:key => asset}})
79
+ manage_timer(response)
80
+ response
81
+ end
82
+
83
+ def self.upload_timber(name, master)
84
+ source = TIMBER_ZIP % (master ? 'master' : LAST_KNOWN_STABLE)
85
+ puts master ? "Using latest build from shopify" : "Using last known stable build -- #{LAST_KNOWN_STABLE}"
86
+ response = shopify.post("/admin/themes.json", :body => {:theme => {:name => name, :src => source, :role => 'unpublished'}})
87
+ manage_timer(response)
88
+ body = JSON.parse(response.body)
89
+ if theme = body['theme']
90
+ watch_until_processing_complete(theme)
91
+ else
92
+ puts "Could not download theme!"
93
+ puts body
94
+ exit 1
95
+ end
96
+ end
97
+
98
+ def self.config
99
+ @config ||= if File.exist? 'config.yml'
100
+ config = YAML.load(File.read('config.yml'))
101
+ config
102
+ else
103
+ puts "config.yml does not exist!" unless test?
104
+ {}
105
+ end
106
+ end
107
+
108
+ def self.config=(config)
109
+ @config = config
110
+ end
111
+
112
+ def self.path
113
+ @path ||= config[:theme_id] ? "/admin/themes/#{config[:theme_id]}/assets.json" : "/admin/assets.json"
114
+ end
115
+
116
+ def self.ignore_files
117
+ (config[:ignore_files] || []).compact.map { |r| Regexp.new(r) }
118
+ end
119
+
120
+ def self.whitelist_files
121
+ (config[:whitelist_files] || []).compact
122
+ end
123
+
124
+ def self.is_binary_data?(string)
125
+ if string.respond_to?(:encoding)
126
+ string.encoding == "US-ASCII"
127
+ else
128
+ ( string.count( "^ -~", "^\r\n" ).fdiv(string.size) > 0.3 || string.index( "\x00" ) ) unless string.empty?
129
+ end
130
+ end
131
+
132
+ def self.check_config
133
+ shopify.get(path).code == 200
134
+ end
135
+
136
+ private
137
+ def self.shopify
138
+ basic_auth config[:api_key], config[:password]
139
+ # TODO: switch back to https
140
+ # base_uri "https://#{config[:store]}"
141
+ base_uri "http://#{config[:store]}"
142
+ ShopifyTheme
143
+ end
144
+
145
+ def self.watch_until_processing_complete(theme)
146
+ count = 0
147
+ while true do
148
+ Kernel.sleep(count)
149
+ response = shopify.get("/admin/themes/#{theme['id']}.json")
150
+ theme = JSON.parse(response.body)['theme']
151
+ return theme if theme['previewable']
152
+ count += 5
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,304 @@
1
+ require 'thor'
2
+ require 'yaml'
3
+ YAML::ENGINE.yamler = 'syck' if defined? Syck
4
+ require 'abbrev'
5
+ require 'base64'
6
+ require 'fileutils'
7
+ require 'json'
8
+ require 'filewatcher'
9
+ require 'launchy'
10
+ require 'mimemagic'
11
+
12
+ module ShopifyTheme
13
+ EXTENSIONS = [
14
+ {mimetype: 'application/x-liquid', extensions: %w(liquid), parents: 'text/plain'},
15
+ {mimetype: 'application/json', extensions: %w(json), parents: 'text/plain'},
16
+ {mimetype: 'application/js', extensions: %w(map), parents: 'text/plain'},
17
+ {mimetype: 'application/vnd.ms-fontobject', extensions: %w(eot)},
18
+ {mimetype: 'image/svg+xml', extensions: %w(svg svgz)}
19
+ ]
20
+
21
+ def self.configureMimeMagic
22
+ ShopifyTheme::EXTENSIONS.each do |extension|
23
+ MimeMagic.add(extension.delete(:mimetype), extension)
24
+ end
25
+ end
26
+
27
+ class Cli < Thor
28
+ include Thor::Actions
29
+
30
+ IGNORE = %w(config.yml)
31
+ DEFAULT_WHITELIST = %w(layout/ assets/ config/ snippets/ templates/ locales/)
32
+ TIMEFORMAT = "%H:%M:%S"
33
+
34
+ tasks.keys.abbrev.each do |shortcut, command|
35
+ map shortcut => command.to_sym
36
+ end
37
+
38
+ desc "check", "check configuration"
39
+ def check
40
+ if ShopifyTheme.check_config
41
+ say("Configuration [OK]", :green)
42
+ else
43
+ say("Configuration [FAIL]", :red)
44
+ end
45
+ end
46
+
47
+ desc "configure API_KEY PASSWORD STORE THEME_ID", "generate a config file for the store to connect to"
48
+ def configure(api_key=nil, password=nil, store=nil, theme_id=nil)
49
+ config = {:api_key => api_key, :password => password, :store => store, :theme_id => theme_id}
50
+ create_file('config.yml', config.to_yaml)
51
+ end
52
+
53
+ desc "bootstrap API_KEY PASSWORD STORE THEME_NAME", "bootstrap with Timber to shop and configure local directory. Include master if you'd like to use the latest build for the theme"
54
+ method_option :master, :type => :boolean, :default => false
55
+ def bootstrap(api_key=nil, password=nil, store=nil, theme_name=nil, master=nil)
56
+ ShopifyTheme.config = {:api_key => api_key, :password => password, :store => store}
57
+
58
+ theme_name ||= 'Timber'
59
+ say("Registering #{theme_name} theme on #{store}", :green)
60
+ theme = ShopifyTheme.upload_timber(theme_name, master || false)
61
+
62
+ say("Creating directory named #{theme_name}", :green)
63
+ empty_directory(theme_name)
64
+
65
+ say("Saving configuration to #{theme_name}", :green)
66
+ ShopifyTheme.config.merge!(theme_id: theme['id'])
67
+ create_file("#{theme_name}/config.yml", ShopifyTheme.config.to_yaml)
68
+
69
+ say("Downloading #{theme_name} assets from Shopify")
70
+ Dir.chdir(theme_name)
71
+ download()
72
+ end
73
+
74
+ desc "download FILE", "download the shops current theme assets"
75
+ method_option :quiet, :type => :boolean, :default => false
76
+ method_option :exclude
77
+ def download(*keys)
78
+ assets = keys.empty? ? ShopifyTheme.asset_list : keys
79
+
80
+ if options['exclude']
81
+ assets = assets.delete_if { |asset| asset =~ Regexp.new(options['exclude']) }
82
+ end
83
+
84
+ assets.each do |asset|
85
+ download_asset(asset)
86
+ say("#{ShopifyTheme.api_usage} Downloaded: #{asset}", :green) unless options['quiet']
87
+ end
88
+ say("Done.", :green) unless options['quiet']
89
+ end
90
+
91
+ desc "open", "open the store in your browser"
92
+ def open(*keys)
93
+ if Launchy.open shop_theme_url
94
+ say("Done.", :green)
95
+ end
96
+ end
97
+
98
+ desc "upload FILE", "upload all theme assets to shop"
99
+ method_option :quiet, :type => :boolean, :default => false
100
+ def upload(*keys)
101
+ assets = keys.empty? ? local_assets_list : keys
102
+ assets.each do |asset|
103
+ send_asset(asset, options['quiet'])
104
+ end
105
+ say("Done.", :green) unless options['quiet']
106
+ end
107
+
108
+ desc "replace FILE", "completely replace shop theme assets with local theme assets"
109
+ method_option :quiet, :type => :boolean, :default => false
110
+ def replace(*keys)
111
+ say("Are you sure you want to completely replace your shop theme assets? This is not undoable.", :yellow)
112
+ if ask("Continue? (Y/N): ") == "Y"
113
+ # only delete files on remote that are not present locally
114
+ # files present on remote and present locally get overridden anyway
115
+ remote_assets = keys.empty? ? (ShopifyTheme.asset_list - local_assets_list) : keys
116
+ remote_assets.each do |asset|
117
+ delete_asset(asset, options['quiet']) unless ShopifyTheme.ignore_files.any? { |regex| regex =~ asset }
118
+ end
119
+ local_assets = keys.empty? ? local_assets_list : keys
120
+ local_assets.each do |asset|
121
+ send_asset(asset, options['quiet'])
122
+ end
123
+ say("Done.", :green) unless options['quiet']
124
+ end
125
+ end
126
+
127
+ desc "remove FILE", "remove theme asset"
128
+ method_option :quiet, :type => :boolean, :default => false
129
+ def remove(*keys)
130
+ keys.each do |key|
131
+ delete_asset(key, options['quiet'])
132
+ end
133
+ say("Done.", :green) unless options['quiet']
134
+ end
135
+
136
+ desc "watch", "upload and delete individual theme assets as they change, use the --keep_files flag to disable remote file deletion"
137
+ method_option :quiet, :type => :boolean, :default => false
138
+ method_option :keep_files, :type => :boolean, :default => false
139
+ def watch
140
+ puts "Watching current folder: #{Dir.pwd}"
141
+ watcher do |filename, event|
142
+ filename = filename.gsub("#{Dir.pwd}/", '')
143
+
144
+ next unless local_assets_list.include?(filename)
145
+ action = if [:changed, :new].include?(event)
146
+ :send_asset
147
+ elsif event == :delete
148
+ :delete_asset
149
+ else
150
+ raise NotImplementedError, "Unknown event -- #{event} -- #{filename}"
151
+ end
152
+
153
+ send(action, filename, options['quiet'])
154
+ end
155
+ end
156
+
157
+ desc "systeminfo", "print out system information and actively loaded libraries for aiding in submitting bug reports"
158
+ def systeminfo
159
+ ruby_version = "#{RUBY_VERSION}"
160
+ ruby_version += "-p#{RUBY_PATCHLEVEL}" if RUBY_PATCHLEVEL
161
+ puts "Ruby: v#{ruby_version}"
162
+ puts "Operating System: #{RUBY_PLATFORM}"
163
+ %w(Thor Listen HTTParty Launchy).each do |lib|
164
+ require "#{lib.downcase}/version"
165
+ puts "#{lib}: v" + Kernel.const_get("#{lib}::VERSION")
166
+ end
167
+ end
168
+
169
+ protected
170
+
171
+ def config
172
+ @config ||= YAML.load_file 'config.yml'
173
+ end
174
+
175
+ def shop_theme_url
176
+ url = config[:store]
177
+ url += "?preview_theme_id=#{config[:theme_id]}" if config[:theme_id] && config[:theme_id].to_i > 0
178
+ url
179
+ end
180
+
181
+ private
182
+
183
+ def watcher
184
+ FileWatcher.new(Dir.pwd).watch() do |filename, event|
185
+ yield(filename, event)
186
+ end
187
+ end
188
+
189
+ def local_assets_list
190
+ local_files.reject do |p|
191
+ @permitted_files ||= (DEFAULT_WHITELIST | ShopifyTheme.whitelist_files).map{|pattern| Regexp.new(pattern)}
192
+ @permitted_files.none? { |regex| regex =~ p } || ShopifyTheme.ignore_files.any? { |regex| regex =~ p }
193
+ end
194
+ end
195
+
196
+ def local_files
197
+ Dir.glob(File.join('**', '*')).reject do |f|
198
+ File.directory?(f)
199
+ end
200
+ end
201
+
202
+ def download_asset(key)
203
+ return unless valid?(key)
204
+ notify_and_sleep("Approaching limit of API permits. Naptime until more permits become available!") if ShopifyTheme.needs_sleep?
205
+ asset = ShopifyTheme.get_asset(key)
206
+ if asset['value']
207
+ # For CRLF line endings
208
+ content = asset['value'].gsub("\r", "")
209
+ format = "w"
210
+ elsif asset['attachment']
211
+ content = Base64.decode64(asset['attachment'])
212
+ format = "w+b"
213
+ end
214
+
215
+ FileUtils.mkdir_p(File.dirname(key))
216
+ File.open(key, format) {|f| f.write content} if content
217
+ end
218
+
219
+ def send_asset(asset, quiet=false)
220
+ return unless valid?(asset)
221
+ data = {:key => asset}
222
+ content = File.read(asset)
223
+ if binary_file?(asset) || ShopifyTheme.is_binary_data?(content)
224
+ content = File.open(asset, "rb") { |io| io.read }
225
+ data.merge!(:attachment => Base64.encode64(content))
226
+ else
227
+ data.merge!(:value => content)
228
+ end
229
+
230
+ response = show_during("[#{timestamp}] Uploading: #{asset}", quiet) do
231
+ ShopifyTheme.send_asset(data)
232
+ end
233
+ if response.success?
234
+ say("[#{timestamp}] Uploaded: #{asset}", :green) unless quiet
235
+ else
236
+ report_error(Time.now, "Could not upload #{asset}", response)
237
+ end
238
+ end
239
+
240
+ def delete_asset(key, quiet=false)
241
+ return unless valid?(key)
242
+ response = show_during("[#{timestamp}] Removing: #{key}", quiet) do
243
+ ShopifyTheme.delete_asset(key)
244
+ end
245
+ if response.success?
246
+ say("[#{timestamp}] Removed: #{key}", :green) unless quiet
247
+ else
248
+ report_error(Time.now, "Could not remove #{key}", response)
249
+ end
250
+ end
251
+
252
+ def notify_and_sleep(message)
253
+ say(message, :red)
254
+ ShopifyTheme.sleep
255
+ end
256
+
257
+ def valid?(key)
258
+ return true if DEFAULT_WHITELIST.include?(key.split('/').first + "/")
259
+ say("'#{key}' is not in a valid file for theme uploads", :yellow)
260
+ say("Files need to be in one of the following subdirectories: #{DEFAULT_WHITELIST.join(' ')}", :yellow)
261
+ false
262
+ end
263
+
264
+ def binary_file?(path)
265
+ mime = MimeMagic.by_path(path)
266
+ say("'#{path}' is an unknown file-type, uploading asset as binary", :yellow) if mime.nil? && ENV['TEST'] != 'true'
267
+ mime.nil? || !mime.text?
268
+ end
269
+
270
+ def report_error(time, message, response)
271
+ say("[#{timestamp(time)}] Error: #{message}", :red)
272
+ say("Error Details: #{errors_from_response(response)}", :yellow)
273
+ end
274
+
275
+ def errors_from_response(response)
276
+ object = {status: response.headers['status'], request_id: response.headers['x-request-id']}
277
+
278
+ errors = response.parsed_response ? response.parsed_response["errors"] : response.body
279
+
280
+ object[:errors] = case errors
281
+ when NilClass
282
+ ''
283
+ when String
284
+ errors.strip
285
+ else
286
+ errors.values.join(", ")
287
+ end
288
+ object.delete(:errors) if object[:errors].length <= 0
289
+ object
290
+ end
291
+
292
+ def show_during(message = '', quiet = false, &block)
293
+ print(message) unless quiet
294
+ result = yield
295
+ print("\r#{' ' * message.length}\r") unless quiet
296
+ result
297
+ end
298
+
299
+ def timestamp(time = Time.now)
300
+ time.strftime(TIMEFORMAT)
301
+ end
302
+ end
303
+ end
304
+ ShopifyTheme.configureMimeMagic
@@ -0,0 +1,3 @@
1
+ module ShopifyTheme
2
+ VERSION = "0.0.21"
3
+ end
@@ -0,0 +1 @@
1
+ # using the default shipit config
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "shopify_theme/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "dev_shopify_theme"
7
+ s.version = ShopifyTheme::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["John Duff", "Lawson Kurtz"]
10
+ s.email = ["john.duff@shopify.com", "lawson.kurtz@viget.com"]
11
+ s.homepage = "https://github.com/ltk/shopify_theme"
12
+ s.summary = %q{*Insecure* command line tool for developing themes (Dev env only!)}
13
+ s.description = %q{*Insecure* command line tool to help with developing Shopify themes. Provides simple commands to download, upload and delete files from a theme. Also includes the watch command to watch a directory and upload files as they change. (Dev env only!)}
14
+ s.license = 'MIT'
15
+
16
+ s.add_dependency('thor', '>= 0.14.4')
17
+ s.add_dependency('httparty', '~> 0.13.0')
18
+ s.add_dependency('json', '~> 1.8.0')
19
+ s.add_dependency('mimemagic')
20
+ s.add_dependency('filewatcher')
21
+ s.add_dependency('launchy')
22
+
23
+ s.add_development_dependency 'rake'
24
+ s.add_development_dependency 'minitest', '>= 5.0.0'
25
+ s.add_development_dependency 'pry'
26
+ s.add_development_dependency 'pry-debugger'
27
+
28
+ s.files = `git ls-files`.split("\n")
29
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
30
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
31
+ s.require_paths = ['lib']
32
+ end
@@ -0,0 +1,4 @@
1
+ ENV['TEST'] = 'true'
2
+ require 'minitest/autorun'
3
+ require 'pry'
4
+ require 'pry-debugger'
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+ require 'shopify_theme'
3
+ require 'shopify_theme/cli'
4
+
5
+ module ShopifyTheme
6
+ describe "Cli" do
7
+
8
+ class CliDouble < Cli
9
+ attr_writer :local_files, :mock_config
10
+
11
+ desc "",""
12
+ def config
13
+ @mock_config || super
14
+ end
15
+
16
+ desc "",""
17
+ def shop_theme_url
18
+ super
19
+ end
20
+
21
+ desc "",""
22
+ def binary_file?(file)
23
+ super
24
+ end
25
+
26
+ desc "", ""
27
+ def local_files
28
+ @local_files
29
+ end
30
+ end
31
+
32
+ before do
33
+ @cli = CliDouble.new
34
+ ShopifyTheme.config = {}
35
+ end
36
+
37
+ it "should remove assets that are not a part of the white list" do
38
+ @cli.local_files = ['assets/image.png', 'config.yml', 'layout/theme.liquid', 'locales/en.default.json']
39
+ local_assets_list = @cli.send(:local_assets_list)
40
+ assert_equal 3, local_assets_list.length
41
+ assert_equal false, local_assets_list.include?('config.yml')
42
+ end
43
+
44
+ it "should remove assets that are part of the ignore list" do
45
+ ShopifyTheme.config = {ignore_files: ['config/settings.html']}
46
+ @cli.local_files = ['assets/image.png', 'layout/theme.liquid', 'config/settings.html']
47
+ local_assets_list = @cli.send(:local_assets_list)
48
+ assert_equal 2, local_assets_list.length
49
+ assert_equal false, local_assets_list.include?('config/settings.html')
50
+ end
51
+
52
+ it "should generate the shop path URL to the query parameter preview_theme_id if the id is present" do
53
+ @cli.mock_config = {store: 'somethingfancy.myshopify.com', theme_id: 12345}
54
+ assert_equal "somethingfancy.myshopify.com?preview_theme_id=12345", @cli.shop_theme_url
55
+ end
56
+
57
+ it "should generate the shop path URL withouth the preview_theme_id if the id is not present" do
58
+ @cli.mock_config = {store: 'somethingfancy.myshopify.com'}
59
+ assert_equal "somethingfancy.myshopify.com", @cli.shop_theme_url
60
+
61
+ @cli.mock_config = {store: 'somethingfancy.myshopify.com', theme_id: ''}
62
+ assert_equal "somethingfancy.myshopify.com", @cli.shop_theme_url
63
+ end
64
+
65
+ it "should report binary files as such" do
66
+ extensions = %w(png gif jpg jpeg eot svg ttf woff otf swf ico pdf)
67
+ extensions.each do |ext|
68
+ assert @cli.binary_file?("hello.#{ext}"), "#{ext.upcase}s are binary files"
69
+ end
70
+ end
71
+
72
+ it "should report unknown files as binary files" do
73
+ assert @cli.binary_file?('omg.wut'), "Unknown filetypes are assumed to be binary"
74
+ end
75
+
76
+ it "should not report text based files as binary" do
77
+ refute @cli.binary_file?('theme.liquid'), "liquid files are not binary"
78
+ refute @cli.binary_file?('style.sass.liquid'), "sass.liquid files are not binary"
79
+ refute @cli.binary_file?('style.css'), 'CSS files are not binary'
80
+ refute @cli.binary_file?('application.js'), 'Javascript files are not binary'
81
+ refute @cli.binary_file?('settings_data.json'), 'JSON files are not binary'
82
+ refute @cli.binary_file?('applicaton.js.map'), 'Javascript Map files are not binary'
83
+ end
84
+ end
85
+ end
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dev_shopify_theme
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.21
5
+ platform: ruby
6
+ authors:
7
+ - John Duff
8
+ - Lawson Kurtz
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-01-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thor
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '>='
19
+ - !ruby/object:Gem::Version
20
+ version: 0.14.4
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '>='
26
+ - !ruby/object:Gem::Version
27
+ version: 0.14.4
28
+ - !ruby/object:Gem::Dependency
29
+ name: httparty
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 0.13.0
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ version: 0.13.0
42
+ - !ruby/object:Gem::Dependency
43
+ name: json
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ~>
47
+ - !ruby/object:Gem::Version
48
+ version: 1.8.0
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: 1.8.0
56
+ - !ruby/object:Gem::Dependency
57
+ name: mimemagic
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: filewatcher
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: launchy
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rake
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: minitest
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '>='
117
+ - !ruby/object:Gem::Version
118
+ version: 5.0.0
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - '>='
124
+ - !ruby/object:Gem::Version
125
+ version: 5.0.0
126
+ - !ruby/object:Gem::Dependency
127
+ name: pry
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: pry-debugger
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ description: '*Insecure* command line tool to help with developing Shopify themes.
155
+ Provides simple commands to download, upload and delete files from a theme. Also
156
+ includes the watch command to watch a directory and upload files as they change.
157
+ (Dev env only!)'
158
+ email:
159
+ - john.duff@shopify.com
160
+ - lawson.kurtz@viget.com
161
+ executables:
162
+ - dev-theme
163
+ extensions: []
164
+ extra_rdoc_files: []
165
+ files:
166
+ - .gitignore
167
+ - .travis.yml
168
+ - CHANGELOG.md
169
+ - CONTRIBUTING
170
+ - Gemfile
171
+ - MIT-LICENSE
172
+ - README.md
173
+ - Rakefile
174
+ - bin/dev-theme
175
+ - doc/how_to_find_theme_id.png
176
+ - lib/shopify_theme.rb
177
+ - lib/shopify_theme/cli.rb
178
+ - lib/shopify_theme/version.rb
179
+ - shipit.rubygems.yml
180
+ - shopify_theme.gemspec
181
+ - spec/spec_helper.rb
182
+ - spec/unit/cli_spec.rb
183
+ homepage: https://github.com/ltk/shopify_theme
184
+ licenses:
185
+ - MIT
186
+ metadata: {}
187
+ post_install_message:
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - '>='
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ required_rubygems_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - '>='
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ requirements: []
202
+ rubyforge_project:
203
+ rubygems_version: 2.2.1
204
+ signing_key:
205
+ specification_version: 4
206
+ summary: '*Insecure* command line tool for developing themes (Dev env only!)'
207
+ test_files:
208
+ - spec/spec_helper.rb
209
+ - spec/unit/cli_spec.rb
210
+ has_rdoc: