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.
- checksums.yaml +4 -4
- data/solara/lib/.DS_Store +0 -0
- data/solara/lib/core/.DS_Store +0 -0
- data/solara/lib/core/brands/brand_onboarder.rb +1 -1
- data/solara/lib/core/brands/brand_switcher.rb +92 -1
- data/solara/lib/core/dashboard/brand/BrandDetail.js +34 -2
- data/solara/lib/core/dashboard/brand/BrandDetailController.js +27 -234
- data/solara/lib/core/dashboard/brand/BrandDetailModel.js +14 -5
- data/solara/lib/core/dashboard/brand/BrandDetailView.js +16 -200
- data/solara/lib/core/dashboard/brand/SectionsFormManager.js +293 -0
- data/solara/lib/core/dashboard/brand/brand.html +223 -174
- data/solara/lib/core/dashboard/brand/source/BrandLocalSource.js +2 -5
- data/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js +36 -133
- data/solara/lib/core/dashboard/brands/Brands.js +31 -0
- data/solara/lib/core/dashboard/brands/BrandsController.js +0 -5
- data/solara/lib/core/dashboard/brands/BrandsView.js +2 -2
- data/solara/lib/core/dashboard/brands/brands.html +71 -52
- data/solara/lib/core/dashboard/component/AliasesBottomSheet.js +6 -6
- data/solara/lib/core/dashboard/component/BrandOptionsBottomSheet.js +4 -4
- data/solara/lib/core/dashboard/component/ConfirmationDialog.js +15 -10
- data/solara/lib/core/dashboard/component/EditJsonSheet.js +160 -0
- data/solara/lib/core/dashboard/component/MessageBottomSheet.js +5 -5
- data/solara/lib/core/dashboard/component/OnboardBrandBottomSheet.js +9 -3
- data/solara/lib/core/dashboard/handler/base_handler.rb +1 -0
- data/solara/lib/core/dashboard/handler/edit_section_handler.rb +1 -5
- data/solara/lib/core/dashboard/handler/onboard_brand_handler.rb +0 -15
- data/solara/lib/core/doctor/schema/brand_configurations.json +0 -8
- data/solara/lib/core/doctor/schema/platform/global/resources_manifest.json +30 -0
- data/solara/lib/core/doctor/schema/platform/json_manifest.json +39 -0
- data/solara/lib/core/doctor/validator/template/android_template_validation_config.yml +35 -1
- data/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml +30 -1
- data/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml +35 -1
- data/solara/lib/core/doctor/validator/template/template_validator.rb +9 -9
- data/solara/lib/core/scripts/brand_config_manager.rb +1 -1
- data/solara/lib/core/scripts/brand_configurations_manager.rb +41 -0
- data/solara/lib/core/scripts/code_generator.rb +342 -118
- data/solara/lib/core/scripts/file_manager.rb +11 -15
- data/solara/lib/core/scripts/file_path.rb +21 -1
- data/solara/lib/core/scripts/gitignore_manager.rb +12 -6
- data/solara/lib/core/scripts/json_manifest_processor.rb +136 -0
- data/solara/lib/core/scripts/platform/ios/infoplist_string_catalog_manager.rb +11 -1
- data/solara/lib/core/scripts/resource_manifest_processor.rb +151 -0
- data/solara/lib/core/scripts/solara_status_manager.rb +1 -1
- data/solara/lib/core/scripts/theme_generator.rb +21 -242
- data/solara/lib/core/solara_configurator.rb +1 -1
- data/solara/lib/core/template/brands/global/resources_manifest.json +10 -0
- data/solara/lib/core/template/brands/json/Json-Manifest.md +59 -0
- data/solara/lib/core/template/brands/json/json_manifest.json +16 -0
- data/solara/lib/core/template/brands/shared/theme.json +213 -29
- data/solara/lib/core/template/config/android_template_config.json +50 -0
- data/solara/lib/core/template/config/flutter_template_config.json +35 -0
- data/solara/lib/core/template/config/ios_template_config.json +50 -0
- data/solara/lib/core/template/configurations.json +46 -0
- data/solara/lib/core/template/project_template_generator.rb +2 -0
- data/solara/lib/solara/version.rb +1 -1
- data/solara/lib/solara.rb +19 -0
- data/solara/lib/solara_manager.rb +21 -13
- metadata +13 -4
- data/solara/lib/core/dashboard/component/AddFieldSheet.js +0 -175
- 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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
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(
|
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.
|
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
|
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
|