solara 0.4.0 → 0.6.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/solara/lib/.DS_Store +0 -0
  3. data/solara/lib/core/.DS_Store +0 -0
  4. data/solara/lib/core/brands/brand_onboarder.rb +1 -1
  5. data/solara/lib/core/brands/brand_switcher.rb +92 -1
  6. data/solara/lib/core/dashboard/brand/BrandDetail.js +34 -2
  7. data/solara/lib/core/dashboard/brand/BrandDetailController.js +27 -234
  8. data/solara/lib/core/dashboard/brand/BrandDetailModel.js +14 -5
  9. data/solara/lib/core/dashboard/brand/BrandDetailView.js +16 -200
  10. data/solara/lib/core/dashboard/brand/SectionsFormManager.js +293 -0
  11. data/solara/lib/core/dashboard/brand/brand.html +223 -174
  12. data/solara/lib/core/dashboard/brand/source/BrandLocalSource.js +2 -5
  13. data/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js +36 -133
  14. data/solara/lib/core/dashboard/brands/Brands.js +31 -0
  15. data/solara/lib/core/dashboard/brands/BrandsController.js +0 -5
  16. data/solara/lib/core/dashboard/brands/BrandsView.js +2 -2
  17. data/solara/lib/core/dashboard/brands/brands.html +71 -52
  18. data/solara/lib/core/dashboard/component/AliasesBottomSheet.js +6 -6
  19. data/solara/lib/core/dashboard/component/BrandOptionsBottomSheet.js +4 -4
  20. data/solara/lib/core/dashboard/component/ConfirmationDialog.js +15 -10
  21. data/solara/lib/core/dashboard/component/EditJsonSheet.js +160 -0
  22. data/solara/lib/core/dashboard/component/MessageBottomSheet.js +5 -5
  23. data/solara/lib/core/dashboard/component/OnboardBrandBottomSheet.js +9 -3
  24. data/solara/lib/core/dashboard/handler/base_handler.rb +1 -0
  25. data/solara/lib/core/dashboard/handler/edit_section_handler.rb +1 -5
  26. data/solara/lib/core/dashboard/handler/onboard_brand_handler.rb +0 -15
  27. data/solara/lib/core/doctor/schema/brand_configurations.json +0 -8
  28. data/solara/lib/core/doctor/schema/platform/global/resources_manifest.json +30 -0
  29. data/solara/lib/core/doctor/schema/platform/json_manifest.json +39 -0
  30. data/solara/lib/core/doctor/validator/template/android_template_validation_config.yml +35 -1
  31. data/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml +30 -1
  32. data/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml +35 -1
  33. data/solara/lib/core/doctor/validator/template/template_validator.rb +9 -9
  34. data/solara/lib/core/scripts/brand_config_manager.rb +1 -1
  35. data/solara/lib/core/scripts/brand_configurations_manager.rb +41 -0
  36. data/solara/lib/core/scripts/code_generator.rb +342 -118
  37. data/solara/lib/core/scripts/file_manager.rb +11 -15
  38. data/solara/lib/core/scripts/file_path.rb +21 -1
  39. data/solara/lib/core/scripts/gitignore_manager.rb +12 -6
  40. data/solara/lib/core/scripts/json_manifest_processor.rb +136 -0
  41. data/solara/lib/core/scripts/platform/ios/infoplist_string_catalog_manager.rb +11 -1
  42. data/solara/lib/core/scripts/resource_manifest_processor.rb +151 -0
  43. data/solara/lib/core/scripts/solara_status_manager.rb +1 -1
  44. data/solara/lib/core/scripts/theme_generator.rb +21 -242
  45. data/solara/lib/core/solara_configurator.rb +1 -1
  46. data/solara/lib/core/template/brands/global/resources_manifest.json +10 -0
  47. data/solara/lib/core/template/brands/json/Json-Manifest.md +59 -0
  48. data/solara/lib/core/template/brands/json/json_manifest.json +16 -0
  49. data/solara/lib/core/template/brands/shared/theme.json +213 -29
  50. data/solara/lib/core/template/config/android_template_config.json +50 -0
  51. data/solara/lib/core/template/config/flutter_template_config.json +35 -0
  52. data/solara/lib/core/template/config/ios_template_config.json +50 -0
  53. data/solara/lib/core/template/configurations.json +46 -0
  54. data/solara/lib/core/template/project_template_generator.rb +2 -0
  55. data/solara/lib/solara/version.rb +1 -1
  56. data/solara/lib/solara.rb +19 -0
  57. data/solara/lib/solara_manager.rb +21 -13
  58. metadata +13 -4
  59. data/solara/lib/core/dashboard/component/AddFieldSheet.js +0 -175
  60. data/solara/lib/core/dashboard/handler/brand_configurations_manager.rb +0 -73
