diff --git a/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb b/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb index 1d710c779b..5ed672c9a3 100644 --- a/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +++ b/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb @@ -34,7 +34,9 @@ def install_dependencies def dependencies_installed? # Assuming if node_modules folder exist at root of script folder, all deps are installed - ctx.dir_exist?("node_modules") + return false unless ctx.dir_exist?("node_modules") + check_if_ep_dependencies_up_to_date! + true end private @@ -58,6 +60,39 @@ def compile def bytecode File.read(format(BYTECODE_FILE, name: script_name)) end + + def check_if_ep_dependencies_up_to_date! + return true if ENV['SHOPIFY_CLI_SCRIPTS_IGNORE_OUTDATED'] + + # ignore exit code since it will not be 0 unless every package is up to date which they probably won't be + out, _ = ctx.capture2e("npm", "outdated", "--json", "--depth", "0") + parsed_outdated_check = JSON.parse(out) + outdated_ep_packages = parsed_outdated_check + .select { |package_name, _| package_name.start_with?('@shopify/extension-point-as-') } + .select { |_, version_info| !package_is_up_to_date?(version_info) } + .keys + raise Errors::PackagesOutdatedError.new(outdated_ep_packages), + "NPM packages out of date: #{outdated_ep_packages.join(', ')}" unless outdated_ep_packages.empty? + end + + def package_is_up_to_date?(version_info) + require 'semantic/semantic' + current_version = version_info['current'] + latest_version = version_info['latest'] + + # making an assumption that the script developer knows what they're doing if they're not referencing a + # semver version + begin + current_version = ::Semantic::Version.new(current_version) + latest_version = ::Semantic::Version.new(latest_version) + rescue ArgumentError + return true + end + + return false if current_version.major < latest_version.major + return false if latest_version.major == 0 && current_version.minor < latest_version.minor + true + end end end end diff --git a/lib/project_types/script/layers/infrastructure/errors.rb b/lib/project_types/script/layers/infrastructure/errors.rb index ecd0cee315..40c48efcc0 100644 --- a/lib/project_types/script/layers/infrastructure/errors.rb +++ b/lib/project_types/script/layers/infrastructure/errors.rb @@ -34,6 +34,13 @@ class ShopAuthenticationError < ScriptProjectError; end class ShopScriptConflictError < ScriptProjectError; end class ShopScriptUndefinedError < ScriptProjectError; end class TaskRunnerNotFoundError < ScriptProjectError; end + class PackagesOutdatedError < ScriptProjectError + attr_reader :outdated_packages + def initialize(outdated_packages) + super("EP packages are outdated and need to be updated: #{outdated_packages.join(', ')}") + @outdated_packages = outdated_packages + end + end end end end diff --git a/lib/project_types/script/messages/messages.rb b/lib/project_types/script/messages/messages.rb index 72052e275f..c24f89e5e5 100644 --- a/lib/project_types/script/messages/messages.rb +++ b/lib/project_types/script/messages/messages.rb @@ -71,6 +71,9 @@ module Messages shop_script_conflict_help: "Disable that script or uninstall that app and try again.", shop_script_undefined_cause: "Script is already turned off in store.", + + packages_outdated_cause: "The following npm packages are out of date: %s.", + packages_outdated_help: "Run `npm update` to update them.", }, create: { diff --git a/lib/project_types/script/ui/error_handler.rb b/lib/project_types/script/ui/error_handler.rb index c88974d1aa..24771b778c 100644 --- a/lib/project_types/script/ui/error_handler.rb +++ b/lib/project_types/script/ui/error_handler.rb @@ -142,6 +142,14 @@ def self.error_messages(e) { cause_of_error: ShopifyCli::Context.message('script.error.shop_script_undefined_cause'), } + when Layers::Infrastructure::Errors::PackagesOutdatedError + { + cause_of_error: ShopifyCli::Context.message( + 'script.error.packages_outdated_cause', + e.outdated_packages.join(', ') + ), + help_suggestion: ShopifyCli::Context.message('script.error.packages_outdated_help'), + } end end end diff --git a/test/project_types/script/layers/infrastructure/assemblyscript_task_runner_test.rb b/test/project_types/script/layers/infrastructure/assemblyscript_task_runner_test.rb index 26e245bbe4..99181b96f0 100644 --- a/test/project_types/script/layers/infrastructure/assemblyscript_task_runner_test.rb +++ b/test/project_types/script/layers/infrastructure/assemblyscript_task_runner_test.rb @@ -61,15 +61,70 @@ describe ".dependencies_installed?" do subject { as_task_runner.dependencies_installed? } - it "should return true if node_modules folder exists" do + before do FileUtils.mkdir_p("node_modules") + end + + it "should return true if node_modules folder exists" do + stub_npm_outdated({}) assert_equal true, subject end it "should return false if node_modules folder does not exists" do Dir.stubs(:exist?).returns(false) + stub_npm_outdated({}) assert_equal false, subject end + + it "should not error if `npm outdated` returns nothing" do + stub_npm_outdated({}) + subject + end + + it "should not error if `npm outdated` does not return an EP package" do + stub_npm_outdated(create_package_version_info(package_name: "somepackage")) + subject + end + + it "should not error if current version is linked" do + stub_npm_outdated(create_package_version_info(current: "linked")) + subject + end + + it "should not error if latest version is an https URL" do + stub_npm_outdated(create_package_version_info(latest: "https://github.com/somethingsomething")) + subject + end + + it "should not error if patch version is different" do + stub_npm_outdated(create_package_version_info(current: "0.9.0", latest: "0.9.1")) + subject + end + + it "should not error if it's a non-zero major version and minor version is different" do + stub_npm_outdated(create_package_version_info(current: "1.0.0", latest: "1.1.0")) + subject + end + + it "should error if it's a zero major version and minor version is different" do + package_name = "@shopify/extension-point-as-foo" + stub_npm_outdated(create_package_version_info(package_name: package_name, current: "0.9.0", latest: "0.10.0")) + msg = "NPM packages out of date: #{package_name}" + error = assert_raises Script::Layers::Infrastructure::Errors::PackagesOutdatedError, msg do + subject + end + assert_equal msg, error.message + end + + it "should error if major version is different" do + package_name = "@shopify/extension-point-as-foo" + stub_npm_outdated(create_package_version_info(package_name: package_name, current: "0.9.0", latest: "1.0.0")) + msg = "NPM packages out of date: #{package_name}" + error = assert_raises Script::Layers::Infrastructure::Errors::PackagesOutdatedError do + subject + end + assert_equal msg, error.message + end end describe ".install_dependencies" do @@ -93,4 +148,16 @@ end end end + + private + + def stub_npm_outdated(output) + ctx.stubs(:capture2e) + .with("npm", "outdated", "--json", "--depth", "0") + .returns([output.to_json, mock]) + end + + def create_package_version_info(package_name: "@shopify/extension-point-as-foo", current: "0.9.0", latest: "0.10.0") + { package_name => { "current" => current, "latest" => latest } } + end end