shopify-cli 2.24.0 → 2.25.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/lib/project_types/extension/commands/serve.rb +57 -3
  6. data/lib/project_types/extension/extension_project.rb +8 -1
  7. data/lib/project_types/extension/loaders/project.rb +3 -2
  8. data/lib/project_types/extension/messages/messages.rb +21 -6
  9. data/lib/project_types/extension/models/server_config/development_renderer.rb +1 -1
  10. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +18 -6
  11. data/lib/project_types/theme/commands/serve.rb +15 -3
  12. data/lib/project_types/theme/messages/messages.rb +4 -2
  13. data/lib/shopify_cli/commands/logout.rb +13 -2
  14. data/lib/shopify_cli/environment.rb +1 -1
  15. data/lib/shopify_cli/file_system_listener.rb +30 -0
  16. data/lib/shopify_cli/git.rb +116 -33
  17. data/lib/shopify_cli/identity_auth.rb +1 -0
  18. data/lib/shopify_cli/project.rb +1 -1
  19. data/lib/shopify_cli/tasks/ensure_project_type.rb +3 -1
  20. data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +1 -1
  21. data/lib/shopify_cli/theme/dev_server/certificate_manager.rb +1 -1
  22. data/lib/shopify_cli/theme/dev_server/errors.rb +9 -0
  23. data/lib/shopify_cli/theme/dev_server/header_hash.rb +1 -1
  24. data/lib/shopify_cli/theme/dev_server/hooks/file_change_hook.rb +77 -0
  25. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_deleter.rb +1 -1
  26. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  27. data/lib/shopify_cli/theme/dev_server/{hot-reload-no-script.html → hot_reload/resources/hot-reload-no-script.html} +0 -0
  28. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/hot_reload.js +48 -0
  29. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/sse_client.js +43 -0
  30. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme.js +114 -0
  31. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme_extension.js +121 -0
  32. data/lib/shopify_cli/theme/dev_server/hot_reload/script_injector.rb +57 -0
  33. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +1 -1
  34. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +8 -76
  35. data/lib/shopify_cli/theme/dev_server/local_assets.rb +28 -28
  36. data/lib/shopify_cli/theme/dev_server/proxy.rb +33 -25
  37. data/lib/shopify_cli/theme/dev_server/proxy_param_builder.rb +82 -0
  38. data/lib/shopify_cli/theme/dev_server/reload_mode.rb +1 -1
  39. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +1 -1
  40. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +1 -1
  41. data/lib/shopify_cli/theme/dev_server/sse.rb +1 -1
  42. data/lib/shopify_cli/theme/dev_server/watcher.rb +8 -9
  43. data/lib/shopify_cli/theme/dev_server/web_server.rb +1 -1
  44. data/lib/shopify_cli/theme/dev_server.rb +287 -99
  45. data/lib/shopify_cli/theme/extension/app_extension.rb +40 -0
  46. data/lib/shopify_cli/theme/extension/dev_server/hooks/file_change_hook.rb +68 -0
  47. data/lib/shopify_cli/theme/extension/dev_server/hot_reload/script_injector.rb +30 -0
  48. data/lib/shopify_cli/theme/extension/dev_server/hot_reload.rb +13 -0
  49. data/lib/shopify_cli/theme/extension/dev_server/local_assets.rb +30 -0
  50. data/lib/shopify_cli/theme/{dev_server/proxy/template_param_builder.rb → extension/dev_server/proxy_param_builder.rb} +26 -16
  51. data/lib/shopify_cli/theme/extension/dev_server/watcher.rb +47 -0
  52. data/lib/shopify_cli/theme/extension/dev_server.rb +150 -0
  53. data/lib/shopify_cli/theme/extension/host_theme.rb +104 -0
  54. data/lib/shopify_cli/theme/extension/syncer/extension_serve_job.rb +133 -0
  55. data/lib/shopify_cli/theme/extension/syncer/operation.rb +21 -0
  56. data/lib/shopify_cli/theme/extension/syncer.rb +81 -0
  57. data/lib/shopify_cli/theme/extension/ui/host_theme_progress_bar.rb +35 -0
  58. data/lib/shopify_cli/theme/ignore_helper.rb +31 -0
  59. data/lib/shopify_cli/theme/root.rb +62 -0
  60. data/lib/shopify_cli/theme/syncer.rb +12 -6
  61. data/lib/shopify_cli/theme/theme.rb +10 -52
  62. data/lib/shopify_cli/version.rb +1 -1
  63. metadata +27 -7
  64. data/.github/workflows/triage.yml +0 -22
  65. data/lib/shopify_cli/theme/dev_server/hot-reload.js +0 -194
  66. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +0 -33
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/file_system_listener"
4
+ require "shopify_cli/theme/dev_server"
5
+ require "forwardable"
6
+
7
+ module ShopifyCLI
8
+ module Theme
9
+ module Extension
10
+ class DevServer < ShopifyCLI::Theme::DevServer
11
+ # Watches for file changes and publish events to the theme
12
+ class Watcher
13
+ extend Forwardable
14
+
15
+ def_delegators :@listener, :add_observer, :changed, :notify_observers
16
+
17
+ def initialize(ctx, extension:, syncer:, poll: false)
18
+ @ctx = ctx
19
+ @extension = extension
20
+ @syncer = syncer
21
+ @listener = FileSystemListener.new(root: @extension.root.to_s, force_poll: poll, ignore_regex: nil)
22
+
23
+ add_observer(self, :notify_updates)
24
+ end
25
+
26
+ def start
27
+ @listener.start
28
+ end
29
+
30
+ def stop
31
+ @listener.stop
32
+ end
33
+
34
+ def notify_updates(modified, added, removed)
35
+ @syncer.enqueue_updates(files(modified).select { |file| @extension.extension_file?(file) })
36
+ @syncer.enqueue_creates(files(added).select { |file| @extension.extension_file?(file) })
37
+ @syncer.enqueue_deletes(files(removed))
38
+ end
39
+
40
+ def files(paths)
41
+ paths.map { |file| @extension[file] }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ require "shopify_cli/theme/extension/app_extension"
6
+ require "shopify_cli/theme/dev_server"
7
+ require "shopify_cli/theme/extension/host_theme"
8
+ require "shopify_cli/theme/syncer"
9
+
10
+ require_relative "dev_server/local_assets"
11
+ require_relative "dev_server/proxy_param_builder"
12
+ require_relative "dev_server/watcher"
13
+ require_relative "dev_server/hooks/file_change_hook"
14
+ require_relative "dev_server/hot_reload"
15
+ require_relative "dev_server/hot_reload/script_injector"
16
+ require_relative "syncer"
17
+
18
+ module ShopifyCLI
19
+ module Theme
20
+ module Extension
21
+ class DevServer < ShopifyCLI::Theme::DevServer
22
+ # Themes
23
+ Proxy = ShopifyCLI::Theme::DevServer::Proxy
24
+ CdnFonts = ShopifyCLI::Theme::DevServer::CdnFonts
25
+
26
+ # Extensions
27
+ ScriptInjector = ShopifyCLI::Theme::Extension::DevServer::HotReload::ScriptInjector
28
+
29
+ attr_accessor :project, :specification_handler
30
+
31
+ class << self
32
+ def start(ctx, root, port: 9292, theme: nil, project:, specification_handler:)
33
+ instance.project = project
34
+ instance.specification_handler = specification_handler
35
+
36
+ super(ctx, root, port: port, theme: theme)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def middleware_stack
43
+ @app = Proxy.new(ctx, theme, param_builder)
44
+ @app = CdnFonts.new(@app, theme: theme)
45
+ @app = LocalAssets.new(ctx, @app, extension)
46
+ @app = HotReload.new(ctx, @app, broadcast_hooks: broadcast_hooks, watcher: watcher, mode: mode,
47
+ script_injector: script_injector)
48
+ end
49
+
50
+ def sync_theme
51
+ # Ensures the host theme exists
52
+ theme
53
+
54
+ # Ensure the theme app extension is pushed
55
+ extension
56
+ end
57
+
58
+ def syncer
59
+ @syncer ||= Syncer.new(
60
+ ctx,
61
+ extension: extension,
62
+ project: project,
63
+ specification_handler: specification_handler
64
+ )
65
+ end
66
+
67
+ def theme
68
+ @theme ||= if theme_identifier
69
+ theme = ShopifyCLI::Theme::Theme.find_by_identifier(ctx, identifier: theme_identifier)
70
+ theme || ctx.abort(not_found_error_message)
71
+ else
72
+ HostTheme.find_or_create!(ctx)
73
+ end
74
+ end
75
+
76
+ def extension
77
+ return @extension if @extension
78
+
79
+ app = fetch_theme_app_extension_info.dig(*%w(data app)) || {}
80
+
81
+ app_id = app["id"]
82
+
83
+ registrations = app["extensionRegistrations"] || []
84
+ registration = registrations.find { |r| r["type"] == "THEME_APP_EXTENSION" } || {}
85
+
86
+ location = registration.dig(*%w(draftVersion location))
87
+ registration_id = registration["id"]
88
+
89
+ @extension = AppExtension.new(
90
+ ctx,
91
+ root: root,
92
+ app_id: app_id,
93
+ location: location,
94
+ registration_id: registration_id,
95
+ )
96
+ end
97
+
98
+ def fetch_theme_app_extension_info
99
+ params = {
100
+ api_key: project.app.api_key,
101
+ type: specification_handler.identifier.downcase,
102
+ }
103
+
104
+ PartnersAPI.query(@ctx, "get_extension_registrations", **params)
105
+ end
106
+
107
+ def watcher
108
+ @watcher ||= Watcher.new(ctx, syncer: syncer, extension: extension, poll: poll)
109
+ end
110
+
111
+ def param_builder
112
+ @param_builder ||= ProxyParamBuilder
113
+ .new
114
+ .with_extension(extension)
115
+ .with_syncer(syncer)
116
+ end
117
+
118
+ def setup_server
119
+ CLI::UI::Frame.open(frame_title, color: :magenta, timing: nil) do
120
+ ctx.puts(preview_message)
121
+ end
122
+
123
+ watcher.start
124
+ syncer.start
125
+ end
126
+
127
+ # Hooks
128
+
129
+ def broadcast_hooks
130
+ file_handler = Hooks::FileChangeHook.new(ctx, extension: extension, syncer: syncer)
131
+ [file_handler]
132
+ end
133
+
134
+ def script_injector
135
+ ScriptInjector.new(ctx)
136
+ end
137
+
138
+ # Messages
139
+
140
+ def frame_title
141
+ ctx.message("serve.frame_title", root)
142
+ end
143
+
144
+ def preview_message
145
+ ctx.message("serve.preview_message", extension.location, theme.editor_url, address)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "../syncer"
5
+ require_relative "../development_theme"
6
+ require "shopify_cli/git"
7
+ require "shopify_cli/theme/extension/ui/host_theme_progress_bar"
8
+ require "tmpdir"
9
+
10
+ require "shopify_cli/git"
11
+ require "shopify_cli/theme/development_theme"
12
+ require "shopify_cli/theme/syncer"
13
+
14
+ module ShopifyCLI
15
+ module Theme
16
+ module Extension
17
+ class HostTheme < DevelopmentTheme
18
+ def id
19
+ ShopifyCLI::DB.get(:host_theme_id)
20
+ end
21
+
22
+ def name
23
+ existing_name = ShopifyCLI::DB.get(:host_theme_name)
24
+ if existing_name.nil? || existing_name.length > API_NAME_LIMIT
25
+ generate_host_theme_name
26
+ else
27
+ existing_name
28
+ end
29
+ end
30
+
31
+ def ensure_exists!
32
+ if exists?
33
+ @ctx.debug("Using temporary host theme: ##{id} #{name}")
34
+ else
35
+ create
36
+ @ctx.debug("Created temporary host theme: #{@id}")
37
+ end
38
+
39
+ self
40
+ end
41
+
42
+ def self.delete(ctx)
43
+ new(ctx, root: nil).delete
44
+ end
45
+
46
+ def delete
47
+ delete_theme if exists? # Avoid deleting any existing development theme logic
48
+
49
+ ShopifyCLI::DB.del(:host_theme_id) if ShopifyCLI::DB.exists?(:host_theme_id)
50
+ ShopifyCLI::DB.del(:host_theme_name) if ShopifyCLI::DB.exists?(:host_theme_name)
51
+ end
52
+
53
+ def create
54
+ super
55
+ ShopifyCLI::DB.set(host_theme_id: @id)
56
+
57
+ generate_tmp_theme
58
+ end
59
+
60
+ def self.find_or_create!(ctx)
61
+ new(ctx, root: nil).ensure_exists!
62
+ end
63
+
64
+ private
65
+
66
+ def generate_host_theme_name
67
+ hostname = Socket.gethostname.split(".").shift
68
+ hash = SecureRandom.hex(3)
69
+
70
+ theme_name = "App Ext. Host ()"
71
+ hostname_character_limit = API_NAME_LIMIT - theme_name.length - hash.length - 1
72
+ identifier = encode_identifier("#{hash}-#{hostname[0, hostname_character_limit]}")
73
+ theme_name = "App Ext. Host (#{identifier})"
74
+
75
+ ShopifyCLI::DB.set(host_theme_name: theme_name)
76
+
77
+ theme_name
78
+ end
79
+
80
+ def generate_tmp_theme
81
+ ctx = @ctx.dup
82
+
83
+ Dir.mktmpdir do |dir|
84
+ @root = Pathname.new(dir)
85
+ ctx.root = dir
86
+
87
+ syncer = ShopifyCLI::Theme::Syncer.new(ctx, theme: self)
88
+
89
+ begin
90
+ syncer.start_threads
91
+ ::CLI::UI::Frame.open(ctx.message("theme.push.info.pushing", name, id, shop)) do
92
+ UI::HostThemeProgressBar.new(syncer, dir).progress(:upload_theme!, delete: false)
93
+ end
94
+ rescue Errno::ENOENT => e
95
+ ctx.debug(e.message)
96
+ ensure
97
+ syncer.shutdown
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+ require "project_types/extension/loaders/project"
3
+ require "project_types/extension/loaders/specification_handler"
4
+ require "shopify_cli/partners_api"
5
+ require "shopify_cli/thread_pool/job"
6
+
7
+ module ShopifyCLI
8
+ module Theme
9
+ module Extension
10
+ class Syncer
11
+ class ExtensionServeJob < ThreadPool::Job
12
+ POLL_FREQUENCY = 0.5 # second
13
+ PUSH_INTERVAL = 5 # seconds
14
+
15
+ RESPONSE_FIELD = %w(data extensionUpdateDraft)
16
+ VERSION_FIELD = "extensionVersion"
17
+ USER_ERRORS_FIELD = "userErrors"
18
+ ERROR_FILE_REGEX = /\[([^\]\[]*)\]/
19
+
20
+ def initialize(ctx, syncer:, extension:, project:, specification_handler:)
21
+ super(POLL_FREQUENCY)
22
+
23
+ @ctx = ctx
24
+ @extension = extension
25
+ @project = project
26
+ @specification_handler = specification_handler
27
+
28
+ @syncer = syncer
29
+ @syncer_mutex = Mutex.new
30
+
31
+ @job_in_progress = false
32
+ @job_in_progress_mutex = Mutex.new
33
+ end
34
+
35
+ def perform!
36
+ return unless @syncer.any_operation?
37
+ return if job_in_progress?
38
+ return if recently_synced? && !@syncer.any_blocking_operation?
39
+
40
+ job_in_progress!
41
+
42
+ input = {
43
+ api_key: @project.app.api_key,
44
+ registration_id: @project.registration_id,
45
+ config: JSON.generate(@specification_handler.config(@ctx)),
46
+ extension_context: @specification_handler.extension_context(@ctx),
47
+ }
48
+ response = ShopifyCLI::PartnersAPI.query(@ctx, "extension_update_draft", **input).dig(*RESPONSE_FIELD)
49
+ user_errors = response.dig(USER_ERRORS_FIELD)
50
+
51
+ if user_errors
52
+ @ctx.puts(error_message(@project.title))
53
+ error_files = erroneous_files(user_errors)
54
+ print_items(error_files)
55
+ else
56
+ @ctx.puts(success_message(@project.title))
57
+ print_items({}.freeze)
58
+ ::Extension::Tasks::Converters::VersionConverter.from_hash(@ctx, response.dig(VERSION_FIELD))
59
+ end
60
+
61
+ @syncer_mutex.synchronize do
62
+ @syncer.pending_operations.clear
63
+ @syncer.latest_sync = Time.now
64
+ end
65
+
66
+ ensure
67
+ job_in_progress!(false)
68
+ end
69
+
70
+ private
71
+
72
+ def job_in_progress!(in_progress = true)
73
+ @job_in_progress_mutex.synchronize { @job_in_progress = in_progress }
74
+ end
75
+
76
+ def job_in_progress?
77
+ @job_in_progress
78
+ end
79
+
80
+ def recently_synced?
81
+ Time.now - @syncer.latest_sync < PUSH_INTERVAL
82
+ end
83
+
84
+ def timestamp
85
+ Time.now.strftime("%T")
86
+ end
87
+
88
+ def success_message(project)
89
+ "#{timestamp} {{green:Pushed}} {{>}} {{blue:'#{project}'}} to a draft"
90
+ end
91
+
92
+ def error_message(project)
93
+ "#{timestamp} {{red:Error}} {{>}} {{blue:'#{project}'}} could not be pushed:"
94
+ end
95
+
96
+ def print_file_success(file)
97
+ @ctx.puts("{{blue:- #{file.relative_path}}}")
98
+ end
99
+
100
+ def print_file_error(file, err)
101
+ @ctx.puts("{{red:- #{file.relative_path}}}")
102
+ @ctx.puts("{{red: - Cause: #{err}}}")
103
+ end
104
+
105
+ def erroneous_files(errors)
106
+ files = {}
107
+ errors.each do |e|
108
+ path = e["message"][ERROR_FILE_REGEX, 1]
109
+ file = @extension[path]
110
+ files[file] = e["message"]
111
+ end
112
+ files
113
+ end
114
+
115
+ def print_items(erroneous_files)
116
+ @syncer.pending_files.each do |file|
117
+ err = erroneous_files.dig(file)
118
+ if err
119
+ print_file_error(file, err)
120
+ erroneous_files.delete(file)
121
+ else
122
+ print_file_success(file)
123
+ end
124
+ end
125
+ erroneous_files.each do |file, err|
126
+ print_file_error(file, err)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ module Extension
8
+ class Syncer
9
+ class Operation < Struct.new(:file, :kind)
10
+ def delete?
11
+ kind == :delete
12
+ end
13
+
14
+ def create?
15
+ kind == :create
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "syncer/extension_serve_job"
4
+ require_relative "syncer/operation"
5
+
6
+ require "shopify_cli/thread_pool"
7
+
8
+ module ShopifyCLI
9
+ module Theme
10
+ module Extension
11
+ class Syncer
12
+ attr_accessor :pending_operations, :latest_sync
13
+
14
+ def initialize(ctx, extension:, project:, specification_handler:)
15
+ @ctx = ctx
16
+ @extension = extension
17
+ @project = project
18
+ @specification_handler = specification_handler
19
+
20
+ @pool = ThreadPool.new(pool_size: 1)
21
+ @pending_operations = extension.extension_files.map { |file| Operation.new(file, :update) }
22
+ @pending_operations_mutex = Mutex.new
23
+ @latest_sync = Time.now - ExtensionServeJob::PUSH_INTERVAL
24
+ end
25
+
26
+ def enqueue_creates(files)
27
+ operations = files.map { |file| Operation.new(file, :create) }
28
+ enqueue_operations(operations)
29
+ end
30
+
31
+ def enqueue_updates(files)
32
+ operations = files.map { |file| Operation.new(file, :update) }
33
+ enqueue_operations(operations)
34
+ end
35
+
36
+ def enqueue_deletes(files)
37
+ operations = files.map { |file| Operation.new(file, :delete) }
38
+ enqueue_operations(operations)
39
+ end
40
+
41
+ def start
42
+ @pool.schedule(job)
43
+ end
44
+
45
+ def shutdown
46
+ @pool.shutdown
47
+ end
48
+
49
+ def pending_files
50
+ pending_operations.map(&:file)
51
+ end
52
+
53
+ def any_operation?
54
+ pending_operations.any?
55
+ end
56
+
57
+ def any_blocking_operation?
58
+ pending_operations.any? { |operation| operation.delete? || operation.create? }
59
+ end
60
+
61
+ private
62
+
63
+ def enqueue_operations(operations)
64
+ @pending_operations_mutex.synchronize do
65
+ operations.each { |f| @pending_operations << f unless @pending_operations.include?(f) }
66
+ end
67
+ end
68
+
69
+ def job
70
+ ExtensionServeJob.new(
71
+ @ctx,
72
+ syncer: self,
73
+ extension: @extension,
74
+ project: @project,
75
+ specification_handler: @specification_handler
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,35 @@
1
+
2
+ module ShopifyCLI
3
+ module Theme
4
+ module Extension
5
+ module UI
6
+ class HostThemeProgressBar
7
+ GIT_CLONE_PROGRESS_SHARE = 0.2
8
+ SYNC_PROGRESS_SHARE = 0.8
9
+
10
+ def initialize(syncer, dir)
11
+ @syncer = syncer
12
+ @dir = dir
13
+ end
14
+
15
+ def progress(method, **args)
16
+ @syncer.lock_io!
17
+ CLI::UI::Progress.progress do |bar|
18
+ Git.public_send(:raw_clone, "https://github.com/Shopify/dawn.git", @dir) do |percent|
19
+ bar.tick(set_percent: percent * GIT_CLONE_PROGRESS_SHARE)
20
+ end
21
+
22
+ @syncer.public_send(method, **args) do |left, total|
23
+ next if total == 0
24
+ bar.tick(set_percent: (1 - left.to_f / total) * SYNC_PROGRESS_SHARE + GIT_CLONE_PROGRESS_SHARE)
25
+ end
26
+
27
+ bar.tick(set_percent: 1)
28
+ end
29
+ @syncer.unlock_io!
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ module IgnoreHelper
6
+ def ignore_operation?(operation)
7
+ path = operation.file_path
8
+ ignore_path?(path)
9
+ end
10
+
11
+ def ignore_file?(file)
12
+ path = file.relative_path
13
+ ignore_path?(path)
14
+ end
15
+
16
+ def ignore_path?(path)
17
+ ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
18
+ end
19
+
20
+ private
21
+
22
+ def ignored_by_ignore_filter?(path)
23
+ ignore_filter&.ignore?(path)
24
+ end
25
+
26
+ def ignored_by_include_filter?(path)
27
+ !!include_filter && !include_filter.match?(path)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ require_relative "file"
3
+ require "pathname"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Root
8
+ attr_reader :root, :ctx
9
+
10
+ def initialize(ctx, root:)
11
+ @ctx = ctx
12
+ @root = Pathname.new(root) if root
13
+ end
14
+
15
+ def static_asset_files
16
+ glob("assets/*", raise_on_dir: true).reject(&:liquid?)
17
+ end
18
+
19
+ def liquid_files
20
+ glob("**/*.liquid")
21
+ end
22
+
23
+ def json_files
24
+ glob("**/*.json")
25
+ end
26
+
27
+ def glob(pattern, raise_on_dir: false)
28
+ root
29
+ .glob(pattern)
30
+ .select { |path| file?(path, raise_on_dir) }
31
+ .map { |path| File.new(path, root) }
32
+ end
33
+
34
+ def static_asset_file?(file)
35
+ static_asset_files.include?(self[file])
36
+ end
37
+
38
+ def static_asset_paths
39
+ static_asset_files.map(&:relative_path)
40
+ end
41
+
42
+ def [](file)
43
+ case file
44
+ when File
45
+ file
46
+ when Pathname
47
+ File.new(file, root)
48
+ when String
49
+ File.new(root.join(file), root)
50
+ end
51
+ end
52
+
53
+ def file?(path, raise_on_dir = false)
54
+ if raise_on_dir && ::File.directory?(path)
55
+ @ctx.abort(@ctx.message("theme.serve.error.invalid_subdirectory", path.to_s))
56
+ end
57
+
58
+ ::File.file?(path)
59
+ end
60
+ end
61
+ end
62
+ end