@@ -5,23 +5,19 @@ class FileManager
5
5
  source_path = Pathname.new(source_dir).expand_path
6
6
  destination_path = Pathname.new(destination_dir).expand_path
7
7
 
8
- if Dir.exist?(source_path)
9
- Dir.glob(source_path.join('*')).each do |item|
10
- relative_path = Pathname.new(item).relative_path_from(source_path).to_s
11
- destination_item_path = destination_path.join(relative_path)
12
-
13
- if File.directory?(item)
14
- FileUtils.mkdir_p(destination_item_path)
15
- FileUtils.cp_r(item + '/.', destination_item_path) # Ensure to copy contents
16
- else
17
- FileUtils.mkdir_p(destination_item_path.dirname) # Create parent directory
18
- FileUtils.cp(item, destination_item_path)
19
- end
8
+ Dir.glob(source_path.join('*')).each do |item|
9
+ relative_path = Pathname.new(item).relative_path_from(source_path).to_s
10
+ destination_item_path = destination_path.join(relative_path)
20
11
 
21
- Solara.logger.debug("🚗 Copied #{relative_path} \n\t↑ From: #{source_path} \n\t↓ To: #{destination_path}")
22
- end
12
+ if File.directory?(item)
13
+ FileUtils.mkdir_p(destination_item_path)
14
+ FileUtils.cp_r(item + '/.', destination_item_path) # Ensure to copy contents
23
15
  else
24
- Solara.logger.failure("#{source_path} not found!")
16
+ FileUtils.mkdir_p(destination_item_path.dirname) # Create parent directory
17
+ FileUtils.cp(item, destination_item_path)
18
+ end
19
+
20
+ Solara.logger.debug("🚗 Copied #{relative_path} \n\t↑ From: #{source_path} \n\t↓ To: #{destination_path}")
25
21
  end
26
22
  end
27
23
 
@@ -99,6 +99,14 @@ module FilePath
99
99
  File.join(brands, brand_key)
100
100
  end
101
101
 
102
+ def self.brand_json_dir(brand_key, platform = nil)
103
+ File.join(brand(brand_key), platform || SolaraSettingsManager.instance.platform, 'json')
104
+ end
105
+
106
+ def self.brand_global_json_dir
107
+ File.join(global, 'json')
108
+ end
109
+
102
110
  def self.android_config(brand_key)
103
111
  File.join(android_brand_root(brand_key), 'android_config.json')
104
112
  end
@@ -116,7 +124,15 @@ module FilePath
116
124
  end
117
125
 
118
126
  def self.brand_fonts
119
- File.join(solara_brand, 'global', 'fonts')
127
+ File.join(global, 'fonts')
128
+ end
129
+
130
+ def self.resources_manifest
131
+ File.join(global, 'resources_manifest.json')
132
+ end
133
+
134
+ def self.global
135
+ File.join(solara_brand, 'global')
120
136
  end
121
137
 
122
138
  def self.brand_config(brand_key)
@@ -287,6 +303,10 @@ module FilePath
287
303
  File.join(root, 'core', 'template')
288
304
  end
289
305
 
306
+ def self.brand_configurations
307
+ File.join(solara_template, 'configurations.json')
308
+ end
309
+
290
310
  def self.solara_aliases_json
291
311
  File.join(dot_solara, 'aliases', 'aliases.json')
292
312
  end
@@ -4,7 +4,7 @@ class GitignoreManager
4
4
  create_gitignore_if_not_exists
5
5
  end
6
6
 
7
- def self.ignore
7
+ def self.ignore_common_files
8
8
  Solara.logger.start_step("Exclude Brand-Generated Files and Folders from Git")
9
9
 
10
10
  items = [
@@ -18,9 +18,7 @@ class GitignoreManager
18
18
  ]
19
19
 
20
20
  if Platform.is_flutter || Platform.is_ios
