shopify-cli 1.7.0 → 1.10.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +28 -0
  3. data/.rubocop_todo.yml +15 -2
  4. data/CHANGELOG.md +21 -0
  5. data/Gemfile.lock +14 -14
  6. data/README.md +2 -1
  7. data/dev.yml +3 -0
  8. data/lib/docgen/markdown.rb +1 -1
  9. data/lib/graphql/extension_create.graphql +17 -2
  10. data/lib/graphql/fetch_specifications.graphql +14 -0
  11. data/lib/project_types/extension/cli.rb +4 -10
  12. data/lib/project_types/extension/commands/create.rb +2 -2
  13. data/lib/project_types/extension/commands/extension_command.rb +10 -6
  14. data/lib/project_types/extension/commands/push.rb +2 -2
  15. data/lib/project_types/extension/commands/register.rb +6 -5
  16. data/lib/project_types/extension/commands/serve.rb +1 -7
  17. data/lib/project_types/extension/extension_project.rb +19 -4
  18. data/lib/project_types/extension/extension_project_keys.rb +2 -1
  19. data/lib/project_types/extension/features/argo.rb +18 -34
  20. data/lib/project_types/extension/features/argo_renderer_package.rb +47 -0
  21. data/lib/project_types/extension/features/argo_serve.rb +69 -0
  22. data/lib/project_types/extension/features/argo_setup.rb +1 -1
  23. data/lib/project_types/extension/forms/questions/ask_type.rb +16 -5
  24. data/lib/project_types/extension/messages/message_loading.rb +3 -1
  25. data/lib/project_types/extension/messages/messages.rb +6 -6
  26. data/lib/project_types/extension/models/registration.rb +1 -0
  27. data/lib/project_types/extension/models/specification.rb +6 -2
  28. data/lib/project_types/extension/models/specification_handlers/default.rb +9 -1
  29. data/lib/project_types/extension/models/specifications.rb +12 -1
  30. data/lib/project_types/extension/models/version.rb +1 -1
  31. data/lib/project_types/extension/tasks/configure_features.rb +3 -1
  32. data/lib/project_types/extension/tasks/converters/registration_converter.rb +2 -0
  33. data/lib/project_types/extension/tasks/fetch_specifications.rb +8 -28
  34. data/lib/project_types/node/commands/generate.rb +0 -22
  35. data/lib/project_types/node/forms/create.rb +10 -1
  36. data/lib/project_types/node/messages/messages.rb +5 -4
  37. data/lib/project_types/rails/forms/create.rb +11 -1
  38. data/lib/project_types/rails/messages/messages.rb +5 -4
  39. data/lib/project_types/script/cli.rb +7 -7
  40. data/lib/project_types/script/commands/create.rb +2 -7
  41. data/lib/project_types/script/commands/push.rb +3 -3
  42. data/lib/project_types/script/config/extension_points.yml +27 -10
  43. data/lib/project_types/script/errors.rb +0 -35
  44. data/lib/project_types/script/forms/create.rb +3 -14
  45. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +5 -5
  46. data/lib/project_types/script/graphql/get_app_scripts.graphql +6 -0
  47. data/lib/project_types/script/graphql/script_service_proxy.graphql +1 -2
  48. data/lib/project_types/script/layers/application/create_script.rb +32 -22
  49. data/lib/project_types/script/layers/application/extension_points.rb +3 -2
  50. data/lib/project_types/script/layers/application/push_script.rb +6 -3
  51. data/lib/project_types/script/layers/domain/config_ui.rb +16 -0
  52. data/lib/project_types/script/layers/domain/errors.rb +16 -0
  53. data/lib/project_types/script/layers/domain/extension_point.rb +60 -45
  54. data/lib/project_types/script/layers/domain/metadata.rb +18 -25
  55. data/lib/project_types/script/layers/domain/push_package.rb +4 -4
  56. data/lib/project_types/script/layers/domain/script_project.rb +54 -0
  57. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +37 -8
  58. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +5 -40
  59. data/lib/project_types/script/layers/infrastructure/errors.rb +50 -19
  60. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +8 -9
  61. data/lib/project_types/script/layers/infrastructure/rust_task_runner.rb +1 -1
  62. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +198 -0
  63. data/lib/project_types/script/layers/infrastructure/script_service.rb +27 -66
  64. data/lib/project_types/script/messages/messages.rb +28 -51
  65. data/lib/project_types/script/tasks/ensure_env.rb +77 -0
  66. data/lib/project_types/script/ui/error_handler.rb +63 -47
  67. data/lib/shopify-cli/context.rb +28 -0
  68. data/lib/shopify-cli/js_system.rb +2 -2
  69. data/lib/shopify-cli/messages/messages.rb +3 -2
  70. data/lib/shopify-cli/method_object.rb +1 -1
  71. data/lib/shopify-cli/oauth.rb +2 -2
  72. data/lib/shopify-cli/packager.rb +1 -1
  73. data/lib/shopify-cli/resolve_constant.rb +1 -1
  74. data/lib/shopify-cli/resources/env_file.rb +1 -1
  75. data/lib/shopify-cli/result.rb +3 -3
  76. data/lib/shopify-cli/tasks/ensure_dev_store.rb +1 -1
  77. data/lib/shopify-cli/transform_data_structure.rb +2 -2
  78. data/lib/shopify-cli/version.rb +1 -1
  79. data/lib/shopify_cli.rb +0 -1
  80. data/vendor/deps/smart_properties/REVISION +1 -1
  81. data/vendor/deps/smart_properties/lib/smart_properties/property.rb +7 -1
  82. data/vendor/deps/smart_properties/lib/smart_properties/version.rb +1 -1
  83. metadata +11 -10
  84. data/.travis.yml +0 -14
  85. data/lib/project_types/script/commands/disable.rb +0 -25
  86. data/lib/project_types/script/commands/enable.rb +0 -80
  87. data/lib/project_types/script/graphql/shop_script_delete.graphql +0 -14
  88. data/lib/project_types/script/graphql/shop_script_update_or_create.graphql +0 -28
  89. data/lib/project_types/script/layers/application/disable_script.rb +0 -21
  90. data/lib/project_types/script/layers/application/enable_script.rb +0 -23
  91. data/lib/project_types/script/script_project.rb +0 -85
