From 1b778b6a483e057b41c3cc8f11adf62ca2cc6521 Mon Sep 17 00:00:00 2001 From: Malek Kamel Date: Sun, 27 Oct 2024 10:24:13 +0300 Subject: [PATCH] Add template sync and JSON file viewer in dashboard - Display user-defined JSON files in dashboard - Add mechanism to sync existing brand with template changes --- Gemfile.lock | 2 +- solara/lib/core/brands/brand_onboarder.rb | 54 +++---- solara/lib/core/brands/brand_switcher.rb | 1 + .../dashboard/brand/BrandDetailController.js | 2 +- .../dashboard/brand/SectionsFormManager.js | 6 +- solara/lib/core/dashboard/brand/brand.html | 4 +- .../brand/source/BrandLocalSource.js | 4 +- .../brand/source/BrandRemoteSource.js | 20 ++- .../dashboard/handler/edit_section_handler.rb | 22 +-- .../android_template_validation_config.yml | 6 +- .../flutter_template_validation_config.yml | 4 +- .../ios_template_validation_config.yml | 6 +- .../lib/core/scripts/brand_config_updater.rb | 25 ++++ .../scripts/brand_configurations_manager.rb | 90 ++++++++++-- solara/lib/core/scripts/brand_importer.rb | 29 ++-- solara/lib/core/scripts/code_generator.rb | 2 +- solara/lib/core/scripts/gitignore_manager.rb | 3 +- .../core/scripts/json_manifest_processor.rb | 2 +- .../scripts/resource_manifest_processor.rb | 3 +- solara/lib/core/scripts/string_case.rb | 18 +++ .../config/android_template_config.json | 12 +- .../config/flutter_template_config.json | 8 +- .../template/config/ios_template_config.json | 12 +- solara/lib/core/template/configurations.json | 41 ++++-- .../template/project_template_generator.rb | 132 ++++++++++++++++-- solara/lib/solara_manager.rb | 6 +- 26 files changed, 371 insertions(+), 143 deletions(-) create mode 100644 solara/lib/core/scripts/brand_config_updater.rb create mode 100644 solara/lib/core/scripts/string_case.rb diff --git a/Gemfile.lock b/Gemfile.lock index 8ea1e78..7791fae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - solara (0.4.0) + solara (0.6.0) cgi (~> 0.4.1) colorize (~> 1.1.0) json-schema (~> 4.3.1) diff --git a/solara/lib/core/brands/brand_onboarder.rb b/solara/lib/core/brands/brand_onboarder.rb index bc880cb..e43ad94 100644 --- a/solara/lib/core/brands/brand_onboarder.rb +++ b/solara/lib/core/brands/brand_onboarder.rb @@ -5,43 +5,47 @@ Dir[File.expand_path('platform/flutter/*.rb', __dir__)].each { |file| require_relative file } class BrandOnboarder - def initialize(brand_key, brand_name, clone_brand_key: nil) - @brand_key = brand_key - @brand_name = brand_name - @clone_brand_key = clone_brand_key - end - def onboard - if @clone_brand_key.nil? || @clone_brand_key.strip.empty? - generate_brand_template + def onboard(brand_key, brand_name, clone_brand_key: nil) + if clone_brand_key.nil? || clone_brand_key.strip.empty? + generate_from_template(brand_key) else - clone_brand + clone_brand(brand_key, clone_brand_key: clone_brand_key) end - add_to_brands_list + add_to_brands_list(brand_key, brand_name) end - def generate_brand_template - Solara.logger.debug("Onboarding #{@brand_key} from template.") - - template_dir = FilePath.template_brands - target_dir = FilePath.brand(@brand_key) - config_file = FilePath.template_config - - generator = ProjectTemplateGenerator.new(template_dir, target_dir, config_file) - generator.create_project + def sync_with_template(brand_key) + generator = template_generator(brand_key) + generator.sync_with_template end - def clone_brand - Solara.logger.debug("Cloning #{@clone_brand_key} to #{@brand_key}") - source = FilePath.brand(@clone_brand_key) - destination = FilePath.brand(@brand_key) + def clone_brand(brand_key, clone_brand_key:) + Solara.logger.debug("Cloning #{clone_brand_key} to #{brand_key}") + source = FilePath.brand(clone_brand_key) + destination = FilePath.brand(brand_key) FileManager.delete_if_exists(destination) FolderCopier.new(source, destination).copy end - def add_to_brands_list - BrandsManager.instance.add_brand(@brand_name, @brand_key) + def add_to_brands_list(brand_key, brand_name) + BrandsManager.instance.add_brand(brand_name, brand_key) + end + + def generate_from_template(brand_key) + Solara.logger.debug("Onboarding #{brand_key} from template.") + + generator = template_generator(brand_key) + generator.create_project + end + + def template_generator(brand_key) + template_dir = FilePath.template_brands + target_dir = FilePath.brand(brand_key) + config_file = FilePath.template_config + + ProjectTemplateGenerator.new(template_dir, target_dir, config_file) end end diff --git a/solara/lib/core/brands/brand_switcher.rb b/solara/lib/core/brands/brand_switcher.rb index e498a62..d7a1db7 100644 --- a/solara/lib/core/brands/brand_switcher.rb +++ b/solara/lib/core/brands/brand_switcher.rb @@ -16,6 +16,7 @@ def initialize(brand_key, ignore_health_check: false) def start Solara.logger.header("Switching to #{@brand_key}") + BrandOnboarder.new.sync_with_template(@brand_key) @health_checker.check_health BrandsManager.instance.save_current_brand(@brand_key) @artifacts_switcher.switch diff --git a/solara/lib/core/dashboard/brand/BrandDetailController.js b/solara/lib/core/dashboard/brand/BrandDetailController.js index a78f5e2..5751c7c 100644 --- a/solara/lib/core/dashboard/brand/BrandDetailController.js +++ b/solara/lib/core/dashboard/brand/BrandDetailController.js @@ -158,7 +158,7 @@ class BrandDetailController { if (this.model.isRemote()) return try { - await this.model.saveSection(container.dataset.key, section.content); + await this.model.saveSection(container.dataset.filename, section.content); await this.checkBrandHealth(); if (this.model.isCurrentBrand) { await this.switchToBrand(true) diff --git a/solara/lib/core/dashboard/brand/SectionsFormManager.js b/solara/lib/core/dashboard/brand/SectionsFormManager.js index 4e1b6d7..6808cb8 100644 --- a/solara/lib/core/dashboard/brand/SectionsFormManager.js +++ b/solara/lib/core/dashboard/brand/SectionsFormManager.js @@ -22,7 +22,7 @@ class SectionsFormManager { const sectionElement = document.createElement('div'); sectionElement.className = 'section'; - sectionElement.dataset.key = section.key + sectionElement.dataset.filename = section.filename sectionElement.dataset.name = section.name const titleContainer = document.createElement('div'); @@ -35,7 +35,7 @@ class SectionsFormManager { sectionElement.appendChild(titleContainer); - sectionElement.id = section.key; + sectionElement.id = section.filename; this.sectionsContainer.appendChild(sectionElement); @@ -61,7 +61,7 @@ class SectionItemManager { displayJSONCards() { let cardContent = this.container.querySelector('.card-content') if (cardContent !== null) this.container.innerHTML = '' - this.container.appendChild(this.createCard(this.section.content, 'root', null, this.section.key)); + this.container.appendChild(this.createCard(this.section.content, 'root', null, this.section.filename)); } createCard(obj, key, parent, cardTitle) { diff --git a/solara/lib/core/dashboard/brand/brand.html b/solara/lib/core/dashboard/brand/brand.html index bdc64f6..2ca41e8 100644 --- a/solara/lib/core/dashboard/brand/brand.html +++ b/solara/lib/core/dashboard/brand/brand.html @@ -90,7 +90,7 @@ .right { width: 15%; position: fixed; - max-height: 50%; + max-height: 95%; overflow-y: auto; padding: 7px; right: 0; @@ -253,7 +253,7 @@ #error-button { position: fixed; bottom: 14px; - right: 14px; + left: 14px; background-color: #ff4136; color: white; border: none; diff --git a/solara/lib/core/dashboard/brand/source/BrandLocalSource.js b/solara/lib/core/dashboard/brand/source/BrandLocalSource.js index b1e1b29..1781cd9 100644 --- a/solara/lib/core/dashboard/brand/source/BrandLocalSource.js +++ b/solara/lib/core/dashboard/brand/source/BrandLocalSource.js @@ -43,13 +43,13 @@ class BrandLocalSource { } } - async saveSection(key, configuration, brandKey) { + async saveSection(filename, configuration, brandKey) { if (this.savingInProgress) return; this.savingInProgress = true; const dataToSend = { brand_key: brandKey, - key: key, + filename: filename, data: configuration }; diff --git a/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js b/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js index 66951f5..e39635c 100644 --- a/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js +++ b/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js @@ -21,8 +21,8 @@ class BrandRemoteSource { } const content = await contentResponse.json(); return { - key: config.key, - name: config.name, + filename: config.filename, + name: this.snakeToCapitalizedSpaced(config.filename, 'ios'), content: content }; }); @@ -36,6 +36,22 @@ class BrandRemoteSource { } } + // TODO: should be ecnapsulated + snakeToCapitalizedSpaced( + snakeCaseString, + exclude = '', + transform = (item) => item.charAt(0).toUpperCase() + item.slice(1) + ) { + // Split by underscores + const parts = snakeCaseString.split('_').map(item => { + // Return the item as-is if it matches the exclude value + return item === exclude ? item : transform(item); + }); + + // Join the parts with a space + return parts.join(' '); + } + async getBrandConfigurationsJsonFromDirectory(dirHandle) { const schema = await this.fetchBrandConfigurationsSchema(); diff --git a/solara/lib/core/dashboard/handler/edit_section_handler.rb b/solara/lib/core/dashboard/handler/edit_section_handler.rb index 07cd438..6da5fad 100644 --- a/solara/lib/core/dashboard/handler/edit_section_handler.rb +++ b/solara/lib/core/dashboard/handler/edit_section_handler.rb @@ -7,14 +7,14 @@ def mount begin request_payload = JSON.parse(req.body) brand_key = request_payload['brand_key'] - key = request_payload['key'] + filename = request_payload['filename'] data = request_payload['data'] - update_section(key, data, brand_key) + update_section(filename, data, brand_key) set_current_brand_content_changed(brand_key, true) res.status = 200 - res.body = JSON.generate({ success: true, message: "Configuration for #{key} updated successfully" }) + res.body = JSON.generate({ success: true, message: "Configuration for #{filename} updated successfully" }) res.content_type = 'application/json' rescue JSON::ParserError => e handle_error(res, e, "Invalid JSON in request body", 400) @@ -27,20 +27,8 @@ def mount end end - def update_section(key, data, brand_key) - template = BrandConfigurationsManager.new(brand_key).template_with_key(key) - - path = template[:path] - - if File.exist?(path) - File.write(path, JSON.pretty_generate(data)) - Solara.logger.debug("Updated Config for #{path}: #{data}") - else - raise "Config file not found: #{path}" - end - rescue StandardError => e - Solara.logger.failure("Error updating section: #{e.message}") - raise + def update_section(filename, data, brand_key) + BrandConfigUpdater.new.update(filename, data, brand_key) end def set_current_brand_content_changed(brand_key, changed) diff --git a/solara/lib/core/doctor/validator/template/android_template_validation_config.yml b/solara/lib/core/doctor/validator/template/android_template_validation_config.yml index 7f3df66..1ecfa53 100644 --- a/solara/lib/core/doctor/validator/template/android_template_validation_config.yml +++ b/solara/lib/core/doctor/validator/template/android_template_validation_config.yml @@ -28,7 +28,7 @@ structure: json: type: directory contents: - json_manifest.json: + android_json_manifest.json: type: file validations: - type: valid_json @@ -63,7 +63,7 @@ structure: json: type: directory contents: - json_manifest.json: + ios_json_manifest.json: type: file validations: - type: valid_json @@ -94,7 +94,7 @@ structure: json: type: directory contents: - json_manifest.json: + global_json_manifest.json: type: file validations: - type: valid_json diff --git a/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml b/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml index 7c208a8..501d56b 100644 --- a/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml +++ b/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml @@ -5,7 +5,7 @@ structure: json: type: directory contents: - json_manifest.json: + flutter_json_manifest.json: type: file validations: - type: valid_json @@ -89,7 +89,7 @@ structure: json: type: directory contents: - json_manifest.json: + global_json_manifest.json: type: file validations: - type: valid_json diff --git a/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml b/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml index 7f3df66..1ecfa53 100644 --- a/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml +++ b/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml @@ -28,7 +28,7 @@ structure: json: type: directory contents: - json_manifest.json: + android_json_manifest.json: type: file validations: - type: valid_json @@ -63,7 +63,7 @@ structure: json: type: directory contents: - json_manifest.json: + ios_json_manifest.json: type: file validations: - type: valid_json @@ -94,7 +94,7 @@ structure: json: type: directory contents: - json_manifest.json: + global_json_manifest.json: type: file validations: - type: valid_json diff --git a/solara/lib/core/scripts/brand_config_updater.rb b/solara/lib/core/scripts/brand_config_updater.rb new file mode 100644 index 0000000..0c9efdc --- /dev/null +++ b/solara/lib/core/scripts/brand_config_updater.rb @@ -0,0 +1,25 @@ +class BrandConfigUpdater + + def update(filename, data, brand_key) + template = BrandConfigurationsManager.new(brand_key).template_with_filename(filename) + + path = template[:path] + json = JSON.pretty_generate(data) + + begin + # Check if the file exists + if File.exist?(path) + # If it exists, update the file + File.write(path, json) + Solara.logger.debug("Updated Config for #{path}: #{data}") + else + # If it doesn't exist, create the file with the data + File.write(path, json) + Solara.logger.debug("Created Config for #{path}: #{data}") + end + rescue StandardError => e + Solara.logger.failure("Error updating #{brand_key} config file #{path}: #{e.message}") + raise + end + end +end \ No newline at end of file diff --git a/solara/lib/core/scripts/brand_configurations_manager.rb b/solara/lib/core/scripts/brand_configurations_manager.rb index aea5f47..efb1a94 100644 --- a/solara/lib/core/scripts/brand_configurations_manager.rb +++ b/solara/lib/core/scripts/brand_configurations_manager.rb @@ -4,19 +4,12 @@ def initialize(brand_key) @brand_key = brand_key end - def template_with_key(key) - templates.select { |section| section[:key] === key }.first + def template_with_filename(filename) + templates.select { |section| section[:filename] === filename }.first end def templates - configurations = JSON.parse(File.read(FilePath.brand_configurations))['configurations'] - configurations.map do |config| - { - key: config['key'], - name: config['name'], - path: "#{FilePath.brand(@brand_key)}/#{config['filePath']}" - } - end + TemplateManager.new(@brand_key).templates end def create @@ -24,18 +17,87 @@ def create config_templates.map do |template| create_config_item( - template[:key], + template[:filename], template[:name], template[:path] ) end end - def create_config_item(key, name, path) + def create_config_item(filename, name, path) { - key: key, + filename: filename, name: name, content: JSON.parse(File.read(path)), } end -end \ No newline at end of file +end + +class TemplateManager + + def initialize(brand_key) + @brand_key = brand_key + end + + def templates + result = parse_configurations + collect_json_templates.each do |template| + result << template unless result.any? { |r| r['filename'] == template['filename'] } + end + result.compact + rescue StandardError => e + Solara.logger.error("Failed to generate templates: #{e.message}") + [] + end + + private + + def parse_configurations + configurations = JSON.parse(File.read(FilePath.brand_configurations))['configurations'] + configurations.map do |config| + path = build_path(config['filePath']) + next unless File.exist?(path) + + filename_without_extension = File.basename(config['filename'], '.json') + + { + filename: File.basename(config['filename']), + name: StringCase.snake_to_capitalized_spaced(filename_without_extension, exclude: "ios"), + path: path + } + end + rescue StandardError => e + Solara.logger.error("Failed to parse configurations: #{e.message}") + [] + end + + def collect_json_templates + directories = [ + FilePath.brand_global_json_dir, + FilePath.brand_json_dir(@brand_key) + ] + + directories.flat_map do |dir| + get_json_files(dir).map do |file| + filename_without_extension = File.basename(file, '.json') + + { + filename: File.basename(file), + name: StringCase.snake_to_capitalized_spaced(filename_without_extension, exclude: "ios"), + path: file + } + end + end + end + + def get_json_files(dir) + Dir.glob(File.join(dir, '**', '*.json')) + rescue StandardError => e + Solara.logger.error("Error reading directory #{dir}: #{e.message}") + [] + end + + def build_path(file_path) + "#{FilePath.brand(@brand_key)}/#{file_path}" + end + end \ No newline at end of file diff --git a/solara/lib/core/scripts/brand_importer.rb b/solara/lib/core/scripts/brand_importer.rb index 3caef7b..91fa474 100644 --- a/solara/lib/core/scripts/brand_importer.rb +++ b/solara/lib/core/scripts/brand_importer.rb @@ -26,8 +26,8 @@ def import(configurations_path) validate_json(configurations_path) validate_json_schema(configurations_path) - configurations_json = JSON.parse(File.read(configurations_path)) - brand = configurations_json['brand'] + configurations = JSON.parse(File.read(configurations_path)) + brand = configurations['brand'] brand_key = brand['key'] # Ensure to use 'key' instead of 'brand_key' exists = BrandsManager.instance.exists(brand_key) @@ -35,32 +35,21 @@ def import(configurations_path) SolaraManager.new.onboard(brand_key, brand['name'], open_dashboard: false) end - update_brand(brand_key, configurations_json) + SolaraManager.new.sync_brand_with_template(brand_key) if exists + update_brand(brand_key, configurations) message_suffix = exists ? "The existing brand '#{brand_key}' has been updated." : "A new brand with the key '#{brand_key}' has been onboarded." Solara.logger.success("Successfully imported (#{configurations_path}). #{message_suffix}") end - def update_brand(brand_key, configurations_json) - brand_path = FilePath.brand(brand_key) - - configurations_json['configurations'].each do |configuration| - file_name = configuration['key'] - file_path = find_file_in_subdirectories(brand_path, file_name) - - # Create or replace the contents of the configuration file - if file_path - File.write(file_path, JSON.pretty_generate(configuration['content'])) - else - Solara.logger.failure("File #{file_name} not found in #{brand_path}, ignoring importing it!") - end + def update_brand(brand_key, configurations) + configurations['configurations'].each do |configuration| + filename = configuration['filename'] + data = configuration['content'] + BrandConfigUpdater.new.update(filename, data, brand_key) end end - def find_file_in_subdirectories(base_path, file_name) - Dir.glob(File.join(base_path, '**', file_name)).first - end - def validate_json(configurations_path) begin JsonFileValidator.new([configurations_path]).validate diff --git a/solara/lib/core/scripts/code_generator.rb b/solara/lib/core/scripts/code_generator.rb index 2210589..72f4633 100644 --- a/solara/lib/core/scripts/code_generator.rb +++ b/solara/lib/core/scripts/code_generator.rb @@ -212,7 +212,7 @@ def language_specific_classes end def capitalize(string) - "#{string[0].upcase}#{string[1..-1]}" + StringCase.capitalize(string) end end diff --git a/solara/lib/core/scripts/gitignore_manager.rb b/solara/lib/core/scripts/gitignore_manager.rb index e999e8c..af960ca 100644 --- a/solara/lib/core/scripts/gitignore_manager.rb +++ b/solara/lib/core/scripts/gitignore_manager.rb @@ -27,11 +27,12 @@ def self.ignore_common_files def add_items(items) items.each do |item| - add_item(FileManager.get_relative_path_to_root(item)) + add_item(item.start_with?('/') ? item : FileManager.get_relative_path_to_root(item)) end end def add_item(item) + puts item existing_items = read_gitignore if existing_items.include?(item) diff --git a/solara/lib/core/scripts/json_manifest_processor.rb b/solara/lib/core/scripts/json_manifest_processor.rb index 02fc7da..c8ef576 100644 --- a/solara/lib/core/scripts/json_manifest_processor.rb +++ b/solara/lib/core/scripts/json_manifest_processor.rb @@ -19,7 +19,7 @@ def process private def read_manifest - manifest_path = File.join(@json_path, 'json_manifest.json') + manifest_path = File.join(@json_path, "#{SolaraSettingsManager.instance.platform}_json_manifest.json") JSON.parse(File.read(manifest_path)) rescue JSON::ParserError => e Solara.logger.debug("Error parsing manifest JSON: #{e.message}") diff --git a/solara/lib/core/scripts/resource_manifest_processor.rb b/solara/lib/core/scripts/resource_manifest_processor.rb index 5f45b82..a195a57 100644 --- a/solara/lib/core/scripts/resource_manifest_processor.rb +++ b/solara/lib/core/scripts/resource_manifest_processor.rb @@ -89,7 +89,8 @@ def destinations_of_directory_contents(src_dir, dst_dir) def git_ignore(files) files.each do |file| - GitignoreManager.new(FilePath.project_root).add_items(["/#{file}"]) + path = FileManager.get_relative_path_to_root(file) + GitignoreManager.new(FilePath.project_root).add_items(["/#{path}"]) end end diff --git a/solara/lib/core/scripts/string_case.rb b/solara/lib/core/scripts/string_case.rb new file mode 100644 index 0000000..e011cf3 --- /dev/null +++ b/solara/lib/core/scripts/string_case.rb @@ -0,0 +1,18 @@ +class StringCase + + def self.capitalize(string) + "#{string[0].upcase}#{string[1..-1]}" + end + + def self.snake_to_capitalized_spaced(snake_case_string, exclude: '', transform: ->(item) { StringCase.capitalize(item) }) + # Split by underscores, then apply the transformation to each part + parts = snake_case_string.split('_').map do |item| + # Return the item as-is if it matches the exclude value + item == exclude ? item : transform.call(item) + end + + # Join the parts with a space + parts.join(' ') + end + +end \ No newline at end of file diff --git a/solara/lib/core/template/config/android_template_config.json b/solara/lib/core/template/config/android_template_config.json index 4971bdd..54b9811 100644 --- a/solara/lib/core/template/config/android_template_config.json +++ b/solara/lib/core/template/config/android_template_config.json @@ -72,22 +72,22 @@ }, { "source": "json/json_manifest.json", - "target": "android/json/", + "target": "android/json/android_json_manifest.json", "condition": "true" }, { "source": "json/json_manifest.json", - "target": "ios/json/", + "target": "ios/json/ios_json_manifest.json", "condition": "true" }, { "source": "json/Json-Manifest.md", - "target": "android/json/", + "target": "android/json/Json-Manifest.md", "condition": "true" }, { "source": "json/Json-Manifest.md", - "target": "ios/json/", + "target": "ios/json/Json-Manifest.md", "condition": "true" }, { @@ -97,12 +97,12 @@ }, { "source": "json/json_manifest.json", - "target": "../../global/json", + "target": "../../global/json/global_json_manifest.json", "condition": "true" }, { "source": "json/Json-Manifest.md", - "target": "../../global/json", + "target": "../../global/json/Json-Manifest.md", "condition": "true" }, { diff --git a/solara/lib/core/template/config/flutter_template_config.json b/solara/lib/core/template/config/flutter_template_config.json index cda9f96..4f98bbc 100644 --- a/solara/lib/core/template/config/flutter_template_config.json +++ b/solara/lib/core/template/config/flutter_template_config.json @@ -72,12 +72,12 @@ }, { "source": "json/json_manifest.json", - "target": "flutter/json/", + "target": "flutter/json/flutter_json_manifest.json", "condition": "true" }, { "source": "json/Json-Manifest.md", - "target": "flutter/json/", + "target": "flutter/json/Json-Manifest.md", "condition": "true" }, { @@ -87,12 +87,12 @@ }, { "source": "json/json_manifest.json", - "target": "../../global/json", + "target": "../../global/json/global_json_manifest.json", "condition": "true" }, { "source": "json/Json-Manifest.md", - "target": "../../global/json", + "target": "../../global/json/Json-Manifest.md", "condition": "true" }, { diff --git a/solara/lib/core/template/config/ios_template_config.json b/solara/lib/core/template/config/ios_template_config.json index 70411c5..8f4020c 100644 --- a/solara/lib/core/template/config/ios_template_config.json +++ b/solara/lib/core/template/config/ios_template_config.json @@ -72,22 +72,22 @@ }, { "source": "json/json_manifest.json", - "target": "android/json/", + "target": "android/json/android_json_manifest.json", "condition": "true" }, { "source": "json/json_manifest.json", - "target": "ios/json/", + "target": "ios/json/ios_json_manifest.json", "condition": "true" }, { "source": "json/Json-Manifest.md", - "target": "android/json/", + "target": "android/json/Json-Manifest.md", "condition": "true" }, { "source": "json/Json-Manifest.md", - "target": "ios/json/", + "target": "ios/json/Json-Manifest.md", "condition": "true" }, { @@ -97,12 +97,12 @@ }, { "source": "json/json_manifest.json", - "target": "../../global/json", + "target": "../../global/json/global_json_manifest.json", "condition": "true" }, { "source": "json/Json-Manifest.md", - "target": "../../global/json", + "target": "../../global/json/Json-Manifest.md", "condition": "true" }, { diff --git a/solara/lib/core/template/configurations.json b/solara/lib/core/template/configurations.json index 225efcb..e7e0475 100644 --- a/solara/lib/core/template/configurations.json +++ b/solara/lib/core/template/configurations.json @@ -1,46 +1,59 @@ { "configurations": [ { - "key": "brand_config.json", - "name": "Brand Configuration", + "filename": "brand_config.json", "filePath": "shared/brand_config.json", "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/shared/brand_config.json" }, { - "key": "theme.json", - "name": "Theme Configuration", + "filename": "theme.json", "filePath": "shared/theme.json", "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/shared/theme.json" }, { - "key": "android_config.json", - "name": "Android Configuration", + "filename": "android_config.json", "filePath": "android/android_config.json", "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/android/android_config.json" }, { - "key": "android_signing.json", - "name": "Android Signing", + "filename": "android_signing.json", "filePath": "android/android_signing.json", "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/android/android_signing.json" }, { - "key": "ios_config.json", - "name": "iOS Configuration", + "filename": "ios_config.json", "filePath": "ios/ios_config.json", "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/ios/ios_config.json" }, { - "key": "ios_signing.json", - "name": "iOS Signing", + "filename": "ios_signing.json", "filePath": "ios/ios_signing.json", "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/ios/ios_signing.json" }, { - "key": "InfoPlist.xcstrings", - "name": "InfoPlist.xcstrings", + "filename": "InfoPlist.xcstrings", "filePath": "ios/InfoPlist.xcstrings", "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/ios/InfoPlist.xcstrings" + }, + { + "filename": "ios_json_manifest.json", + "filePath": "ios/json/ios_json_manifest.json", + "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/json/json_manifest.json" + }, + { + "filename": "android_json_manifest.json", + "filePath": "android/json/android_json_manifest.json", + "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/json/json_manifest.json" + }, + { + "filename": "flutter_json_manifest.json", + "filePath": "flutter/json/flutter_json_manifest.json", + "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/json/json_manifest.json" + }, + { + "filename": "global_json_manifest.json", + "filePath": "../../global/json/global_json_manifest.json", + "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/json/json_manifest.json" } ] } \ No newline at end of file diff --git a/solara/lib/core/template/project_template_generator.rb b/solara/lib/core/template/project_template_generator.rb index b9dfaa1..bf6470f 100644 --- a/solara/lib/core/template/project_template_generator.rb +++ b/solara/lib/core/template/project_template_generator.rb @@ -7,28 +7,125 @@ def initialize(template_dir, target_dir, config_file) @target_dir = target_dir @config_file = config_file @config = read_config + @file_mappings = build_file_mappings end def create_project @config["files"].each do |file| - if evaluate_condition(file["condition"], @config["variables"]) - source_path = File.join(@template_dir, file["source"]) - target_path = File.join(@target_dir, file["target"]) + next unless evaluate_condition(file["condition"], @config["variables"]) + source_path = File.join(@template_dir, file["source"]) + target_path = File.join(@target_dir, file["target"]) - if file["source"].nil? || file["source"].empty? || !File.exist?(source_path) - # Create the target directory if no source path is provided - FileUtils.mkdir_p(target_path) - else - copy_content = file.fetch("copy_content", true) - copy_item(source_path, target_path, copy_content) - replace_variables(target_path, @config["variables"]) if copy_content - end + if file["source"].nil? || file["source"].empty? || !File.exist?(source_path) + # Create the target directory if no source path is provided + FileUtils.mkdir_p(target_path) + else + copy_content = file.fetch("copy_content", true) + copy_item(source_path, target_path, copy_content, file) + replace_variables(target_path, @config["variables"]) if copy_content + end + end + end + + def sync_with_template + @config["files"].each do |file| + next unless evaluate_condition(file["condition"], @config["variables"]) + next if file["source"].nil? || file["source"].empty? + + source_path = File.join(@template_dir, file["source"]) + target_path = File.join(@target_dir, file["target"]) + + next unless File.exist?(source_path) + + if File.directory?(source_path) + FileUtils.mkdir_p(target_path) unless Dir.exist?(target_path) + sync_directory(source_path, target_path, file) + elsif !File.exist?(target_path) && should_copy_path?(source_path) + FileUtils.mkdir_p(File.dirname(target_path)) + FileUtils.cp(source_path, target_path) + copy_content = file.fetch("copy_content", true) + replace_variables(target_path, @config["variables"]) if copy_content end end end private +# This method: +# - Creates a hash of source paths to their targets +# - Removes leading slashes for consistency +# - Handles directories differently from files +# - For directories: stores true to indicate it's a directory that should be copied +# For files: stores the specific target path + def build_file_mappings + mappings = {} + @config["files"].each do |file| + source_path = file["source"].sub(/^\//, '') # Removes leading slashes for consistency + if File.directory?(File.join(@template_dir, source_path)) + mappings[source_path] = true # Directories are marked as true + else + mappings[source_path] = file["target"] # Files store their target path + end + end + mappings + end + +# This method: +# - Converts the full path to a relative path +# - Checks if the path matches any configured source +# - For directories (ending with '/'): checks if the path is within that directory +# - For files: checks for exact matches +# - Returns false if no match is found + def should_copy_path?(path) + relative_path = path.sub(@template_dir, '').sub(/^\//, '') + + @file_mappings.each do |source, target| + if source.end_with?('/') + return true if relative_path.start_with?(source) + else + return true if relative_path == source + end + end + + false + end + + def get_target_path(source_path) + relative_path = source_path.sub(@template_dir, '').sub(/^\//, '') + @config["files"].each do |file| + if file["source"] == relative_path + return File.join(@target_dir, file["target"]) + end + end + nil + end + + def sync_directory(source_dir, target_dir, config_entry) + return unless File.directory?(source_dir) + + Dir.foreach(source_dir) do |item| + next if item == '.' || item == '..' + + source_path = File.join(source_dir, item) + + if specific_target = get_target_path(source_path) + target_path = specific_target + else + target_path = File.join(target_dir, item) + end + + next unless should_copy_path?(source_path) + + if File.directory?(source_path) + FileUtils.mkdir_p(target_path) unless Dir.exist?(target_path) + sync_directory(source_path, target_path, config_entry) + elsif !File.exist?(target_path) + FileUtils.mkdir_p(File.dirname(target_path)) + FileUtils.cp(source_path, target_path) + end + end + end + def read_config JSON.parse(File.read(@config_file)) end @@ -37,12 +134,21 @@ def evaluate_condition(condition, variables) true end - def copy_item(source, target, copy_content) + def copy_item(source, target, copy_content, config_entry) if File.directory?(source) FileUtils.mkdir_p(target) Dir.foreach(source) do |item| next if item == '.' || item == '..' - copy_item(File.join(source, item), File.join(target, item), copy_content) + source_path = File.join(source, item) + + if specific_target = get_target_path(source_path) + target_path = specific_target + else + target_path = File.join(target, item) + end + + next unless should_copy_path?(source_path) + copy_item(source_path, target_path, copy_content, config_entry) end else FileUtils.mkdir_p(File.dirname(target)) diff --git a/solara/lib/solara_manager.rb b/solara/lib/solara_manager.rb index 890e421..ca1c101 100644 --- a/solara/lib/solara_manager.rb +++ b/solara/lib/solara_manager.rb @@ -40,7 +40,7 @@ def onboard(brand_key, brand_name, init: false, clone_brand_key: nil, open_dashb return end - BrandOnboarder.new(brand_key, brand_name, clone_brand_key: clone_brand_key).onboard + BrandOnboarder.new.onboard(brand_key, brand_name, clone_brand_key: clone_brand_key) switch(brand_key, ignore_health_check: true) @@ -78,4 +78,8 @@ def doctor(brand_key = nil, print_logs: true) DoctorManager.new.visit_brands(keys, print_logs: print_logs) end + def sync_brand_with_template(brand_key) + BrandOnboarder.new.sync_with_template(brand_key) + end + end \ No newline at end of file