shopify-cli 2.24.0 → 2.25.0

Sign up to get free protection for your applications and to get access to all the features.
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