# frozen_string_literal: true require "hashdiff" module NeetoCompliance class JsconfigVerifier < Base attr_accessor :diffs def initialize @diffs = [] end def local_copy "app/javascript/jsconfig.json" end def commons_copy NeetoCompliance::NeetoCommons.path.join "common_files", "app", "javascript", "jsconfig.json" end def local_jsconfig @_local_jsconfig ||= read_file local_copy end def required_jsconfig @_required_jsconfig ||= read_file commons_copy end def read_file(file) JSON.parse File.read(file) end def verify_command self.diffs = Hashdiff.best_diff(required_jsconfig, local_jsconfig) # diffs is an array of differences and each element is a diff. self.diffs.any? { |diff| removed_or_modified?(diff) } end def removed_or_modified?(diff) # A diff is of the format: ['-', 'a.x', 2] # A modified key is of the format: ['~', 'a.x', 2, 4] # First element is a symbol. '-' indicates removal and `~` indicates modification diff[0] == "-" || diff[0] == "~" end def local_file_exists? File.file?(local_copy) end def newly_added_paths self.diffs = Hashdiff.diff(required_jsconfig, local_jsconfig, use_lcs: false) capture_addition_into_paths_key = "(?=.*\+)(?=.*compilerOptions.paths)" @_newly_added_paths ||= self.diffs.select { |diff| diff.join =~ /#{capture_addition_into_paths_key}.*/ } end def custom_paths paths = Hash.new newly_added_paths.each do |path| if path[0] == "+" key = path[1].split(".").last value = path[2] paths[key] = value end end @_custom_paths ||= paths end def modified_paths paths = Hash.new newly_added_paths.each do |path| if path[0] == "~" # An unwanted '[0]' character is added at the to all modified diffs by the gem paths[path[1].split(".").last.chomp("[0]")] = { common: path[2], local: path[3] } end end @_modified_paths ||= paths end def add_comments_to_jsconfig_file text = File.read(local_copy) text['"commons_paths_comment": "",'] = "// don't modify the below paths without consulting with neetoCompliance team" text['"custom_paths_comment": ""'] = "// You can add custom project specific paths below this comment" File.open(local_copy, "w") { |file| file.puts text } end def exception_message(paths) modified_paths = "" paths.each do |key, value| modified_paths += " path: #{key} Common value: [\"#{value[:common]}\"] Project's value: [\"#{value[:local]}\"]" end exception_message = %{ Could not autofix jsconfig changes. The below path(s) conflict with the one defined in neeto-commons-backend: #{modified_paths} } end def add_place_holders_for_comments # Adds placeholders into the entire object to keep track of places where we need to insert comments paths_with_comment = { "commons_paths_comment": "" }.merge(required_jsconfig["compilerOptions"]["paths"]) paths_with_comment = paths_with_comment.merge("custom_paths_comment": "") paths_with_comment.merge(custom_paths) end def write_jsconfig_object_to_file jsconfig_data = required_jsconfig jsconfig_data["compilerOptions"]["paths"] = add_place_holders_for_comments File.open(local_copy, "w") do |file| file.write(JSON.pretty_generate(jsconfig_data)) end end def auto_correct! unless valid? if !modified_paths.empty? raise StandardError.new exception_message(modified_paths) end write_jsconfig_object_to_file add_comments_to_jsconfig_file end end def valid? return false unless local_file_exists? is_modified_or_removed = verify_command !is_modified_or_removed end def autofix_command "cp #{commons_copy} #{local_copy}" end def neeto_audit_script_command "bundle exec neeto-audit -a" end def upstream_copy "https://github.com/bigbinary/neeto-commons-backend/blob/stable/lib/neeto-commons-backend/common_files/app/javascript/jsconfig.json" end def autofix_suggestion base_message = %{ Your jsconfig.json content doesn't match with the expected neeto-commons content. The following is the recommended jsconfig.json content in neeto ecosystem: 1) View #{upstream_copy} 2) Or open #{commons_copy} This can be fixed automatically by running: #{neeto_audit_script_command.yellow} To fix manually, start by fully replacing app/javascript/jsconfig.json of your app with above mentioned content, by running the following: #{autofix_command.yellow} } # The following is an example diff: # [["-", "compilerOptions.moduleResolution", "NodeNext"], # ["~", "compilerOptions.module", "ESNext", "ESNex"], # ["+", "compilerOptions.paths.neetou", ["../../node_modules/@bigbinary/neetoui/**"]], # For the above diff, we only need to get the newly added items marked by "+" # and also check whether it's a "path" that has been added. if newly_added_paths.empty? return base_message end message = %{ #{base_message} ================================================== It seems that you've also added some custom paths in your jsconfig. You can cherry pick and append the custom paths to app/javascript/jsconfig.json, if need be, after running above mentioned copy command. Refer the comments in #{upstream_copy} to see how to add custom paths. The following are the custom paths that we have detected in your app/javascript/jsconfig.json: #{JSON.pretty_generate(custom_paths).yellow} } end end end