dev_shopify_theme 0.0.21

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.
@@ -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: