solara 0.6.0 → 0.7.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 (33) 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 +29 -25
  5. data/solara/lib/core/brands/brand_switcher.rb +1 -0
  6. data/solara/lib/core/dashboard/brand/BrandDetailController.js +1 -1
  7. data/solara/lib/core/dashboard/brand/SectionsFormManager.js +3 -3
  8. data/solara/lib/core/dashboard/brand/brand.html +2 -2
  9. data/solara/lib/core/dashboard/brand/source/BrandLocalSource.js +2 -2
  10. data/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js +19 -3
  11. data/solara/lib/core/dashboard/handler/edit_section_handler.rb +5 -17
  12. data/solara/lib/core/doctor/schema/brand_configurations.json +2 -2
  13. data/solara/lib/core/doctor/validator/template/android_template_validation_config.yml +3 -3
  14. data/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml +2 -2
  15. data/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml +3 -3
  16. data/solara/lib/core/scripts/brand_config_updater.rb +29 -0
  17. data/solara/lib/core/scripts/brand_configurations_manager.rb +76 -14
  18. data/solara/lib/core/scripts/brand_importer.rb +9 -20
  19. data/solara/lib/core/scripts/code_generator.rb +1 -1
  20. data/solara/lib/core/scripts/gitignore_manager.rb +2 -1
  21. data/solara/lib/core/scripts/json_manifest_processor.rb +1 -1
  22. data/solara/lib/core/scripts/resource_manifest_processor.rb +2 -1
  23. data/solara/lib/core/scripts/string_case.rb +18 -0
  24. data/solara/lib/core/template/.DS_Store +0 -0
  25. data/solara/lib/core/template/brands/json/json_manifest.json +1 -1
  26. data/solara/lib/core/template/config/android_template_config.json +6 -6
  27. data/solara/lib/core/template/config/flutter_template_config.json +4 -4
  28. data/solara/lib/core/template/config/ios_template_config.json +6 -6
  29. data/solara/lib/core/template/configurations.json +34 -21
  30. data/solara/lib/core/template/project_template_generator.rb +119 -13
  31. data/solara/lib/solara/version.rb +1 -1
  32. data/solara/lib/solara_manager.rb +5 -1
  33. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acd7dd411828a00b018a7ead20fc5f21853c45682a7b21f419377385b7e5cbb4
4
- data.tar.gz: 187461f358743a466bcd31971622a32091fc4368264f51f73c7ef73b048ae6c3
3
+ metadata.gz: ee9572ad8162826e8ebd8ed6b9f5e6a0ba5c2ef23ecec56ed610206becc3e80f
4
+ data.tar.gz: d0cfbf2ba68d160fdf531f3148e216662063d2a9182c341ea22e1266e283bcf5
5
5
  SHA512:
