shoperb-theme-editor 0.8.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.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/CONTRIBUTING.md +40 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +124 -0
  5. data/bin/shoperb +313 -0
  6. data/lib/shoperb_theme_editor/api/server.rb +10 -0
  7. data/lib/shoperb_theme_editor/api/views/callback.erb +53 -0
  8. data/lib/shoperb_theme_editor/api.rb +264 -0
  9. data/lib/shoperb_theme_editor/build/json.rb +167 -0
  10. data/lib/shoperb_theme_editor/build/liquid.rb +38 -0
  11. data/lib/shoperb_theme_editor/build/section.rb +51 -0
  12. data/lib/shoperb_theme_editor/build/settings.rb +224 -0
  13. data/lib/shoperb_theme_editor/build.rb +68 -0
  14. data/lib/shoperb_theme_editor/configuration.rb +98 -0
  15. data/lib/shoperb_theme_editor/error.rb +35 -0
  16. data/lib/shoperb_theme_editor/ext/array.rb +17 -0
  17. data/lib/shoperb_theme_editor/ext/nil_class.rb +7 -0
  18. data/lib/shoperb_theme_editor/ext/sequel.rb +82 -0
  19. data/lib/shoperb_theme_editor/ext.rb +2 -0
  20. data/lib/shoperb_theme_editor/init.rb +87 -0
  21. data/lib/shoperb_theme_editor/logger.rb +58 -0
  22. data/lib/shoperb_theme_editor/mounter/models/address.rb +65 -0
  23. data/lib/shoperb_theme_editor/mounter/models/attribute.rb +11 -0
  24. data/lib/shoperb_theme_editor/mounter/models/attribute_key.rb +17 -0
  25. data/lib/shoperb_theme_editor/mounter/models/base.rb +196 -0
  26. data/lib/shoperb_theme_editor/mounter/models/blog_category.rb +47 -0
  27. data/lib/shoperb_theme_editor/mounter/models/blog_post.rb +45 -0
  28. data/lib/shoperb_theme_editor/mounter/models/brand.rb +11 -0
  29. data/lib/shoperb_theme_editor/mounter/models/cart.rb +35 -0
  30. data/lib/shoperb_theme_editor/mounter/models/cart_item.rb +71 -0
  31. data/lib/shoperb_theme_editor/mounter/models/category.rb +99 -0
  32. data/lib/shoperb_theme_editor/mounter/models/collection.rb +40 -0
  33. data/lib/shoperb_theme_editor/mounter/models/country.rb +18 -0
  34. data/lib/shoperb_theme_editor/mounter/models/currency.rb +17 -0
  35. data/lib/shoperb_theme_editor/mounter/models/custom_field.rb +22 -0
  36. data/lib/shoperb_theme_editor/mounter/models/customer.rb +77 -0
  37. data/lib/shoperb_theme_editor/mounter/models/customer_customer_group.rb +12 -0
  38. data/lib/shoperb_theme_editor/mounter/models/customer_group.rb +13 -0
  39. data/lib/shoperb_theme_editor/mounter/models/customer_subscription.rb +40 -0
  40. data/lib/shoperb_theme_editor/mounter/models/customer_subscription_plan.rb +32 -0
  41. data/lib/shoperb_theme_editor/mounter/models/discount.rb +40 -0
  42. data/lib/shoperb_theme_editor/mounter/models/discount_variant.rb +15 -0
  43. data/lib/shoperb_theme_editor/mounter/models/image.rb +51 -0
  44. data/lib/shoperb_theme_editor/mounter/models/language.rb +17 -0
  45. data/lib/shoperb_theme_editor/mounter/models/link.rb +61 -0
  46. data/lib/shoperb_theme_editor/mounter/models/media_file.rb +19 -0
  47. data/lib/shoperb_theme_editor/mounter/models/menu.rb +21 -0
  48. data/lib/shoperb_theme_editor/mounter/models/meta.rb +10 -0
  49. data/lib/shoperb_theme_editor/mounter/models/news_item.rb +11 -0
  50. data/lib/shoperb_theme_editor/mounter/models/order.rb +133 -0
  51. data/lib/shoperb_theme_editor/mounter/models/order_item.rb +137 -0
  52. data/lib/shoperb_theme_editor/mounter/models/order_item_attribute.rb +17 -0
  53. data/lib/shoperb_theme_editor/mounter/models/order_return.rb +42 -0
  54. data/lib/shoperb_theme_editor/mounter/models/order_return_item.rb +29 -0
  55. data/lib/shoperb_theme_editor/mounter/models/order_return_item_entity.rb +25 -0
  56. data/lib/shoperb_theme_editor/mounter/models/order_return_parcel.rb +20 -0
  57. data/lib/shoperb_theme_editor/mounter/models/page.rb +26 -0
  58. data/lib/shoperb_theme_editor/mounter/models/payment_card.rb +21 -0
  59. data/lib/shoperb_theme_editor/mounter/models/payment_method.rb +67 -0
  60. data/lib/shoperb_theme_editor/mounter/models/payment_provider.rb +23 -0
  61. data/lib/shoperb_theme_editor/mounter/models/product.rb +144 -0
  62. data/lib/shoperb_theme_editor/mounter/models/product_attribute.rb +32 -0
  63. data/lib/shoperb_theme_editor/mounter/models/product_search.rb +53 -0
  64. data/lib/shoperb_theme_editor/mounter/models/product_type.rb +21 -0
  65. data/lib/shoperb_theme_editor/mounter/models/review.rb +38 -0
  66. data/lib/shoperb_theme_editor/mounter/models/search.rb +11 -0
  67. data/lib/shoperb_theme_editor/mounter/models/shipping_method.rb +39 -0
  68. data/lib/shoperb_theme_editor/mounter/models/shop.rb +58 -0
  69. data/lib/shoperb_theme_editor/mounter/models/state.rb +17 -0
  70. data/lib/shoperb_theme_editor/mounter/models/theme.rb +89 -0
  71. data/lib/shoperb_theme_editor/mounter/models/variant.rb +96 -0
  72. data/lib/shoperb_theme_editor/mounter/models/variant_attribute.rb +46 -0
  73. data/lib/shoperb_theme_editor/mounter/models/vendor.rb +38 -0
  74. data/lib/shoperb_theme_editor/mounter/server/assets.rb +35 -0
  75. data/lib/shoperb_theme_editor/mounter/server/defaults.rb +44 -0
  76. data/lib/shoperb_theme_editor/mounter/server/exception_handler.rb +22 -0
  77. data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_footer.liquid +0 -0
  78. data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_header.liquid +0 -0
  79. data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_stylesheets.liquid +3 -0
  80. data/lib/shoperb_theme_editor/mounter/server/renderer.rb +166 -0
  81. data/lib/shoperb_theme_editor/mounter/server/routes/cart.rb +127 -0
  82. data/lib/shoperb_theme_editor/mounter/server/routes/dummy.rb +34 -0
  83. data/lib/shoperb_theme_editor/mounter/server/routes/locale.rb +31 -0
  84. data/lib/shoperb_theme_editor/mounter/server/routes/pages.rb +33 -0
  85. data/lib/shoperb_theme_editor/mounter/server/routes/search.rb +18 -0
  86. data/lib/shoperb_theme_editor/mounter/server/routes.rb +366 -0
  87. data/lib/shoperb_theme_editor/mounter/server/routes_helper.rb +278 -0
  88. data/lib/shoperb_theme_editor/mounter/server.rb +66 -0
  89. data/lib/shoperb_theme_editor/mounter.rb +30 -0
  90. data/lib/shoperb_theme_editor/os.rb +13 -0
  91. data/lib/shoperb_theme_editor/package.rb +81 -0
  92. data/lib/shoperb_theme_editor/sync/images.rb +69 -0
  93. data/lib/shoperb_theme_editor/sync/pagination.rb +52 -0
  94. data/lib/shoperb_theme_editor/sync.rb +229 -0
  95. data/lib/shoperb_theme_editor/translations.rb +22 -0
  96. data/lib/shoperb_theme_editor/utils.rb +50 -0
  97. data/lib/shoperb_theme_editor.rb +159 -0
  98. data/shoperb_theme_editor.gemspec +60 -0
  99. metadata +510 -0
