solara 0.6.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
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 +3 -2
  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: 46ea56483f52c7dc15a2a1fa784f8071a97d05d0e38f4e0dac462551b74a282a
4
+ data.tar.gz: e76afce57fbb369b687e6350ee5f3021e054c7ed5af58f272ba3f3a0e3fd74fc
5
5
  SHA512:
6
- metadata.gz: f3e85254b364be2888f9e9a760967bf64e4672df8ae4dde907b6f8a8928afa7266ad9a26f1ff35867ae8b7fb2fc362aa0be6f6a70c7aafb3b29e83fa2c456e72
7
- data.tar.gz: ee5abac576555d1415bcaa33cec324bf21dcf0519b28eefc1687b6b129b6a74c1f160eb5f2b30f3037facda8ca163e0867c4a7acd8ecdfe2ef2ab88fcf84e330
6
+ metadata.gz: da4d00729e4914590d29740e5b7dfae4cf06b6b6d78c0f082fa0c9f6841a95e63edd7d3e8d01ddcc417bfd56cc613a2917af0a3b59401fb28cd5450f8ac35831
7
+ data.tar.gz: a6cd734730a731cd495c05fc6eb75ae0dc61eeb6b9771851e50ae7a15b53c4413ce5ee1d56557aa8d67e931c7143a6b5bd1d930b1f564d414eff3cd9b00f9cea
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}")
@@ -63,7 +63,8 @@ class JsonManifestProcessor
63
63
  file_name = File.basename(file_path)
64
64
  # Skip files that were already processed via manifest
65
65
  next if manifest_files.include?(file_name)
66
- next if file_name == 'json_manifest.json'
66
+ ignored = %w[flutter_json_manifest.json android_json_manifest.json ios_json_manifest.json global_json_manifest.json]
67
+ next if ignored.any? { |item| item == file_name }
67
68
 
68
69
  class_name = derive_class_name(file_name)
69
70
  process_json_file(file_path, class_name, {}, false)
@@ -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.1"
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.1
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