21
- items << FileManager.get_relative_path_to_root(FilePath.project_infoplist_string_catalog)
22
- # The excluded InfoPlist.xcstrings maybe at the root. In this case we have to avoid ignoring the brands files.
23
- items << '!solara/brand/brands/**/InfoPlist.xcstrings'
21
+ items << "/#{FileManager.get_relative_path_to_root(FilePath.project_infoplist_string_catalog)}"
24
22
  end
25
23
 
26
24
  GitignoreManager.new(FilePath.project_root).add_items(items)
@@ -29,7 +27,7 @@ class GitignoreManager
29
27
 
30
28
  def add_items(items)
31
29
  items.each do |item|
32
- add_item(item)
30
+ add_item(FileManager.get_relative_path_to_root(item))
33
31
  end
34
32
  end
35
33
 
@@ -39,7 +37,15 @@ class GitignoreManager
39
37
  if existing_items.include?(item)
40
38
  Solara.logger.debug("'#{item}' already exists in .gitignore")
41
39
  else
42
- File.open(@gitignore_path, 'a') do |file|
40
+ File.open(@gitignore_path, 'a+') do |file|
41
+ # Move the file pointer to the beginning to check the last character
42
+ file.seek(0, IO::SEEK_END)
43
+ if file.size > 0
44
+ # Only add a new line if the last character is not a newline
45
+ file.seek(-1, IO::SEEK_END)
46
+ last_char = file.getc
47
+ file.puts if last_char != "\n"
48
+ end
43
49
  file.puts(item)
44
50
  end
45
51
  Solara.logger.debug("Added '#{item}' to .gitignore")
@@ -0,0 +1,136 @@
1
+ require 'json'
2
+
3
+ class JsonManifestProcessor
4
+ def initialize(json_path, language, output_path)
5
+ @json_path = json_path
6
+ @language = language
7
+ @output_path = output_path
8
+ @manifest = read_manifest
9
+ end
10
+
11
+ def process
12
+ # First process files specified in manifest
13
+ process_manifest_files if @manifest && @manifest['files']
14
+
15
+ # Then process remaining JSON files
16
+ process_remaining_files
17
+ end
18
+
19
+ private
20
+
21
+ def read_manifest
22
+ manifest_path = File.join(@json_path, 'json_manifest.json')
23
+ JSON.parse(File.read(manifest_path))
24
+ rescue JSON::ParserError => e
25
+ Solara.logger.debug("Error parsing manifest JSON: #{e.message}")
26
+ nil
27
+ rescue Errno::ENOENT => e
28
+ Solara.logger.debug("Manifest file not found: #{e.message}")
29
+ nil
30
+ rescue StandardError => e
31
+ Solara.logger.debug("Unexpected error reading manifest: #{e.message}")
32
+ nil
33
+ end
34
+
35
+ def process_manifest_files
36
+ @manifest['files'].each do |file|
37
+ process_manifest_file(file)
38
+ end
39
+ end
40
+
41
+ def process_manifest_file(file)
42
+ return unless file['generate']
43
+
44
+ file_name = file['fileName']
45
+ class_name = file['parentClassName']
46
+
47
+ return if file_name.empty? || class_name.empty?
48
+
49
+ custom_class_names = convert_to_map(file['customClassNames'])
50
+ process_json_file(
51
+ File.join(@json_path, file_name),
52
+ class_name,
53
+ custom_class_names,
54
+ true
55
+ )
56
+ end
57
+
58
+ def process_remaining_files
59
+ manifest_files = @manifest&.dig('files')&.map { |f| f['fileName'] } || []
60
+
61
+ json_files = get_json_files
62
+ json_files.each do |file_path|
63
+ file_name = File.basename(file_path)
64
+ # Skip files that were already processed via manifest
65
+ next if manifest_files.include?(file_name)
66
+ next if file_name == 'json_manifest.json'
67
+
68
+ class_name = derive_class_name(file_name)
69
+ process_json_file(file_path, class_name, {}, false)
70
+ end
71
+ end
72
+
73
+ def get_json_files
74
+ Dir.glob(File.join(@json_path, '**', '*.json'))
75
+ rescue StandardError => e
76
+ Solara.logger.debug("Error reading directory #{@json_path}: #{e.message}")
77
+ []
78
+ end
79
+
80
+ def process_json_file(file_path, class_name, custom_types, is_manifest_file)
81
+ begin
82
+ json_content = JSON.parse(File.read(file_path))
83
+ code_generator = CodeGenerator.new(
84
+ json: json_content,
85
+ language: @language,
86
+ parent_class_name: class_name,
87
+ custom_types: custom_types
88
+ )
89
+
90
+ generated_code = code_generator.generate
91
+ output_path = File.join(@output_path, generated_filename(class_name))
92
+ write_to_file(output_path, generated_code)
93
+ rescue JSON::ParserError => e
94
+ Solara.logger.debug("Error parsing JSON file #{File.basename(file_path)}: #{e.message}")
95
+ rescue StandardError => e
96
+ Solara.logger.debug("Error processing file #{File.basename(file_path)}: #{e.message}")
97
+ end
98
+ end
99
+
100
+ def derive_class_name(file_name)
101
+ # Remove .json extension and convert to PascalCase
102
+ base_name = File.basename(file_name, '.json')
103
+ base_name.split('_').map(&:capitalize).join
104
+ end
105
+
106
+ def convert_to_map(custom_class_names)
107
+ return {} unless custom_class_names
108
+ custom_class_names.each_with_object({}) do |item, result|
109
+ result[item['originalName']] = item['customName']
110
+ end
111
+ end
112
+
113
+ def generated_filename(class_name)
114
+ case SolaraSettingsManager.instance.platform
115
+ when Platform::Flutter
116
+ "#{to_snake_case(class_name)}.dart"
117
+ when Platform::IOS
118
+ "#{class_name}.swift"
119
+ when Platform::Android
120
+ "#{class_name}.kt"
121
+ else
122
+ raise ArgumentError, "Invalid platform: #{@platform}"
123
+ end
124
+ end
125
+
126
+ def to_snake_case(string)
127
+ string.gsub(/[A-Z]/) { |match| "_#{match.downcase}" }.sub(/^_/, '')
128
+ end
129
+
130
+ def write_to_file(output, content)
131
+ File.write(output, content)
132
+ Solara.logger.debug("Generated #{output}")
133
+ rescue StandardError => e
134
+ Solara.logger.debug("Error writing to file #{output}: #{e.message}")
135
+ end
136
+ end
@@ -2,12 +2,22 @@ require 'json'
2
2
 