@@ -0,0 +1,30 @@
1
+ module Shoperb module Theme module Editor
2
+ module Mounter
3
+
4
+ Editor.autoload_all self, "mounter"
5
+
6
+ module Model
7
+
8
+ def self.console
9
+ binding.pry
10
+ end
11
+
12
+ Editor.autoload_all self, "mounter/models"
13
+ end
14
+
15
+ def self.start
16
+ Rack::Handler::WEBrick.run(Server.new,
17
+ Port: Editor["port"],
18
+ AccessLog: [],
19
+ Logger: WEBrick::Log::new(Os["/dev/null"], 7),
20
+ StartCallback: -> {
21
+ Logger.success "Server started\nBrowse http://0.0.0.0:#{Editor["port"]}\n"
22
+ }
23
+ )
24
+ rescue Errno::EADDRINUSE => e
25
+ Logger.error e.message
26
+ exit!
27
+ end
28
+
29
+ end
30
+ end end end
@@ -0,0 +1,13 @@
1
+ module Shoperb module Theme module Editor
2
+ module Os
3
+ REPLACEMENTS = {
4
+ "/dev/null" => {
5
+ /mswin|mingw/ => "NUL:"
6
+ }
7
+ }
8
+
9
+ def self.[] key
10
+ (REPLACEMENTS[key].detect { |k,v| k =~ RUBY_PLATFORM } || [])[1] || key
11
+ end
12
+ end
13
+ end end end
@@ -0,0 +1,81 @@
1
+ require 'artisans'
2
+
3
+ module Shoperb module Theme module Editor
4
+ module Package
5
+
6
+ extend self
7
+
8
+ def unzip file
9
+ Zip::File.open(file.path) { |zip_file|
10
+ spec = zip_file.glob("*/config/spec.json").first
11
+ if spec && (spec_content = zip_file.read(spec).presence)
12
+ zip_handle = Editor.handle(spec_content)
13
+ else
14
+ zip_handle = Editor["handle"] = zip_file.entries.first.name.split("/").first
15
+ end
16
+
17
+ # remove old files, which are not present in theme zip anymore
18
+ # but only from folders, mentioned in zip
19
+ zip_files = zip_file.entries.map{|f| f.to_s.gsub(/^#{zip_handle}\//, '') }
20
+ zip_folders = zip_files.map{|f| f.split('/')[0] }.uniq
21
+ files_to_remove = Dir.glob("{#{zip_folders.join(',')}}/**/*.*") - zip_files
22
+
23
+ zip_file.each do |entry|
24
+ entry_name = Pathname.new(entry.name).cleanpath.to_s
25
+ name = entry_name.gsub(/\A#{zip_handle}\//, "")
26
+ extract_path = Utils.base + name
27
+ FileUtils.mkdir_p extract_path.dirname
28
+ Logger.notify "Extracting #{name}" do
29
+ entry.extract(extract_path) { true }
30
+ end
31
+ end
32
+ files_to_remove.each { |f| File.delete(f) }
33
+ }
34
+ ensure
35
+ Utils.rm_tempfile file
36
+ end
37
+
38
+ def zip
39
+ compiler = Editor.compiler("/system/assets/#{Editor["oauth-site"]}/#{Editor.handle}/", digests: true)
40
+
41
+ zip = Zip::OutputStream.write_buffer do |out|
42
+ compiler.compiled_files do |path, content, type = :file|
43
+ case type
44
+ when :symlink
45
+ write_symlink(out, path, content)
46
+ else
47
+ write_file(out, path) { content }
48
+ end
49
+ end
50
+ end
51
+ Utils.write_file("debug.zip") { zip.string } if Editor["verbose"]
52
+ Utils.mk_tempfile zip.string, "#{handle.basename}-", ".zip"
53
+ end
54
+
55
+ def write_file out, file
56
+ out.put_next_entry(zip_file_path = handle + file)
57
+ content = block_given? ? yield : file.read
58
+ if file.to_s[0..6] == "sources"
59
+ out.write content
60
+ else
61
+ Logger.notify "Packing #{file}" do
62
+ out.write content
63
+ end
64
+ end
65
+ end
66
+
67
+ def write_symlink out, current, target
68
+ entry = out.put_next_entry(zip_file_path = handle + current)
69
+ entry.instance_variable_set("@ftype", :symlink)
70
+ entry.instance_variable_set("@filepath", target.to_s)
71
+ Logger.notify "Packing #{current}" do
72
+ out.write((handle + target).relative_path_from(handle + current).to_s[3..-1])
73
+ end
74
+ end
75
+
76
+ def handle
77
+ Pathname.new(Editor.handle)
78
+ end
79
+
80
+ end
81
+ end end end
@@ -0,0 +1,69 @@
1
+ module Shoperb module Theme module Editor
2
+ module Sync
3
+ module Images
4
+
5
+ extend self
6
+ mattr_accessor :queue
7
+ self.queue = []
8
+
9
+ mattr_accessor :dir
10
+ self.dir = Utils.base + "data" + "assets" + "images"
11
+
12
+ def process
13
+ FileUtils.mkdir_p(dir)
14
+ Sync.process Mounter::Model::Image do |image|
15
+ if image["entity_id"]
16
+ process_sizes(image)
17
+ image
18
+ end
19
+ end
20
+ threaded_download "Downloading images"
21
+ end
22
+
23
+ private
24
+
25
+ def process_sizes image
26
+ image["sizes"] = image["sizes"].select{|size,url|url}.map do |size, url|
27
+ filename = filename_from_url(url)
28
+ enqueue_download(url, filename)
29
+ [size, filename.relative_path_from(dir).to_s]
30
+ end.compact.to_h
31
+ end
32
+
33
+ def filename_from_url url
34
+ dir + "#{Pathname.new(url).basename.to_s.split("?")[0]}"
35
+ end
36
+
37
+ def enqueue_download url, filename
38
+ (queue << -> {
39
+ begin
40
+ Utils.write_file(filename) do
41
+ begin
42
+ Timeout::timeout(10) do
43
+ URI(url).open.read
44
+ end
45
+ rescue Timeout::Error => e
46
+ retry
47
+ end
48
+ end
49
+ rescue Exception => e
50
+ Logger.error("#{e.message} (#{url} => #{filename})")
51
+ end
52
+ }) unless File.exist?(filename)
53
+ end
54
+
55
+ def threaded_download message
56
+ return unless queue.any?
57
+ Logger.info message
58
+ Logger.notify message do
59
+ current, count = 0, 10
60
+ queue.each_slice(count) do |dls|
61
+ current = current + count
62
+ Logger.info "#{message} #{Sync.counter[current - count, [current, queue.count].min, queue.count]}\r"
63
+ dls.map { |block| Thread.new(&block) }.map(&:join)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end end end
@@ -0,0 +1,52 @@
1
+ module Shoperb module Theme module Editor
2
+ module Sync
3
+ Pagination = Struct.new("Pagination", :response) do
4
+
5
+ def total
6
+ pagination["total"]
7
+ end
8
+
9
+ def limit
10
+ pagination["limit"]
11
+ end
12
+
13
+ def offset
14
+ pagination["offset"]
15
+ end
16
+
17
+ def to
18
+ [total, next_page * limit].min
19
+ end
20
+
21
+ def page
22
+ offset / limit + 1
23
+ end
24
+
25
+ def next_page
26
+ page + 1
27
+ end
28
+
29
+ def present?
30
+ response && header && total && offset && limit && next_page <= last_page
31
+ end
32
+
33
+ def last_page
34
+ (total.to_f / limit).ceil
35
+ end
36
+
37
+ def message
38
+ yield(offset + limit, to, total)
39
+ end
40
+
41
+ private
42
+
43
+ def pagination
44
+ JSON.parse(header)
45
+ end
46
+
47
+ def header
48
+ response.headers["x-pagination"]
49
+ end
50
+ end
51
+ end
52
+ end end end
@@ -0,0 +1,229 @@
1
+ require 'open-uri'
2
+
3
+ module Shoperb module Theme module Editor
4
+ module Sync
5
+ extend self
6
+
7
+ Editor.autoload_all self, "sync"
8
+
9
+ def vendors
10
+ process Mounter::Model::Vendor do |vendor|
11
+ vendor.delete("image")
12
+ vendor
13
+ end
14
+ end
15
+
16
+ def collections
17
+ process Mounter::Model::Collection
18
+ end
19
+
20
+ def countries
21
+ process Mounter::Model::Country
22
+ end
23
+
24
+ def states
25
+ process Mounter::Model::State do |state|
26
+ assign_relation state, Mounter::Model::Country
27
+ state
28
+ end
29
+ end
30
+
31
+ def pages
32
+ process Mounter::Model::Page
33
+ end
34
+
35
+ def blog_posts
36
+ process Mounter::Model::BlogCategory, 'blog-categories'
37
+ process Mounter::Model::BlogPost
38
+ end
39
+
40
+ def media_files
41
+ process Mounter::Model::MediaFile
42
+ end
43
+
44
+ def media_files
45
+ process Mounter::Model::MediaFile
46
+ end
47
+
48
+ def addresses(customer_ids)
49
+ process Mounter::Model::Address, where: "type:AddressWarehouse|AddressShop|AddressVendor|AddressSupplier"
50
+ lookup = "type:AddressCustomer"
51
+ if customer_ids.present?
52
+ lookup = "owner_type:Customer,owner_id:#{customer_ids}"
53
+ end
54
+ process Mounter::Model::Address, where: lookup
55
+ end
56
+
57
+ def images
58
+ Images.process
59
+ end
60
+
61
+ def menus
62
+ process Mounter::Model::Menu
63
+ end
64
+
65
+ def links
66
+ process Mounter::Model::Link do |link|
67
+ assign_relation link, Mounter::Model::Menu
68
+ link
69
+ end
70
+ end
71
+
72
+ def variants(ids)
73
+ process Mounter::Model::Variant, **product_ids_lookup(ids) do |variant|
74
+ assign_relation variant, Mounter::Model::Product
75
+ variant
76
+ end
77
+ end
78
+
79
+ def variant_attributes
80
+ variant_ids = Mounter::Model::Variant.select_map(:id)
81
+
82
+ klass = Mounter::Model::VariantAttribute
83
+ path = klass.to_s.demodulize.tableize
84
+ opts = {}
85
+ result = []
86
+
87
+ variant_ids.each_slice(50) do |list|
88
+ opts[:variant_id] = list
89
+ result += fetch("api/v1/#{path}", **opts).compact
90
+ end
91
+ uniq = result.uniq { |h| h[klass.primary_key.to_s] }
92
+ Logger.info "Received #{result.count} #{path.pluralize(result.count)}, kept #{uniq.count}.\n" if Editor["verbose"]
93
+ klass.assign uniq.map(&:to_hash)
94
+ end
95
+
96
+ def products(ids)
97
+ lookup = {}
98
+ lookup = {where: "id:#{ids}"} if ids.present?
99
+ process Mounter::Model::ProductType
100
+ process Mounter::Model::Category
101
+ process Mounter::Model::Product, **lookup do |product|
102
+ assign_relation product, Mounter::Model::Category
103
+ assign_relation product, Mounter::Model::ProductType
104
+ assign_relation product, Mounter::Model::Vendor
105
+ product.delete("images")
106
+ product
107
+ end
108
+ product_attributes(ids)
109
+ variants(ids)
110
+ variant_attributes
111
+ end
112
+
113
+ def product_attributes(ids)
114
+ process Mounter::Model::AttributeKey
115
+ process Mounter::Model::ProductAttribute, **product_ids_lookup(ids) do |product_attribute|
116
+ assign_relation product_attribute, Mounter::Model::Product
117
+ product_attribute
118
+ end
119
+ end
120
+
121
+ def product_ids_lookup(ids)
122
+ ids ? { where: "product_id:#{ids}" } : {}
123
+ end
124
+
125
+ def shop
126
+ process Mounter::Model::Currency
127
+ process Mounter::Model::Language
128
+ process Mounter::Model::Shop, "shop" do |shop|
129
+ assign_relation shop, Mounter::Model::Currency
130
+ assign_relation shop, Mounter::Model::Language
131
+ shop
132
+ end
133
+ end
134
+
135
+ def settings_data
136
+ data = get_response "api/v1/themes/#{Editor.handle}/settings_data", page: 1 rescue return
137
+
138
+ if data.body.presence
139
+ data = JSON.parse(data.body)
140
+ File.write('config/settings_data.json', JSON.pretty_generate(data))
141
+ end
142
+ end
143
+
144
+ def customers(ids)
145
+ lookup = {}
146
+ lookup = {where: "id:#{ids}"} if ids.present?
147
+ process Mounter::Model::Customer, **lookup
148
+ end
149
+
150
+ def customer_groups
151
+ process Mounter::Model::CustomerGroup
152
+ #process Mounter::Model::CustomerCustomerGroup
153
+ end
154
+
155
+ def reviews
156
+ process Mounter::Model::Review do |review|
157
+ assign_relation review, Mounter::Model::Product
158
+ assign_relation review, Mounter::Model::Customer
159
+ review
160
+ end
161
+ end
162
+
163
+ def discounts
164
+ process Mounter::Model::Discount
165
+ end
166
+
167
+ def custom_fields
168
+ process Mounter::Model::CustomField
169
+ end
170
+
171
+ def subcriptions
172
+ process Mounter::Model::CustomerSubscriptionPlan
173
+ process Mounter::Model::CustomerSubscription
174
+ end
175
+
176
+ def process klass, path=klass.to_s.demodulize.tableize, **opts, &block
177
+ result = fetch("api/v1/#{path}", **opts).map(&(block || ->(this){this})).compact
178
+ uniq = result.uniq { |h| h[klass.primary_key.to_s] }
179
+ Logger.info "Received #{result.count} #{path.pluralize(result.count)}, kept #{uniq.count}.\n" if Editor["verbose"]
180
+ klass.assign uniq.map(&:to_hash)
181
+ end
182
+
183
+ def counter
184
+ ->(from, to, total) { "(#{from} to #{to} of #{total})" }
185
+ end
186
+
187
+ private
188
+
189
+ def fetch path, message="Syncing #{path}", **opts
190
+ records, response, page = [], nil, nil
191
+ Logger.notify message do
192
+ begin
193
+ Logger.info "#{message} #{page.message(&counter)}\r" if page
194
+ response = get_response(path, **opts.merge(page: page))
195
+ records += Array.wrap(response.parsed)
196
+ end while (page = Pagination.new(response).presence)
197
+ end
198
+ records
199
+ end
200
+
201
+ def get_response path, **opts
202
+ page = opts[:page]
203
+ page = page.try(:next_page)
204
+ opts[:page] = page
205
+ Api.request path, method: :get, &as_json(**opts)
206
+ end
207
+
208
+ def as_json(params={})
209
+ ->(req) {
210
+ req.headers["Accept"] = "application/json"
211
+ req.headers['Current-Shop'] = Editor["oauth-site"]
212
+ req.options.timeout = 120
213
+ req.params = params
214
+ }
215
+ end
216
+
217
+ def assign_relation attributes, klass
218
+ name = klass.to_s.demodulize.underscore
219
+ id = attributes["#{name}_id"]
220
+ primary_key = klass.primary_key
221
+ # attributes["#{name}_#{primary_key}"] = klass.where(id: id).first.try(primary_key) if id
222
+
223
+ # apparently where(id: id) still queries not by ID, but by primary key, so:
224
+ attributes["#{name}_#{primary_key}"] = klass.all.detect { |record| record[:id] == id }.try(primary_key)
225
+ attributes["#{name}_#{primary_key}"] ||= (attributes["#{name}_id"] = id if id)
226
+ end
227
+
228
+ end
229
+ end end end
@@ -0,0 +1,22 @@
1
+ module Shoperb module Theme module Editor
2
+ module Translations
3
+ extend self
4
+ mattr_accessor :locale
5
+
6
+ def translate(string, locale: self.locale, **args)
7
+ key = string.parameterize(separator: ".")
8
+ (translations[locale.to_s] || HashWithIndifferentAccess.new).fetch(key) { key }
9
+ end
10
+
11
+ alias :t :translate
12
+
13
+ def translations
14
+ Dir[File.join("translations", "*.json")].map do |file|
15
+ JSON.parse(File.read(file).force_encoding('UTF-8').presence || "{}", object_class: HashWithIndifferentAccess)
16
+ end.inject({}) do |hash, trans|
17
+ hash.merge!(trans)
18
+ hash
19
+ end
20
+ end
21
+ end
22
+ end end end
@@ -0,0 +1,50 @@
1
+ module Shoperb module Theme module Editor
2
+ module Utils
3
+ extend self
4
+
5
+ mattr_accessor :path
6
+ self.path = Pathname.new("")
7
+
8
+ def rel_path path
9
+ path = Pathname.new(path) unless path.is_a?(Pathname)
10
+ path.relative_path_from(calc_base(path))
11
+ end
12
+
13
+ def calc_base path
14
+ path.absolute? ? base : Pathname.new("./")
15
+ end
16
+
17
+ def base
18
+ Pathname.new(Pathname.new(""))
19
+ end
20
+
21
+ def write_file target
22
+ File.open(target, "w+b") { |f| f.write(yield) }
23
+ end
24
+
25
+ def mk_tempfile content, *names
26
+ Tempfile.new(names).tap do |file|
27
+ file.write(content)
28
+ file.flush
29
+ file.open
30
+ end
31
+ end
32
+
33
+ def rm_tempfile(file)
34
+ return unless file
35
+ if defined?(Tempfile) && file.is_a?(Tempfile)
36
+ begin
37
+ file.close unless file.closed?
38
+ rescue IOError
39
+ end
40
+ begin
41
+ file.unlink
42
+ rescue Errno::ENOENT, IOError
43
+ end
44
+ else
45
+ path = file.respond_to?(:to_path) ? file.to_path : file.to_s
46
+ File.delete(path) if path && !path.empty? && File.exist?(path)
47
+ end
48
+ end
49
+ end
50
+ end end end
@@ -0,0 +1,159 @@
1
+ require "active_support/all"
2
+ require "haml"
3
+ require "rack"
4
+ require "pry"
5
+ require "shoperb_liquid"
6
+
7
+ require_relative 'shoperb_theme_editor/ext'
8
+
9
+ autoload :OpenStruct, "ostruct"
10
+ autoload :FileUtils, "fileutils"
11
+ autoload :Zip, "zip"
12
+ autoload :Pathname, "pathname"
13
+ autoload :Tempfile, "tempfile"
14
+ autoload :Time, "time"
15
+ autoload :OAuth2, "oauth2"
16
+ autoload :Faraday, "faraday"
17
+ autoload :Launchy, "launchy"
18
+ autoload :I18n, "i18n"
19
+ autoload :CoffeeScript, "coffee_script"
20
+ autoload :Sass, "sass"
21
+ autoload :ActionView, "action_view"
22
+ autoload :WEBrick, "webrick"
23
+ autoload :ActionDispatch, "action_dispatch"
24
+ autoload :Sinatra, "sinatra"
25
+ autoload :YAML, "yaml"
26
+ autoload :JSON, "json"
27
+ autoload :Logger, "logger"
28
+ autoload :URI, "uri"
29
+ autoload :Mime, "action_dispatch/http/mime_type"
30
+ autoload :Slop, "slop"
31
+ autoload :Sequel, "sequel"
32
+ require 'faraday/multipart'
33
+ require_relative 'shoperb_theme_editor/ext/sequel.rb'
34
+
35
+ module Shoperb module Theme
36
+ module Editor
37
+ extend self
38
+ mattr_accessor :config
39
+ delegate :[], :[]=, :reset, to: :config
40
+
41
+ def with_configuration(options, *args)
42
+ self.config = Configuration.new(options.to_hash.select { |_, value| !value.nil? }, *args)
43
+ `mkdir -p #{Utils.base + "data"}`
44
+ Sequel::Model.db = Sequel.sqlite((Utils.base + "data/data.db").to_s, setup_regexp_function: true)
45
+ begin
46
+ yield
47
+ rescue Interrupt => e
48
+ exit(130)
49
+ rescue Exception => e
50
+ Error.report(e)
51
+ ensure
52
+ self.config.save
53
+ end
54
+ end
55
+
56
+ def autoload_all mod, folder
57
+ Gem.find_files(
58
+ Pathname.new("shoperb_theme_editor/#{folder}/*.rb").cleanpath.to_s
59
+ ).each { |path|
60
+ name = Pathname.new(path).basename(".rb")
61
+ mod.autoload :"#{name.to_s.sub(/.*\./, '').camelize}",
62
+ Pathname.new("shoperb_theme_editor/#{folder}/#{name}").cleanpath.to_s
63
+ }
64
+ end
65
+
66
+ autoload_all self, "/"
67
+
68
+ def handle content=local_spec_content
69
+ spec(content)["handle"]
70
+ end
71
+
72
+ def spec content=local_spec_content
73
+ JSON.parse(content)
74
+ end
75
+
76
+ def settings_data
77
+ unless File.exist?('config/settings_data.json')
78
+ if File.exist?('presets/default.json')
79
+ File.open('config/settings_data.json', 'w') do |f|
80
+ settings = JSON.parse(File.read('presets/default.json'))['settings']
81
+ f.write JSON.pretty_generate({ general: settings })
82
+ end
83
+ end
84
+ end
85
+
86
+ if File.exist?('config/settings_data.json')
87
+ JSON.parse(File.read('config/settings_data.json'))
88
+ end
89
+ end
90
+
91
+ def local_spec_content
92
+ @local_spec_content ||= begin
93
+ if File.exist?(path = Utils.base + "config/spec.json")
94
+ File.read(path)
95
+ else
96
+ new_spec_content
97
+ end
98
+ end
99
+ end
100
+
101
+ # if spec is missing (for old themes)
102
+ def new_spec_content
103
+ content = JSON.parse(File.read(path = Utils.base + '.shoperb'))
104
+ spec_content = {
105
+ handle: content['handle'],
106
+ compile: {
107
+ stylesheets: ['application.css'],
108
+ javascripts: ['application.js']
109
+ }
110
+ }.to_json
111
+ Dir.mkdir 'config' unless File.exist?('config')
112
+ File.open('config/spec.json', 'w') {|f| f.write(spec_content) }
113
+ spec_content
114
+ end
115
+
116
+ # general theme settings (styles)
117
+ def theme_settings
118
+ (settings_data ? settings_data['general'].presence : nil) || {}
119
+ end
120
+
121
+ def presets
122
+ res = []
123
+
124
+ Pathname.glob(Utils.base + "presets/*.json") do |path|
125
+ content = File.read(path)
126
+ data = JSON.parse(content) rescue { "settings" => { } }
127
+
128
+ res.push([nil, data["settings"]]) if data["default"]
129
+ res.push([data["name_key"], data["settings"]])
130
+ end
131
+
132
+ res.to_h
133
+ end
134
+
135
+ def compiler asset_url, digests: true, **options
136
+ Artisans::ThemeCompiler.new(
137
+ File.expand_path(Utils.base),
138
+ asset_url,
139
+ settings: theme_settings,
140
+ compile: spec["compile"],
141
+ file_reader: SprocketsFileReader.new(digests: digests)
142
+ )
143
+ end
144
+
145
+ class SprocketsFileReader
146
+ def initialize(digests: true)
147
+ @digests = digests
148
+ end
149
+
150
+ def read(file)
151
+ File.read(file) if File.file?(file)
152
+ end
153
+
154
+ def find_digest(path)
155
+ @digests && File.exist?(path) ? Digest::MD5.hexdigest(File.read(path)) : ''
156
+ end
157
+ end
158
+ end
159
+ end end