6
- metadata.gz: f3e85254b364be2888f9e9a760967bf64e4672df8ae4dde907b6f8a8928afa7266ad9a26f1ff35867ae8b7fb2fc362aa0be6f6a70c7aafb3b29e83fa2c456e72
7
- data.tar.gz: ee5abac576555d1415bcaa33cec324bf21dcf0519b28eefc1687b6b129b6a74c1f160eb5f2b30f3037facda8ca163e0867c4a7acd8ecdfe2ef2ab88fcf84e330
6
+ metadata.gz: 7e2e2c181701aa9af6099694beedc3f68e926446e8a1d67b4841e66b5ecf1da8fad76c9b479a52738f0bf7a48551328b7058c0bbafcc1574a68b9b388b37035a
7
+ data.tar.gz: e3e2d47d57ed66593435bb6294013c99711177bfadb4ae25ea74d2ebb1d511abc0eb98dd7fa9f4579106cf7ec26cf76393294b7d4fd9b6c89aa0c679343ef8e3
data/solara/lib/.DS_Store CHANGED
Binary file
Binary file
@@ -5,43 +5,47 @@ Dir[File.expand_path('platform/ios/*.rb', __dir__)].each { |file| require_relati
5
5
  Dir[File.expand_path('platform/flutter/*.rb', __dir__)].each { |file| require_relative file }
6
6
 
7
7
  class BrandOnboarder
8
- def initialize(brand_key, brand_name, clone_brand_key: nil)
9
- @brand_key = brand_key
10
- @brand_name = brand_name
11
- @clone_brand_key = clone_brand_key
12
- end
13
8
 
14
- def onboard
15
- if @clone_brand_key.nil? || @clone_brand_key.strip.empty?
16
- generate_brand_template
9
+ def onboard(brand_key, brand_name, clone_brand_key: nil)
10
+ if clone_brand_key.nil? || clone_brand_key.strip.empty?
11
+ generate_from_template(brand_key)
17
12
  else
18
- clone_brand
13
+ clone_brand(brand_key, clone_brand_key: clone_brand_key)
19
14
  end
20
- add_to_brands_list
15
+ add_to_brands_list(brand_key, brand_name)
21
16
  end
22
17
 
23
- def generate_brand_template
24
- Solara.logger.debug("Onboarding #{@brand_key} from template.")
25
-
26
- template_dir = FilePath.template_brands
27
- target_dir = FilePath.brand(@brand_key)
28
- config_file = FilePath.template_config
29
-
30
- generator = ProjectTemplateGenerator.new(template_dir, target_dir, config_file)
31
- generator.create_project
18
+ def sync_with_template(brand_key)
19
+ generator = template_generator(brand_key)
20
+ generator.sync_with_template
32
21
  end
33
22
 
34
- def clone_brand
35
- Solara.logger.debug("Cloning #{@clone_brand_key} to #{@brand_key}")
36
- source = FilePath.brand(@clone_brand_key)
37
- destination = FilePath.brand(@brand_key)
23
+ def clone_brand(brand_key, clone_brand_key:)
24
+ Solara.logger.debug("Cloning #{clone_brand_key} to #{brand_key}")
25
+ source = FilePath.brand(clone_brand_key)
26
+ destination = FilePath.brand(brand_key)
38
27
 
39
28
  FileManager.delete_if_exists(destination)
40
29
  FolderCopier.new(source, destination).copy
41
30
  end
42
31
 
43
- def add_to_brands_list
44
- BrandsManager.instance.add_brand(@brand_name, @brand_key)
32
+ def add_to_brands_list(brand_key, brand_name)
33
+ BrandsManager.instance.add_brand(brand_name, brand_key)
34
+ end
35
+
36
+ def generate_from_template(brand_key)
37
+ Solara.logger.debug("Onboarding #{brand_key} from template.")
38
+
39
+ generator = template_generator(brand_key)
40
+ generator.create_project
41
+ end
42
+
43
+ def template_generator(brand_key)
44
+ template_dir = FilePath.template_brands
45
+ target_dir = FilePath.brand(brand_key)
46
+ config_file = FilePath.template_config
47
+
48
+ ProjectTemplateGenerator.new(template_dir, target_dir, config_file)
45
49
  end
46
50
 
47
51
  end
@@ -16,6 +16,7 @@ class BrandSwitcher
16
16
  def start
17
17
  Solara.logger.header("Switching to #{@brand_key}")
18
18
 
19
+ BrandOnboarder.new.sync_with_template(@brand_key)
19
20
  @health_checker.check_health
20
21
  BrandsManager.instance.save_current_brand(@brand_key)
21
22
  @artifacts_switcher.switch
@@ -158,7 +158,7 @@ class BrandDetailController {
158
158
  if (this.model.isRemote()) return
159
159
 
160
160
  try {
161
- await this.model.saveSection(container.dataset.key, section.content);
161
+ await this.model.saveSection(container.dataset.filename, section.content);
162
162
  await this.checkBrandHealth();
163
163
  if (this.model.isCurrentBrand) {
164
164
  await this.switchToBrand(true)
@@ -22,7 +22,7 @@ class SectionsFormManager {
22
22
  const sectionElement = document.createElement('div');
23
23
  sectionElement.className = 'section';
24
24
 
25
- sectionElement.dataset.key = section.key
25
+ sectionElement.dataset.filename = section.filename
26
26
  sectionElement.dataset.name = section.name
27
27
 
28
28
  const titleContainer = document.createElement('div');
@@ -35,7 +35,7 @@ class SectionsFormManager {
35
35
 
36
36
  sectionElement.appendChild(titleContainer);
37
37
 
38
- sectionElement.id = section.key;
38
+ sectionElement.id = section.filename;
39
39
 
40
40
  this.sectionsContainer.appendChild(sectionElement);
41
41
 
@@ -61,7 +61,7 @@ class SectionItemManager {
61
61
  displayJSONCards() {
62
62
  let cardContent = this.container.querySelector('.card-content')
63
63
  if (cardContent !== null) this.container.innerHTML = ''
64
- this.container.appendChild(this.createCard(this.section.content, 'root', null, this.section.key));
64
+ this.container.appendChild(this.createCard(this.section.content, 'root', null, this.section.filename));
65
65
  }
66
66
 
67
67
  createCard(obj, key, parent, cardTitle) {
@@ -90,7 +90,7 @@
90
90
  .right {
91
91
  width: 15%;
92
92
  position: fixed;
93
- max-height: 50%;
93
+ max-height: 95%;
94
94
  overflow-y: auto;
95
95
  padding: 7px;
96
96
  right: 0;
@@ -253,7 +253,7 @@
253
253
  #error-button {
254
254
  position: fixed;
255
255
  bottom: 14px;
256
- right: 14px;
256
+ left: 14px;
257
257
  background-color: #ff4136;
258
258
  color: white;
259
259
  border: none;
@@ -43,13 +43,13 @@ class BrandLocalSource {
43
43
  }
44
44
  }
45
45
 
46
- async saveSection(key, configuration, brandKey) {
46
+ async saveSection(filename, configuration, brandKey) {
47
47
  if (this.savingInProgress) return;
48
48
  this.savingInProgress = true;
49
49
 
50
50
  const dataToSend = {
51
51
  brand_key: brandKey,
52
- key: key,
52
+ filename: filename,
53
53
  data: configuration
54
54
  };
55
55
 
@@ -5,7 +5,7 @@ class BrandRemoteSource {
5
5
  }
6
6
 
7
7
  async createNewBrandConfigurations() {
8
- const url = 'https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/configurations.json';
8
+ const url = 'https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/configurations.json';
9
9
 
10
10
  try {
11
11
  const response = await fetch(url);
@@ -21,8 +21,8 @@ class BrandRemoteSource {
21
21
  }
22
22
  const content = await contentResponse.json();
23
23
  return {
24
- key: config.key,
25
- name: config.name,
24
+ filename: config.filename,
25
+ name: this.snakeToCapitalizedSpaced(config.filename, 'ios'),
26
26
  content: content
27
27
  };
28
28
  });
@@ -36,6 +36,22 @@ class BrandRemoteSource {
36
36
  }
37
37
  }
38
38
 
39
+ // TODO: should be ecnapsulated
40
+ snakeToCapitalizedSpaced(
41
+ snakeCaseString,
42
+ exclude = '',
43
+ transform = (item) => item.charAt(0).toUpperCase() + item.slice(1)
44
+ ) {
45
+ // Split by underscores
46
+ const parts = snakeCaseString.split('_').map(item => {
47
+ // Return the item as-is if it matches the exclude value
48
+ return item === exclude ? item : transform(item);
49
+ });
50
+
51
+ // Join the parts with a space
52
+ return parts.join(' ');
53
+ }
54
+
39
55
  async getBrandConfigurationsJsonFromDirectory(dirHandle) {
40
56
  const schema = await this.fetchBrandConfigurationsSchema();
41
57
 
@@ -7,14 +7,14 @@ class EditSectionHandler < BaseHandler
7
7
  begin
8
8
  request_payload = JSON.parse(req.body)
9
9
  brand_key = request_payload['brand_key']
10
- key = request_payload['key']
10
+ filename = request_payload['filename']
11
11
  data = request_payload['data']
12
12
 
13
- update_section(key, data, brand_key)
13
+ update_section(filename, data, brand_key)
14
14
  set_current_brand_content_changed(brand_key, true)
15
15
 
16
16
  res.status = 200
17
- res.body = JSON.generate({ success: true, message: "Configuration for #{key} updated successfully" })
17
+ res.body = JSON.generate({ success: true, message: "Configuration for #{filename} updated successfully" })
18
18
  res.content_type = 'application/json'
19
19
  rescue JSON::ParserError => e
20
20
  handle_error(res, e, "Invalid JSON in request body", 400)
@@ -27,20 +27,8 @@ class EditSectionHandler < BaseHandler
27
27
  end
28
28
  end
29
29
 
30
- def update_section(key, data, brand_key)
31
- template = BrandConfigurationsManager.new(brand_key).template_with_key(key)
32
-
33
- path = template[:path]
34
-
35
- if File.exist?(path)
36
- File.write(path, JSON.pretty_generate(data))
37
- Solara.logger.debug("Updated Config for #{path}: #{data}")
38
- else
39
- raise "Config file not found: #{path}"
40
- end
41
- rescue StandardError => e
42
- Solara.logger.failure("Error updating section: #{e.message}")
43
- raise
30
+ def update_section(filename, data, brand_key)
31
+ BrandConfigUpdater.new.update(filename, data, brand_key)
44
32
  end
45
33
 
46
34
  def set_current_brand_content_changed(brand_key, changed)
@@ -25,7 +25,7 @@
25
25
  "items": {
26
26
  "type": "object",
27
27
  "properties": {
28
- "key": {
28
+ "filename": {
29
29
  "type": "string"
30
30
  },
31
31
  "name": {
@@ -37,7 +37,7 @@
37
37
  }
38
38
  },
39
39
  "required": [
40
- "key",
40
+ "filename",
41
41
  "name",
42
42
  "content"
43
43
  ]
@@ -28,7 +28,7 @@ structure:
28
28
  json:
29
29
  type: directory
30
30
  contents:
31
- json_manifest.json:
31
+ android_json_manifest.json:
32
32
  type: file
33
33
  validations:
34
34
  - type: valid_json
@@ -63,7 +63,7 @@ structure:
63
63
  json:
64
64
  type: directory
65
65
  contents:
66
- json_manifest.json:
66
+ ios_json_manifest.json:
67
67
  type: file
68
68
  validations:
69
69
  - type: valid_json
@@ -94,7 +94,7 @@ structure:
94
94
  json:
95
95
  type: directory
96
96
  contents:
97
- json_manifest.json:
97
+ global_json_manifest.json:
98
98
  type: file
99
99
  validations:
100
100
  - type: valid_json
@@ -5,7 +5,7 @@ structure:
5
5
  json:
6
6
  type: directory
7
7
  contents:
8
- json_manifest.json:
8
+ flutter_json_manifest.json:
9
9
  type: file
10
10
  validations:
11
11
  - type: valid_json
@@ -89,7 +89,7 @@ structure:
89
89
  json:
90
90
  type: directory
91
91
  contents:
92
- json_manifest.json:
92
+ global_json_manifest.json:
93
93
  type: file
94
94
  validations:
95
95
  - type: valid_json
@@ -28,7 +28,7 @@ structure:
28
28
  json:
29
29
  type: directory
30
30
  contents:
31
- json_manifest.json:
31
+ android_json_manifest.json:
32
32
  type: file
33
33
  validations:
34
34
  - type: valid_json
@@ -63,7 +63,7 @@ structure:
63
63
  json:
64
64
  type: directory
65
65
  contents:
66
- json_manifest.json:
66
+ ios_json_manifest.json:
67
67
  type: file
68
68
  validations:
69
69
  - type: valid_json
@@ -94,7 +94,7 @@ structure:
94
94
  json:
95
95
  type: directory
96
96
  contents:
97
- json_manifest.json:
97
+ global_json_manifest.json:
98
98
  type: file
99
99
  validations:
100
100
  - type: valid_json
@@ -0,0 +1,29 @@
1
+ class BrandConfigUpdater
2
+
3
+ def update(filename, data, brand_key)
4
+ template = BrandConfigurationsManager.new(brand_key).template_with_filename(filename)
5
+ if template.nil?
6
+ Solara.logger.debug("Can't find tempate for filename: #{filename}, with brand key: #{brand_key}")
7
+ return
8
+ end
9
+
10
+ path = template[:path]
11
+ json = JSON.pretty_generate(data)
12
+
13
+ begin
14
+ # Check if the file exists
15
+ if File.exist?(path)
16
+ # If it exists, update the file
17
+ File.write(path, json)
18
+ Solara.logger.debug("Updated Config for #{path}: #{data}")
19
+ else
20
+ # If it doesn't exist, create the file with the data
21
+ File.write(path, json)
22
+ Solara.logger.debug("Created Config for #{path}: #{data}")
23
+ end
24
+ rescue StandardError => e
25
+ Solara.logger.failure("Error updating #{brand_key} config file #{path}: #{e.message}")
26
+ raise
27
+ end
28
+ end
29
+ end
@@ -4,19 +4,12 @@ class BrandConfigurationsManager
4
4
  @brand_key = brand_key
5
5
  end
6
6
 
7
- def template_with_key(key)
8
- templates.select { |section| section[:key] === key }.first
7
+ def template_with_filename(filename)
8
+ templates.select { |template| template[:filename] === filename }.first
9
9
  end
10
10
 
11
11
  def templates
12
- configurations = JSON.parse(File.read(FilePath.brand_configurations))['configurations']
13
- configurations.map do |config|
14
- {
15
- key: config['key'],
16
- name: config['name'],
17
- path: "#{FilePath.brand(@brand_key)}/#{config['filePath']}"
18
- }
19
- end
12
+ TemplateManager.new(@brand_key).templates
20
13
  end
21
14
 
22
15
  def create
@@ -24,18 +17,87 @@ class BrandConfigurationsManager
24
17
 
25
18
  config_templates.map do |template|
26
19
  create_config_item(
27
- template[:key],
20
+ template[:filename],
28
21
  template[:name],
29
22
  template[:path]
30
23
  )
31
24
  end
32
25
  end
33
26
 
34
- def create_config_item(key, name, path)
27
+ def create_config_item(filename, name, path)
35
28
  {
36
- key: key,
29
+ filename: filename,
37
30
  name: name,
38
31
  content: JSON.parse(File.read(path)),
39
32
  }
40
33
  end
41
- end
34
+ end
35
+
36
+ class TemplateManager
37
+
38
+ def initialize(brand_key)
39
+ @brand_key = brand_key
40
+ end
41
+
42
+ def templates
43
+ result = parse_configurations
44
+ collect_json_templates.each do |template|
45
+ result << template unless result.any? { |r| r['filename'] == template['filename'] }
46
+ end
47
+ result.compact
48
+ rescue StandardError => e
49
+ Solara.logger.error("Failed to generate templates: #{e.message}")
50
+ []
51
+ end
52
+
53
+ private
54
+
55
+ def parse_configurations
56
+ configurations = JSON.parse(File.read(FilePath.brand_configurations))['configurations']
57
+ configurations.map do |config|
58
+ path = build_path(config['filePath'])
59
+ next unless File.exist?(path)
60
+
61
+ filename_without_extension = File.basename(config['filename'], '.json')
62
+
63
+ {
64
+ filename: File.basename(config['filename']),
65
+ name: StringCase.snake_to_capitalized_spaced(filename_without_extension, exclude: "ios"),
66
+ path: path
67
+ }
68
+ end
69
+ rescue StandardError => e
70
+ Solara.logger.error("Failed to parse configurations: #{e.message}")
71
+ []
72
+ end
73
+
74
+ def collect_json_templates
75
+ directories = [
76
+ FilePath.brand_global_json_dir,
77
+ FilePath.brand_json_dir(@brand_key)
78
+ ]
79
+
80
+ directories.flat_map do |dir|
81
+ get_json_files(dir).map do |file|
82
+ filename_without_extension = File.basename(file, '.json')
83
+
84
+ {
85
+ filename: File.basename(file),
86
+ name: StringCase.snake_to_capitalized_spaced(filename_without_extension, exclude: "ios"),
87
+ path: file
88
+ }
89
+ end
90
+ end
91
+ end
92
+
93
+ def get_json_files(dir)
94
+ Dir.glob(File.join(dir, '**', '*.json'))
95
+ rescue StandardError => e
96
+ Solara.logger.error("Error reading directory #{dir}: #{e.message}")
97
+ []
98
+ end
99
+
100
+ def build_path(file_path)
101
+ "#{FilePath.brand(@brand_key)}/#{file_path}"
102
+ end
103
+ end
@@ -26,8 +26,8 @@ class BrandImporter
26
26
  validate_json(configurations_path)
27
27
  validate_json_schema(configurations_path)
28
28
 
29
- configurations_json = JSON.parse(File.read(configurations_path))
30
- brand = configurations_json['brand']
29
+ configurations = JSON.parse(File.read(configurations_path))
30
+ brand = configurations['brand']
31
31
  brand_key = brand['key'] # Ensure to use 'key' instead of 'brand_key'
32
32
 
33
33
  exists = BrandsManager.instance.exists(brand_key)
@@ -35,32 +35,21 @@ class BrandImporter
35
35
  SolaraManager.new.onboard(brand_key, brand['name'], open_dashboard: false)
36
36
  end
37
37
 
38
- update_brand(brand_key, configurations_json)
38
+ SolaraManager.new.sync_brand_with_template(brand_key) if exists
39
+ update_brand(brand_key, configurations)
39
40
 
40
41
  message_suffix = exists ? "The existing brand '#{brand_key}' has been updated." : "A new brand with the key '#{brand_key}' has been onboarded."
41
42
  Solara.logger.success("Successfully imported (#{configurations_path}). #{message_suffix}")
42
43
  end
43
44
 
44
- def update_brand(brand_key, configurations_json)
45
- brand_path = FilePath.brand(brand_key)
46
-
47
- configurations_json['configurations'].each do |configuration|
48
- file_name = configuration['key']
49
- file_path = find_file_in_subdirectories(brand_path, file_name)
50
-
51
- # Create or replace the contents of the configuration file
52
- if file_path
53
- File.write(file_path, JSON.pretty_generate(configuration['content']))
54
- else
55
- Solara.logger.failure("File #{file_name} not found in #{brand_path}, ignoring importing it!")
56
- end
45
+ def update_brand(brand_key, configurations)
46
+ configurations['configurations'].each do |configuration|
47
+ filename = configuration['filename']
48
+ data = configuration['content']
49
+ BrandConfigUpdater.new.update(filename, data, brand_key)
57
50
  end
58
51
  end
59
52
 
60
- def find_file_in_subdirectories(base_path, file_name)
61
- Dir.glob(File.join(base_path, '**', file_name)).first
62
- end
63
-
64
53
  def validate_json(configurations_path)
65
54
  begin
66
55
  JsonFileValidator.new([configurations_path]).validate
@@ -212,7 +212,7 @@ class BaseCodeGenerator
212
212
  end
213
213
 
214
214
  def capitalize(string)
215
- "#{string[0].upcase}#{string[1..-1]}"
215
+ StringCase.capitalize(string)
216
216
  end
217
217
  end
218
218
 
@@ -27,11 +27,12 @@ class GitignoreManager
27
27
 
28
28
  def add_items(items)
29
29
  items.each do |item|
30
- add_item(FileManager.get_relative_path_to_root(item))
30
+ add_item(item.start_with?('/') ? item : FileManager.get_relative_path_to_root(item))
31
31
  end
32
32
  end
33
33
 
34
34
  def add_item(item)
35
+ puts item
35
36
  existing_items = read_gitignore
36
37
 
37
38
  if existing_items.include?(item)
@@ -19,7 +19,7 @@ class JsonManifestProcessor
19
19
  private
20
20
 
21
21
  def read_manifest
22
- manifest_path = File.join(@json_path, 'json_manifest.json')
22
+ manifest_path = File.join(@json_path, "#{SolaraSettingsManager.instance.platform}_json_manifest.json")
23
23
  JSON.parse(File.read(manifest_path))
24
24
  rescue JSON::ParserError => e
25
25
  Solara.logger.debug("Error parsing manifest JSON: #{e.message}")
@@ -89,7 +89,8 @@ class ResourceManifestProcessor
89
89
 
90
90
  def git_ignore(files)
91
91
  files.each do |file|
92
- GitignoreManager.new(FilePath.project_root).add_items(["/#{file}"])
92
+ path = FileManager.get_relative_path_to_root(file)
93
+ GitignoreManager.new(FilePath.project_root).add_items(["/#{path}"])
93
94
  end
94
95
  end
95
96
 
@@ -0,0 +1,18 @@
1
+ class StringCase
2
+
3
+ def self.capitalize(string)
4
+ "#{string[0].upcase}#{string[1..-1]}"
5
+ end
6
+
7
+ def self.snake_to_capitalized_spaced(snake_case_string, exclude: '', transform: ->(item) { StringCase.capitalize(item) })
8
+ # Split by underscores, then apply the transformation to each part
9
+ parts = snake_case_string.split('_').map do |item|
10
+ # Return the item as-is if it matches the exclude value
11
+ item == exclude ? item : transform.call(item)
12
+ end
13
+
14
+ # Join the parts with a space
15
+ parts.join(' ')
16
+ end
17
+
18
+ end
Binary file
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "This file specifies the JSON files that can be optionally generated for the target platform code.",
2
+ "description": "The JSON Manifest is intended to specify the files you need to customize its generated code",
3
3
  "files": [
4
4
  {
5
5
  "fileName": "",
@@ -72,22 +72,22 @@
72
72
  },
73
73
  {
74
74
  "source": "json/json_manifest.json",
75
- "target": "android/json/",
75
+ "target": "android/json/android_json_manifest.json",
76
76
  "condition": "true"
77
77
  },
78
78
  {
79
79
  "source": "json/json_manifest.json",
80
- "target": "ios/json/",
80
+ "target": "ios/json/ios_json_manifest.json",
81
81
  "condition": "true"
82
82
  },
83
83
  {
84
84
  "source": "json/Json-Manifest.md",
85
- "target": "android/json/",
85
+ "target": "android/json/Json-Manifest.md",
86
86
  "condition": "true"
87
87
  },
88
88
  {
89
89
  "source": "json/Json-Manifest.md",
90
- "target": "ios/json/",
90
+ "target": "ios/json/Json-Manifest.md",
91
91
  "condition": "true"
92
92
  },
93
93
  {
@@ -97,12 +97,12 @@
97
97
  },
98
98
  {
99
99
  "source": "json/json_manifest.json",
100
- "target": "../../global/json",
100
+ "target": "../../global/json/global_json_manifest.json",
101
101
  "condition": "true"
102
102
  },
103
103
  {
104
104
  "source": "json/Json-Manifest.md",
105
- "target": "../../global/json",
105
+ "target": "../../global/json/Json-Manifest.md",
106
106
  "condition": "true"
107
107
  },
108
108
  {
@@ -72,12 +72,12 @@
72
72
  },
73
73
  {
74
74
  "source": "json/json_manifest.json",
75
- "target": "flutter/json/",
75
+ "target": "flutter/json/flutter_json_manifest.json",
76
76
  "condition": "true"
77
77
  },
78
78
  {
79
79
  "source": "json/Json-Manifest.md",
80
- "target": "flutter/json/",
80
+ "target": "flutter/json/Json-Manifest.md",
81
81
  "condition": "true"
82
82
  },
83
83
  {
@@ -87,12 +87,12 @@
87
87
  },
88
88
  {
89
89
  "source": "json/json_manifest.json",
90
- "target": "../../global/json",
90
+ "target": "../../global/json/global_json_manifest.json",
91
91
  "condition": "true"
92
92
  },
93
93
  {
94
94
  "source": "json/Json-Manifest.md",
95
- "target": "../../global/json",
95
+ "target": "../../global/json/Json-Manifest.md",
96
96
  "condition": "true"
97
97
  },
98
98
  {
@@ -72,22 +72,22 @@
72
72
  },
73
73
  {
74
74
  "source": "json/json_manifest.json",
75
- "target": "android/json/",
75
+ "target": "android/json/android_json_manifest.json",
76
76
  "condition": "true"
77
77
  },
78
78
  {
79
79
  "source": "json/json_manifest.json",
80
- "target": "ios/json/",
80
+ "target": "ios/json/ios_json_manifest.json",
81
81
  "condition": "true"
82
82
  },
83
83
  {
84
84
  "source": "json/Json-Manifest.md",
85
- "target": "android/json/",
85
+ "target": "android/json/Json-Manifest.md",
86
86
  "condition": "true"
87
87
  },
88
88
  {
89
89
  "source": "json/Json-Manifest.md",
90
- "target": "ios/json/",
90
+ "target": "ios/json/Json-Manifest.md",
91
91
  "condition": "true"
92
92
  },
93
93
  {
@@ -97,12 +97,12 @@
97
97
  },
98
98
  {
99
99
  "source": "json/json_manifest.json",
100
- "target": "../../global/json",
100
+ "target": "../../global/json/global_json_manifest.json",
101
101
  "condition": "true"
102
102
  },
103
103
  {
104
104
  "source": "json/Json-Manifest.md",
105
- "target": "../../global/json",
105
+ "target": "../../global/json/Json-Manifest.md",
106
106
  "condition": "true"
107
107
  },
108
108
  {
@@ -1,46 +1,59 @@
1
1
  {
2
2
  "configurations": [
3
3
  {
4
- "key": "brand_config.json",
5
- "name": "Brand Configuration",
4
+ "filename": "brand_config.json",
6
5
  "filePath": "shared/brand_config.json",
7
- "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/shared/brand_config.json"
6
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/shared/brand_config.json"
8
7
  },
9
8
  {
10
- "key": "theme.json",
11
- "name": "Theme Configuration",
9
+ "filename": "theme.json",
12
10
  "filePath": "shared/theme.json",
13
- "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/shared/theme.json"
11
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/shared/theme.json"
14
12
  },
15
13
  {
16
- "key": "android_config.json",
17
- "name": "Android Configuration",
14
+ "filename": "android_config.json",
18
15
  "filePath": "android/android_config.json",
19
- "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/android/android_config.json"
16
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/android/android_config.json"
20
17
  },
21
18
  {
22
- "key": "android_signing.json",
23
- "name": "Android Signing",
19
+ "filename": "android_signing.json",
24
20
  "filePath": "android/android_signing.json",
25
- "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/android/android_signing.json"
21
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/android/android_signing.json"
26
22
  },
27
23
  {
28
- "key": "ios_config.json",
29
- "name": "iOS Configuration",
24
+ "filename": "ios_config.json",
30
25
  "filePath": "ios/ios_config.json",
31
- "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/ios/ios_config.json"
26
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/ios/ios_config.json"
32
27
  },
33
28
  {
34
- "key": "ios_signing.json",
35
- "name": "iOS Signing",
29
+ "filename": "ios_signing.json",
36
30
  "filePath": "ios/ios_signing.json",
37
- "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/ios/ios_signing.json"
31
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/ios/ios_signing.json"
38
32
  },
39
33
  {
40
- "key": "InfoPlist.xcstrings",
41
- "name": "InfoPlist.xcstrings",
34
+ "filename": "InfoPlist.xcstrings",
42
35
  "filePath": "ios/InfoPlist.xcstrings",
43
- "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/brands/ios/InfoPlist.xcstrings"
36
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/ios/InfoPlist.xcstrings"
37
+ },
38
+ {
39
+ "filename": "ios_json_manifest.json",
40
+ "filePath": "ios/json/ios_json_manifest.json",
41
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/json/json_manifest.json"
42
+ },
43
+ {
44
+ "filename": "android_json_manifest.json",
45
+ "filePath": "android/json/android_json_manifest.json",
46
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/json/json_manifest.json"
47
+ },
48
+ {
49
+ "filename": "flutter_json_manifest.json",
50
+ "filePath": "flutter/json/flutter_json_manifest.json",
51
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/json/json_manifest.json"
52
+ },
53
+ {
54
+ "filename": "global_json_manifest.json",
55
+ "filePath": "../../global/json/global_json_manifest.json",
56
+ "url": "https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/main/solara/lib/core/template/brands/json/json_manifest.json"
44
57
  }
45
58
  ]
46
59
  }
@@ -7,28 +7,125 @@ class ProjectTemplateGenerator
7
7
  @target_dir = target_dir
8
8
  @config_file = config_file
9
9
  @config = read_config
10
+ @file_mappings = build_file_mappings
10
11
  end
11
12
 
12
13
  def create_project
13
14
  @config["files"].each do |file|
14
- if evaluate_condition(file["condition"], @config["variables"])
15
- source_path = File.join(@template_dir, file["source"])
16
- target_path = File.join(@target_dir, file["target"])
15
+ next unless evaluate_condition(file["condition"], @config["variables"])
16
+ source_path = File.join(@template_dir, file["source"])
17
+ target_path = File.join(@target_dir, file["target"])
17
18
 
18
- if file["source"].nil? || file["source"].empty? || !File.exist?(source_path)
19
- # Create the target directory if no source path is provided
20
- FileUtils.mkdir_p(target_path)
21
- else
22
- copy_content = file.fetch("copy_content", true)
23
- copy_item(source_path, target_path, copy_content)
24
- replace_variables(target_path, @config["variables"]) if copy_content
25
- end
19
+ if file["source"].nil? || file["source"].empty? || !File.exist?(source_path)
20
+ # Create the target directory if no source path is provided
21
+ FileUtils.mkdir_p(target_path)
22
+ else
23
+ copy_content = file.fetch("copy_content", true)
24
+ copy_item(source_path, target_path, copy_content, file)
25
+ replace_variables(target_path, @config["variables"]) if copy_content
26
+ end
27
+ end
28
+ end
29
+
30
+ def sync_with_template
31
+ @config["files"].each do |file|
32
+ next unless evaluate_condition(file["condition"], @config["variables"])
33
+ next if file["source"].nil? || file["source"].empty?
34
+
35
+ source_path = File.join(@template_dir, file["source"])
36
+ target_path = File.join(@target_dir, file["target"])
37
+
38
+ next unless File.exist?(source_path)
39
+
40
+ if File.directory?(source_path)
41
+ FileUtils.mkdir_p(target_path) unless Dir.exist?(target_path)
42
+ sync_directory(source_path, target_path, file)
43
+ elsif !File.exist?(target_path) && should_copy_path?(source_path)
44
+ FileUtils.mkdir_p(File.dirname(target_path))
45
+ FileUtils.cp(source_path, target_path)
46
+ copy_content = file.fetch("copy_content", true)
47
+ replace_variables(target_path, @config["variables"]) if copy_content
26
48
  end
27
49
  end
28
50
  end
29
51
 
30
52
  private
31
53
 
54
+ # This method:
55
+ # - Creates a hash of source paths to their targets
56
+ # - Removes leading slashes for consistency
57
+ # - Handles directories differently from files
58
+ # - For directories: stores true to indicate it's a directory that should be copied
59
+ # For files: stores the specific target path
60
+ def build_file_mappings
61
+ mappings = {}
62
+ @config["files"].each do |file|
63
+ source_path = file["source"].sub(/^\//, '') # Removes leading slashes for consistency
64
+ if File.directory?(File.join(@template_dir, source_path))
65
+ mappings[source_path] = true # Directories are marked as true
66
+ else
67
+ mappings[source_path] = file["target"] # Files store their target path
68
+ end
69
+ end
70
+ mappings
71
+ end
72
+
73
+ # This method:
74
+ # - Converts the full path to a relative path
75
+ # - Checks if the path matches any configured source
76
+ # - For directories (ending with '/'): checks if the path is within that directory
77
+ # - For files: checks for exact matches
78
+ # - Returns false if no match is found
79
+ def should_copy_path?(path)
80
+ relative_path = path.sub(@template_dir, '').sub(/^\//, '')
81
+
82
+ @file_mappings.each do |source, target|
83
+ if source.end_with?('/')
84
+ return true if relative_path.start_with?(source)
85
+ else
86
+ return true if relative_path == source
87
+ end
88
+ end
89
+
90
+ false
91
+ end
92
+
93
+ def get_target_path(source_path)
94
+ relative_path = source_path.sub(@template_dir, '').sub(/^\//, '')
95
+ @config["files"].each do |file|
96
+ if file["source"] == relative_path
97
+ return File.join(@target_dir, file["target"])
98
+ end
99
+ end
100
+ nil
101
+ end
102
+
103
+ def sync_directory(source_dir, target_dir, config_entry)
104
+ return unless File.directory?(source_dir)
105
+
106
+ Dir.foreach(source_dir) do |item|
107
+ next if item == '.' || item == '..'
108
+
109
+ source_path = File.join(source_dir, item)
110
+
111
+ if specific_target = get_target_path(source_path)
112
+ target_path = specific_target
113
+ else
114
+ target_path = File.join(target_dir, item)
115
+ end
116
+
117
+ next unless should_copy_path?(source_path)
118
+
119
+ if File.directory?(source_path)
120
+ FileUtils.mkdir_p(target_path) unless Dir.exist?(target_path)
121
+ sync_directory(source_path, target_path, config_entry)
122
+ elsif !File.exist?(target_path)
123
+ FileUtils.mkdir_p(File.dirname(target_path))
124
+ FileUtils.cp(source_path, target_path)
125
+ end
126
+ end
127
+ end
128
+
32
129
  def read_config
33
130
  JSON.parse(File.read(@config_file))
34
131
  end
@@ -37,12 +134,21 @@ class ProjectTemplateGenerator
37
134
  true
38
135
  end
39
136
 
40
- def copy_item(source, target, copy_content)
137
+ def copy_item(source, target, copy_content, config_entry)
41
138
  if File.directory?(source)
42
139
  FileUtils.mkdir_p(target)
43
140
  Dir.foreach(source) do |item|
44
141
  next if item == '.' || item == '..'
45
- copy_item(File.join(source, item), File.join(target, item), copy_content)
142
+ source_path = File.join(source, item)
143
+
144
+ if specific_target = get_target_path(source_path)
145
+ target_path = specific_target
146
+ else
147
+ target_path = File.join(target, item)
148
+ end
149
+
150
+ next unless should_copy_path?(source_path)
151
+ copy_item(source_path, target_path, copy_content, config_entry)
46
152
  end
47
153
  else
48
154
  FileUtils.mkdir_p(File.dirname(target))
@@ -1,3 +1,3 @@
1
1
  module Solara
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -40,7 +40,7 @@ class SolaraManager
40
40
  return
41
41
  end
42
42
 
43
- BrandOnboarder.new(brand_key, brand_name, clone_brand_key: clone_brand_key).onboard
43
+ BrandOnboarder.new.onboard(brand_key, brand_name, clone_brand_key: clone_brand_key)
44
44
 
45
45
  switch(brand_key, ignore_health_check: true)
46
46
 
@@ -78,4 +78,8 @@ class SolaraManager
78
78
  DoctorManager.new.visit_brands(keys, print_logs: print_logs)
79
79
  end
80
80
 
81
+ def sync_brand_with_template(brand_key)
82
+ BrandOnboarder.new.sync_with_template(brand_key)
83
+ end
84
+
81
85
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solara
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Malek Kamel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-26 00:00:00.000000000 Z
11
+ date: 2024-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -239,6 +239,7 @@ files:
239
239
  - solara/lib/core/doctor/validator/template/template_validator.rb
240
240
  - solara/lib/core/doctor/validator/validation_strategy.rb
241
241
  - solara/lib/core/scripts/brand_config_manager.rb
242
+ - solara/lib/core/scripts/brand_config_updater.rb
242
243
  - solara/lib/core/scripts/brand_configurations_manager.rb
243
244
  - solara/lib/core/scripts/brand_exporter.rb
244
245
  - solara/lib/core/scripts/brand_importer.rb
@@ -270,11 +271,13 @@ files:
270
271
  - solara/lib/core/scripts/solara_settings_manager.rb
271
272
  - solara/lib/core/scripts/solara_status_manager.rb
272
273
  - solara/lib/core/scripts/solara_version_manager.rb
274
+ - solara/lib/core/scripts/string_case.rb
273
275
  - solara/lib/core/scripts/strings_xml_manager.rb
274
276
  - solara/lib/core/scripts/terminal_input_manager.rb
275
277
  - solara/lib/core/scripts/theme_generator.rb
276
278
  - solara/lib/core/scripts/yaml_manager.rb
277
279
  - solara/lib/core/solara_configurator.rb
280
+ - solara/lib/core/template/.DS_Store
278
281
  - solara/lib/core/template/brands/android/android_config.json
279
282
  - solara/lib/core/template/brands/android/android_signing.json
280
283
  - solara/lib/core/template/brands/android/res/.DS_Store