3
3
  module StringCatalogUtils
4
4
  def load_string_catalog(path)
5
+ @path = path
5
6
  JSON.parse(File.read(path))
6
7
  end
7
8
 
8
9
  def get_value(data, key, target, language)
9
10
  lang = language || data['sourceLanguage']
10
- data['strings'][key]['localizations'][lang]['stringUnit'][target]
11
+ localizations = data.dig('strings', key, 'localizations', lang)
12
+
13
+ unless localizations && localizations['stringUnit']
14
+ error_message = "The default language is #{lang}, but no localizations are available for key '#{key}'. Please address this issue in {@path}. You can easily open the file in Xcode to make the necessary adjustments."
15
+ Solara.logger.fatal(error_message)
16
+ exit 1
17
+ end
18
+
19
+ string_unit = localizations['stringUnit']
20
+ string_unit[target]
11
21
  end
12
22
 
13
23
  def has_value?(data, key, language)
@@ -0,0 +1,151 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+
4
+ class ResourceManifestProcessor
5
+ def initialize(brand_key, ignore_health_check:)
6
+ @brand_key = brand_key
7
+ @ignore_health_check = ignore_health_check
8
+ @manifest_file = FilePath.resources_manifest
9
+ @config = load_manifest_file
10
+ end
11
+
12
+ def copy
13
+ @base_source_path = FilePath.brands
14
+ @base_destination_path = FilePath.project_root
15
+ @config['files'].each do |item|
16
+ process_file_item(item)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def clean(item, src, dst)
23
+ paths = destinations(item, src, dst)
24
+ paths.each do |path|
25
+ File.delete(path) if File.exist?(path)
26
+ end
27
+ end
28
+
29
+ def process_file_item(item)
30
+ src = resolve_source_path(@brand_key, item['source'], @base_source_path)
31
+ dst = File.join(@base_destination_path, item['destination'])
32
+
33
+ clean(item, src, dst)
34
+
35
+ return skip_empty_paths(item) if empty_paths?(item)
36
+
37
+ check_mandatory_file(item, src)
38
+
39
+ if File.exist?(src)
40
+ copy_file(src, dst)
41
+ git_ignore(destinations(item, src, dst))
42
+ else
43
+ log_file_not_found(item)
44
+ end
45
+ end
46
+
47
+ def destinations(item, src, dst, visited = {})
48
+ return [] if visited[src]
49
+
50
+ visited[src] = true # Mark the current source as visited
51
+
52
+ if File.file?(src)
53
+ if File.directory?(dst)
54
+ return [File.join(dst, File.basename(src))]
55
+ else
56
+ return [dst]
57
+ end
58
+ elsif File.directory?(src)
59
+ return destinations_of_directory_contents(src, dst)
60
+ end
61
+
62
+ if !item['mandatory'] && !File.file?(src)
63
+ return get_optional_resource_destination(item, dst, visited)
64
+ end
65
+
66
+ []
67
+ end
68
+
69
+ def get_optional_resource_destination(item, dst, visited)
70
+ return [] if item['mandatory']
71
+ keys = BrandsManager.instance.brands_list.map { |brand| brand['key'] }
72
+ keys.each do |key|
73
+ src = resolve_source_path(key, item['source'], @base_source_path)
74
+ result = destinations(item, src, dst, visited)
75
+ return result unless result.empty?
76
+ end
77
+ []
78
+ end
79
+
80
+ def destinations_of_directory_contents(src_dir, dst_dir)
81
+ items = []
82
+ Dir.foreach(src_dir) do |file|
83
+ next if file == '.' || file == '..'
84
+ full_dst_path = File.join(dst_dir, file)
85
+ items << full_dst_path
86
+ end
87
+ items
88
+ end
89
+
90
+ def git_ignore(files)
91
+ files.each do |file|
92
+ GitignoreManager.new(FilePath.project_root).add_items(["/#{file}"])
93
+ end
94
+ end
95
+
96
+ def resolve_source_path(brand_key, source, base_source_path)
97
+ source.gsub(/\{.*?\}/, brand_key).prepend(base_source_path + '/')
98
+ end
99
+
100
+ def empty_paths?(item)
101
+ item['source'].empty? || item['destination'].empty?
102
+ end
103
+
104
+ def skip_empty_paths(item)
105
+ Solara.logger.debug("Skipped (empty source or destination) for #{@brand_key}: #{item['source']} -> #{item['destination']}")
106
+ end
107
+
108
+ def check_mandatory_file(item, src)
109
+ return if @ignore_health_check
110
+
111
+ if item['mandatory'] && !File.exist?(src)
112
+ raise "Mandatory resource file/folder not found for #{@brand_key}: #{src}. Please add the resource or mark it as not mandatory in #{FilePath.resources_manifest}."
113
+ end
114
+
115
+ end
116
+
117
+ def copy_file(src, dst)
118
+ if File.directory?(src)
119
+ FileUtils.mkdir_p(dst)
120
+ FileUtils.cp_r(File.join(src, '.'), dst)
121
+ else
122
+ FileUtils.mkdir_p(File.dirname(dst))
123
+ FileUtils.cp(src, dst)
124
+ end
125
+ Solara.logger.debug("Copied resource for #{@brand_key}: #{File.basename(src)} to #{File.basename(dst)}")
126
+ end
127
+
128
+ def log_file_not_found(item)
129
+ Solara.logger.debug("Skipped resource (not found) for #{@brand_key}: #{item['source']}")
130
+ end
131
+
132
+ def load_manifest_file
133
+ validate_manifest_file_existence
134
+ parse_manifest_file
135
+ end
136
+
137
+ def validate_manifest_file_existence
138
+ unless File.exist?(@manifest_file)
139
+ raise "Resources manifest not found for #{@brand_key}: #{@manifest_file}"
140
+ end
141
+ end
142
+
143
+ def parse_manifest_file
144
+ begin
145
+ JSON.parse(File.read(@manifest_file))
146
+ rescue JSON::ParserError => e
147
+ raise "Invalid resources manifest for #{@brand_key}: #{e.message}"
148
+ end
149
+ end
150
+
151
+ end
@@ -37,7 +37,7 @@ class SolaraStatusManager
37
37
 
38
38
  solara dashboard -k #{current_brand['key']}
39
39
 
40
- Then, click the "Apply Changes" button.
40
+ Then, click the "Sync" button.
41
41
  MESSAGE
42
42
  Solara.logger.info(message)
43
43
  end