@@ -11,7 +11,10 @@ module Script
11
11
  property! :path_to_project, accepts: String
12
12
 
13
13
  BOOTSTRAP = "npx --no-install shopify-scripts-toolchain-as bootstrap --from %{extension_point} --dest %{base}"
14
+ BUILD = "shopify-scripts-toolchain-as build --src src/shopify_main.ts " \
15
+ "--binary build/%{script_name}.wasm --metadata build/metadata.json"
14
16
  MIN_NODE_VERSION = "14.5.0"
17
+ ASC_ARGS = "-- --lib node_modules --optimize --use Date="
15
18
 
16
19
  def setup_dependencies
17
20
  write_npmrc
@@ -19,8 +22,7 @@ module Script
19
22
  end
20
23
 
21
24
  def bootstrap
22
- type = extension_point.type.gsub("_", "-")
23
- out, status = ctx.capture2e(format(BOOTSTRAP, extension_point: type, base: path_to_project))
25
+ out, status = ctx.capture2e(bootstap_command)
24
26
  raise Domain::Errors::ServiceFailureError, out unless status.success?
25
27
  end
26
28
 
@@ -36,9 +38,13 @@ module Script
36
38
  end
37
39
 
38
40
  def extension_point_version
41
+ if extension_point.sdks.assemblyscript.versioned?
42
+ return extension_point.sdks.assemblyscript.version
43
+ end
44
+
39
45
  out, status = ctx.capture2e("npm show #{extension_point.sdks.assemblyscript.package} version --json")
40
46
  raise Domain::Errors::ServiceFailureError, out unless status.success?
41
- JSON.parse(out)
47
+ "^#{JSON.parse(out)}"
42
48
  end
43
49
 
44
50
  def write_package_json
@@ -49,14 +55,13 @@ module Script
49
55
  "devDependencies": {
50
56
  "@shopify/scripts-sdk-as": "#{extension_point.sdks.assemblyscript.sdk_version}",
51
57
  "@shopify/scripts-toolchain-as": "#{extension_point.sdks.assemblyscript.toolchain_version}",
52
- "#{extension_point.sdks.assemblyscript.package}": "^#{extension_point_version}",
53
- "@as-pect/cli": "4.0.0",
54
- "as-wasi": "^0.2.1",
55
- "assemblyscript": "^0.16.1"
58
+ "#{extension_point.sdks.assemblyscript.package}": "#{extension_point_version}",
59
+ "@as-pect/cli": "^6.0.0",
60
+ "assemblyscript": "^0.18.13"
56
61
  },
57
62
  "scripts": {
58
63
  "test": "asp --summary --verbose",
59
- "build": "shopify-scripts-toolchain-as build --src src/script.ts --binary build/#{script_name}.wasm --metadata build/metadata.json -- --lib node_modules --optimize --use Date="
64
+ "build": "#{build_command}"
60
65
  },
61
66
  "engines": {
62
67
  "node": ">=#{MIN_NODE_VERSION}"
@@ -65,6 +70,30 @@ module Script
65
70
  HERE
66
71
  ctx.write("package.json", package_json)
67
72
  end
73
+
74
+ def bootstap_command
75
+ type = extension_point.dasherize_type
76
+ base_command = format(BOOTSTRAP, extension_point: type, base: path_to_project)
77
+ domain = extension_point.domain
78
+
79
+ if domain.nil?
80
+ base_command
81
+ else
82
+ "#{base_command} --domain #{domain}"
83
+ end
84
+ end
85
+
86
+ def build_command
87
+ type = extension_point.dasherize_type
88
+ base_command = format(BUILD, script_name: script_name)
89
+ domain = extension_point.domain
90
+
91
+ if domain.nil?
92
+ "#{base_command} #{ASC_ARGS}"
93
+ else
94
+ "#{base_command} --domain #{domain} --ep #{type} #{ASC_ARGS}"
95
+ end
96
+ end
68
97
  end
69
98
  end
70
99
  end
@@ -33,9 +33,7 @@ module Script
33
33
 
34
34
  def dependencies_installed?
35
35
  # Assuming if node_modules folder exist at root of script folder, all deps are installed
36
- return false unless ctx.dir_exist?("node_modules")
37
- check_if_ep_dependencies_up_to_date!
38
- true
36
+ ctx.dir_exist?("node_modules")
39
37
  end
40
38
 
41
39
  def metadata
@@ -83,47 +81,14 @@ module Script
83
81
  end
84
82
 
85
83
  def bytecode
86
- blob = format(BYTECODE_FILE, name: script_name)
87
- raise Errors::WebAssemblyBinaryNotFoundError unless @ctx.file_exist?(blob)
84
+ filename = format(BYTECODE_FILE, name: script_name)
85
+ raise Errors::WebAssemblyBinaryNotFoundError unless ctx.file_exist?(filename)
88
86
 
89
- contents = File.read(blob)
90
- @ctx.rm(blob)
87
+ contents = ctx.binread(filename)
88
+ ctx.rm(filename)
91
89
 
92
90
  contents
93
91
  end
94
-
95
- def check_if_ep_dependencies_up_to_date!
96
- return true if ENV["SHOPIFY_CLI_SCRIPTS_IGNORE_OUTDATED"]
97
-
98
- # ignore exit code since it will not be 0 unless every package is up to date which they probably won't be
99
- out, _ = ctx.capture2e("npm", "outdated", "--json", "--depth", "0")
100
- parsed_outdated_check = JSON.parse(out)
101
- outdated_ep_packages = parsed_outdated_check
102
- .select { |package_name, _| package_name.start_with?("@shopify/extension-point-as-") }
103
- .select { |_, version_info| !package_is_up_to_date?(version_info) }
104
- .keys
105
- raise Errors::PackagesOutdatedError.new(outdated_ep_packages),
106
- "NPM packages out of date: #{outdated_ep_packages.join(", ")}" unless outdated_ep_packages.empty?
107
- end
108
-
109
- def package_is_up_to_date?(version_info)
110
- require "semantic/semantic"
111
- current_version = version_info["current"]
112
- latest_version = version_info["latest"]
113
-
114
- # making an assumption that the script developer knows what they're doing if they're not referencing a
115
- # semver version
116
- begin
117
- current_version = ::Semantic::Version.new(current_version)
118
- latest_version = ::Semantic::Version.new(latest_version)
119
- rescue ArgumentError
120
- return true
121
- end
122
-
123
- return false if current_version.major < latest_version.major
124
- return false if latest_version.major == 0 && current_version.minor < latest_version.minor
125
- true
126
- end
127
92
  end
128
93
  end
129
94
  end
@@ -5,12 +5,59 @@ module Script
5
5
  module Infrastructure
6
6
  module Errors
7
7
  class AppNotInstalledError < ScriptProjectError; end
8
- class AppScriptNotPushedError < ScriptProjectError; end
9
- class AppScriptUndefinedError < ScriptProjectError; end
10
8
  class BuildError < ScriptProjectError; end
9
+ class ConfigUiSyntaxError < ScriptProjectError; end
10
+
11
+ class ConfigUiMissingKeysError < ScriptProjectError
12
+ attr_reader :filename, :missing_keys
13
+ def initialize(filename, missing_keys)
14
+ super()
15
+ @filename = filename
16
+ @missing_keys = missing_keys
17
+ end
18
+ end
19
+
20
+ class ConfigUiInvalidInputModeError < ScriptProjectError
21
+ attr_reader :filename, :valid_input_modes
22
+ def initialize(filename, valid_input_modes)
23
+ super()
24
+ @filename = filename
25
+ @valid_input_modes = valid_input_modes
26
+ end
27
+ end
28
+
29
+ class ConfigUiFieldsMissingKeysError < ScriptProjectError
30
+ attr_reader :filename, :missing_keys
31
+ def initialize(filename, missing_keys)
32
+ super()
33
+ @filename = filename
34
+ @missing_keys = missing_keys
35
+ end
36
+ end
37
+
38
+ class ConfigUiFieldsInvalidTypeError < ScriptProjectError
39
+ attr_reader :filename, :valid_types
40
+ def initialize(filename, valid_types)
41
+ super()
42
+ @filename = filename
43
+ @valid_types = valid_types
44
+ end
45
+ end
46
+
11
47
  class DependencyInstallError < ScriptProjectError; end
48
+ class DeprecatedEPError < ScriptProjectError; end
12
49
  class EmptyResponseError < ScriptProjectError; end
13
50
  class ForbiddenError < ScriptProjectError; end
51
+ class InvalidContextError < ScriptProjectError; end
52
+
53
+ class InvalidLanguageError < ScriptProjectError
54
+ attr_reader :language, :extension_point_type
55
+ def initialize(language, extension_point_type)
56
+ super()
57
+ @language = language
58
+ @extension_point_type = extension_point_type
59
+ end
60
+ end
14
61
 
15
62
  class GraphqlError < ScriptProjectError
16
63
  attr_reader :errors
@@ -30,25 +77,9 @@ module Script
30
77
  end
31
78
  end
32
79
 
33
- class ScriptServiceUserError < ScriptProjectError
34
- def initialize(query_name, errors)
35
- super("Failed performing #{query_name}. Errors: #{errors}.")
36
- end
37
- end
38
-
80
+ class ScriptProjectAlreadyExistsError < ScriptProjectError; end
39
81
  class ShopAuthenticationError < ScriptProjectError; end
40
- class ShopScriptConflictError < ScriptProjectError; end
41
- class ShopScriptUndefinedError < ScriptProjectError; end
42
82
  class TaskRunnerNotFoundError < ScriptProjectError; end
43
-
44
- class PackagesOutdatedError < ScriptProjectError
45
- attr_reader :outdated_packages
46
- def initialize(outdated_packages)
47
- super("EP packages are outdated and need to be updated: #{outdated_packages.join(", ")}")
48
- @outdated_packages = outdated_packages
49
- end
50
- end
51
-
52
83
  class BuildScriptNotFoundError < ScriptProjectError; end
53
84
  class InvalidBuildScriptError < ScriptProjectError; end
54
85
 
@@ -8,14 +8,14 @@ module Script
8
8
  property! :ctx, accepts: ShopifyCli::Context
9
9
 
10
10
  def create_push_package(script_project:, script_content:, compiled_type:, metadata:)
11
- build_file_path = file_path(script_project.script_name, compiled_type)
11
+ build_file_path = file_path(script_project.id, script_project.script_name, compiled_type)
12
12
  write_to_path(build_file_path, script_content)
13
13
 
14
14
  Domain::PushPackage.new(
15
15
  id: build_file_path,
16
+ uuid: script_project.uuid,
16
17
  extension_point_type: script_project.extension_point_type,
17
18
  script_name: script_project.script_name,
18
- description: script_project.description,
19
19
  script_content: script_content,
20
20
  compiled_type: compiled_type,
21
21
  metadata: metadata,
@@ -24,16 +24,15 @@ module Script
24
24
  end
25
25
 
26
26
  def get_push_package(script_project:, compiled_type:, metadata:)
27
- build_file_path = file_path(script_project.script_name, compiled_type)
27
+ build_file_path = file_path(script_project.id, script_project.script_name, compiled_type)
28
28
  raise Domain::PushPackageNotFoundError unless ctx.file_exist?(build_file_path)
29
29
 
30
- script_content = File.read(build_file_path)
31
-
30
+ script_content = ctx.binread(build_file_path)
32
31
  Domain::PushPackage.new(
33
32
  id: build_file_path,
33
+ uuid: script_project.uuid,
34
34
  extension_point_type: script_project.extension_point_type,
35
35
  script_name: script_project.script_name,
36
- description: script_project.description,
37
36
  script_content: script_content,
38
37
  compiled_type: compiled_type,
39
38
  metadata: metadata,
@@ -45,11 +44,11 @@ module Script
45
44
 
46
45
  def write_to_path(path, content)
47
46
  ctx.mkdir_p(File.dirname(path))
48
- ctx.write(path, content)
47
+ ctx.binwrite(path, content)
49
48
  end
50
49
 
51
- def file_path(script_name, compiled_type)
52
- "#{ScriptProject.current.directory}/build/#{script_name}.#{compiled_type}"
50
+ def file_path(path_to_script, script_name, compiled_type)
51
+ "#{path_to_script}/build/#{script_name}.#{compiled_type}"
53
52
  end
54
53
  end
55
54
  end
@@ -51,7 +51,7 @@ module Script
51
51
  binary_path = "target/#{BUILD_TARGET}/release/#{binary_name}"
52
52
  raise Errors::WebAssemblyBinaryNotFoundError unless ctx.file_exist?(binary_path)
53
53
 
54
- File.read(binary_path)
54
+ ctx.binread(binary_path)
55
55
  end
56
56
  end
57
57
  end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Script
4
+ module Layers
5
+ module Infrastructure
6
+ class ScriptProjectRepository
7
+ include SmartProperties
8
+ property! :ctx, accepts: ShopifyCli::Context
9
+
10
+ DEFAULT_CONFIG_UI_FILENAME = "config-ui.yml"
11
+ MUTABLE_ENV_VALUES = %i(uuid)
12
+
13
+ def create(script_name:, extension_point_type:, language:, no_config_ui:)
14
+ validate_metadata!(extension_point_type, language)
15
+
16
+ optional_identifiers = {}
17
+ config_ui_file = nil
18
+
19
+ unless no_config_ui
20
+ optional_identifiers.merge!(config_ui_file: DEFAULT_CONFIG_UI_FILENAME)
21
+ config_ui_file = ConfigUiRepository
22
+ .new(ctx: ctx)
23
+ .create(DEFAULT_CONFIG_UI_FILENAME, default_config_ui_content(script_name))
24
+ end
25
+
26
+ ShopifyCli::Project.write(
27
+ ctx,
28
+ project_type: :script,
29
+ organization_id: nil,
30
+ extension_point_type: extension_point_type,
31
+ script_name: script_name,
32
+ language: language,
33
+ **optional_identifiers
34
+ )
35
+
36
+ Domain::ScriptProject.new(
37
+ id: ctx.root,
38
+ env: project.env,
39
+ script_name: script_name,
40
+ extension_point_type: extension_point_type,
41
+ language: language,
42
+ config_ui: config_ui_file
43
+ )
44
+ end
45
+
46
+ def get
47
+ validate_metadata!(extension_point_type, language)
48
+
49
+ config_ui = ConfigUiRepository.new(ctx: ctx).get(config_ui_file)
50
+
51
+ Domain::ScriptProject.new(
52
+ id: project.directory,
53
+ env: project.env,
54
+ script_name: script_name,
55
+ extension_point_type: extension_point_type,
56
+ language: language,
57
+ config_ui: config_ui
58
+ )
59
+ end
60
+
61
+ def update_env(**args)
62
+ capture_io do
63
+ args.slice(*MUTABLE_ENV_VALUES).each do |key, value|
64
+ project.env.extra[key.to_s.upcase] = value
65
+ project.env.update(ctx, :extra, project.env.extra)
66
+ end
67
+ end
68
+
69
+ Domain::ScriptProject.new(
70
+ id: ctx.root,
71
+ env: project.env,
72
+ script_name: script_name,
73
+ extension_point_type: extension_point_type,
74
+ language: language,
75
+ config_ui: ConfigUiRepository.new(ctx: ctx).get(config_ui_file),
76
+ )
77
+ end
78
+
79
+ def create_env(api_key:, secret:, uuid:)
80
+ ShopifyCli::Resources::EnvFile.new(
81
+ api_key: api_key,
82
+ secret: secret,
83
+ extra: {
84
+ Domain::ScriptProject::UUID_ENV_KEY => uuid,
85
+ }
86
+ ).write(ctx)
87
+
88
+ Domain::ScriptProject.new(
89
+ id: ctx.root,
90
+ env: project.env,
91
+ script_name: script_name,
92
+ extension_point_type: extension_point_type,
93
+ language: language,
94
+ config_ui: ConfigUiRepository.new(ctx: ctx).get(config_ui_file),
95
+ )
96
+ end
97
+
98
+ private
99
+
100
+ def capture_io(&block)
101
+ CLI::UI::StdoutRouter::Capture.new(&block).run
102
+ end
103
+
104
+ def extension_point_type
105
+ project_config_value!("extension_point_type")
106
+ end
107
+
108
+ def script_name
109
+ project_config_value!("script_name")
110
+ end
111
+
112
+ def config_ui_file
113
+ project_config_value("config_ui_file")
114
+ end
115
+
116
+ def language
117
+ project_config_value("language")&.downcase || default_language
118
+ end
119
+
120
+ def project_config_value(key)
121
+ return nil unless project.config.key?(key)
122
+ project.config[key]
123
+ end
124
+
125
+ def project_config_value!(key)
126
+ raise Errors::InvalidContextError, key unless project.config.key?(key)
127
+ project.config[key]
128
+ end
129
+
130
+ def project
131
+ @project ||= ShopifyCli::Project.current(force_reload: true)
132
+ end
133
+
134
+ def default_config_ui_content(title)
135
+ require "yaml" # takes 20ms, so deferred as late as possible.
136
+ YAML.dump({
137
+ "version" => 1,
138
+ "inputMode" => "single",
139
+ "title" => title,
140
+ "description" => "",
141
+ "fields" => [],
142
+ })
143
+ end
144
+
145
+ def default_language
146
+ Domain::ExtensionPoint::ExtensionPointAssemblyScriptSDK.language
147
+ end
148
+
149
+ def validate_metadata!(extension_point_type, language)
150
+ if Application::ExtensionPoints.deprecated_types.include?(extension_point_type)
151
+ raise Errors::DeprecatedEPError, extension_point_type
152
+ elsif !Application::ExtensionPoints.supported_language?(type: extension_point_type, language: language)
153
+ raise Errors::InvalidLanguageError.new(language, extension_point_type)
154
+ end
155
+ end
156
+
157
+ class ConfigUiRepository
158
+ include SmartProperties
159
+ property! :ctx, accepts: ShopifyCli::Context
160
+
161
+ def create(filename, content)
162
+ File.write(filename, content)
163
+
164
+ Domain::ConfigUi.new(
165
+ filename: filename,
166
+ content: content,
167
+ )
168
+ end
169
+
170
+ def get(filename)
171
+ return nil unless filename
172
+
173
+ path = File.join(ctx.root, filename)
174
+ raise Domain::Errors::MissingSpecifiedConfigUiDefinitionError, filename unless File.exist?(path)
175
+
176
+ content = File.read(path)
177
+ raise Domain::Errors::InvalidConfigUiDefinitionError, filename unless valid_config_ui?(content)
178
+
179
+ Domain::ConfigUi.new(
180
+ filename: filename,
181
+ content: content,
182
+ )
183
+ end
184
+
185
+ private
186
+
187
+ def valid_config_ui?(raw_yaml)
188
+ require "yaml" # takes 20ms, so deferred as late as possible.
189
+ YAML.safe_load(raw_yaml)
190
+ true
191
+ rescue Psych::SyntaxError
192
+ false
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end