Skip to content

Commit 83988ad

Browse files
authored
Merge pull request #1843 from EnsembleUI/artifacts-folder-config
Updated Starter Local App Folder Structure
2 parents 2fb8bc4 + 58de373 commit 83988ad

File tree

24 files changed

+336
-81
lines changed

24 files changed

+336
-81
lines changed

modules/ensemble/lib/ensemble.dart

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:ensemble/framework/app_info.dart';
1616
import 'package:ensemble/framework/logging/console_log_provider.dart';
1717
import 'package:ensemble/framework/logging/log_manager.dart';
1818
import 'package:ensemble/framework/logging/log_provider.dart';
19+
import 'package:ensemble/framework/ensemble_config_service.dart';
1920
import 'package:ensemble/framework/route_observer.dart';
2021
import 'package:ensemble/framework/scope.dart';
2122
import 'package:ensemble/framework/secrets.dart';
@@ -105,7 +106,6 @@ class Ensemble extends WithEnsemble with EnsembleRouteObserver {
105106
try {
106107
// this code block is guaranteed to run at most once
107108
await StorageManager().init();
108-
await SecretsStore().initialize();
109109
Device().initDeviceInfo();
110110
AppInfo().initPackageInfo(_config);
111111
_completer!.complete();
@@ -139,10 +139,12 @@ class Ensemble extends WithEnsemble with EnsembleRouteObserver {
139139
if (_config != null) {
140140
return Future<EnsembleConfig>.value(_config);
141141
}
142+
// Intialize the config service to get `ensemble-config.yaml` file to access the configuration using static property as `EnsembleConfigService.config`
143+
await EnsembleConfigService.initialize();
144+
await SecretsStore().initialize();
142145

143-
final yamlString =
144-
await rootBundle.loadString('ensemble/ensemble-config.yaml');
145-
final YamlMap yamlMap = loadYaml(yamlString);
146+
// get the config YAML
147+
final YamlMap yamlMap = EnsembleConfigService.config;
146148

147149
Account account = Account.fromYaml(yamlMap['accounts']);
148150
dynamic analyticsConfig = yamlMap['analytics'];
@@ -174,6 +176,20 @@ class Ensemble extends WithEnsemble with EnsembleRouteObserver {
174176
envOverrides = {};
175177
env.forEach((key, value) => envOverrides![key.toString()] = value);
176178
}
179+
// Read environmental variables from config/appConfig.json
180+
try {
181+
dynamic path = yamlMap["definitions"]?['local']?["path"];
182+
final configString = await rootBundle
183+
.loadString('${path}/config/appConfig.json');
184+
final Map<String, dynamic> configMap = json.decode(configString);
185+
// Loop through the envVariables from appConfig.json file and add them to the envOverrides
186+
configMap["envVariables"].forEach((key, value) {
187+
envOverrides![key] = value;
188+
});
189+
} catch(e) {
190+
debugPrint("appConfig.json file doesn't exist");
191+
}
192+
177193
DefinitionProvider definitionProvider = DefinitionProvider.from(yamlMap);
178194
_config = EnsembleConfig(
179195
definitionProvider: await definitionProvider.init(),
@@ -452,9 +468,11 @@ class EnsembleConfig {
452468
ThemeData getAppTheme() {
453469
return ThemeManager().getAppTheme(appBundle?.theme);
454470
}
471+
455472
bool hasLegacyCustomAppTheme() {
456473
return ThemeManager().hasLegacyCustomAppTheme(appBundle?.theme);
457474
}
475+
458476
/// retrieve the global widgets/codes/APIs
459477
Map? getResources() {
460478
return appBundle?.resources;

modules/ensemble/lib/framework/definition_providers/local_provider.dart

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import 'dart:io';
12
import 'dart:ui';
23

34
import 'package:ensemble/ensemble.dart';
45
import 'package:ensemble/framework/definition_providers/provider.dart';
56
import 'package:ensemble/framework/widget/screen.dart';
67
import 'package:ensemble/util/utils.dart';
8+
import 'package:flutter/material.dart';
79
import 'package:flutter/services.dart';
810
import 'package:flutter_i18n/flutter_i18n.dart';
911
import 'package:flutter_i18n/loaders/decoders/yaml_decode_strategy.dart';
1012
import 'package:yaml/yaml.dart';
13+
import 'dart:convert';
1114
import 'package:flutter/foundation.dart' as foundation;
1215

1316
/**
@@ -34,9 +37,8 @@ class LocalDefinitionProvider extends FileDefinitionProvider {
3437
{String? screenId, String? screenName}) async {
3538
// Note: Web with local definition caches even if we disable browser cache
3639
// so you may need to re-run the app on definition changes
37-
var pageStr = await rootBundle.loadString(
38-
'$path${screenId ?? screenName ?? appHome}.yaml',
39-
cache: foundation.kReleaseMode);
40+
var pageStr = await rootBundle
41+
.loadString('${path}screens/${screenId ?? screenName ?? appHome}.yaml');
4042
if (pageStr.isEmpty) {
4143
return ScreenDefinition(YamlMap());
4244
}
@@ -45,15 +47,24 @@ class LocalDefinitionProvider extends FileDefinitionProvider {
4547

4648
@override
4749
Future<AppBundle> getAppBundle({bool? bypassCache = false}) async {
48-
YamlMap? config = await _readFile('config.ensemble');
49-
if (config != null) {
50-
appConfig = UserAppConfig(
51-
baseUrl: config['app']?['baseUrl'],
52-
useBrowserUrl: Utils.optionalBool(config['app']?['useBrowserUrl']));
50+
try {
51+
final configString =
52+
await rootBundle.loadString('${path}/config/appConfig.json');
53+
final Map<String, dynamic> appConfigMap = json.decode(configString);
54+
if (appConfigMap.isNotEmpty) {
55+
appConfig = UserAppConfig(
56+
baseUrl: appConfigMap["baseUrl"],
57+
useBrowserUrl: Utils.optionalBool(appConfigMap['useBrowserUrl']),
58+
envVariables: appConfigMap["envVariables"]);
59+
}
60+
} catch (e) {
61+
// ignore error
5362
}
63+
5464
return AppBundle(
55-
theme: await _readFile('theme.ensemble'),
56-
resources: await _readFile('resources.ensemble'));
65+
theme: await _readFile('theme.yaml'),
66+
resources: await getCombinedAppBundle(), // get the combined app bundle for local scripts and widgets
67+
);
5768
}
5869

5970
Future<YamlMap?> _readFile(String file) async {
@@ -66,6 +77,72 @@ class LocalDefinitionProvider extends FileDefinitionProvider {
6677
return null;
6778
}
6879

80+
Future<Map?> getCombinedAppBundle() async {
81+
Map code = {};
82+
Map output = {};
83+
Map widgets = {};
84+
85+
try {
86+
// Get the manifest content
87+
final manifestContent =
88+
await rootBundle.loadString(path + '.manifest.json');
89+
final Map<String, dynamic> manifestMap = json.decode(manifestContent);
90+
91+
// Process App Widgets
92+
try {
93+
if (manifestMap['widgets'] != null) {
94+
final List<Map<String, dynamic>> widgetsList =
95+
List<Map<String, dynamic>>.from(manifestMap['widgets']);
96+
97+
for (var widgetItem in widgetsList) {
98+
try {
99+
// Load the widget content in YamlMap
100+
final widgetContent =
101+
await _readFile("widgets/${widgetItem["name"]}.yaml");
102+
if (widgetContent is YamlMap) {
103+
widgets[widgetItem["name"]] = widgetContent["Widget"];
104+
} else {
105+
debugPrint('Content in ${widgetItem["name"]} is not a YamlMap');
106+
}
107+
} catch (e) {
108+
// ignore error
109+
}
110+
}
111+
}
112+
} catch (e) {
113+
debugPrint('Error processing widgets: $e');
114+
}
115+
116+
// Process App Scripts
117+
try {
118+
if (manifestMap['scripts'] != null) {
119+
final List<Map<String, dynamic>> scriptsList =
120+
List<Map<String, dynamic>>.from(manifestMap['scripts']);
121+
122+
for (var script in scriptsList) {
123+
try {
124+
// Load the script content in string
125+
final scriptContent = await rootBundle
126+
.loadString("${path}scripts/${script["name"]}.js");
127+
code[script["name"]] = scriptContent;
128+
} catch (e) {
129+
// ignore error
130+
}
131+
}
132+
}
133+
} catch (e) {
134+
debugPrint('Error processing scripts: $e');
135+
}
136+
137+
output[ResourceArtifactEntry.Widgets.name] = widgets;
138+
output[ResourceArtifactEntry.Scripts.name] = code;
139+
140+
return output;
141+
} catch (e) {
142+
return null;
143+
}
144+
}
145+
69146
@override
70147
UserAppConfig? getAppConfig() {
71148
return appConfig;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'package:flutter/services.dart';
2+
import 'package:yaml/yaml.dart';
3+
4+
// EnsembleConfigService is a service that provides access to the ensemble-config.yaml file through static property once initialized
5+
class EnsembleConfigService {
6+
static YamlMap? _config;
7+
8+
static Future<void> initialize() async {
9+
final yamlString = await rootBundle.loadString('ensemble/ensemble-config.yaml');
10+
_config = loadYaml(yamlString);
11+
}
12+
13+
static YamlMap get config {
14+
if (_config == null) {
15+
throw StateError('EnsembleConfig not initialized');
16+
}
17+
return _config!;
18+
}
19+
}

modules/ensemble/lib/framework/secrets.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Secrets Configuration
22

3+
import 'dart:convert';
4+
5+
import 'package:ensemble/framework/ensemble_config_service.dart';
36
import 'package:ensemble/framework/storage_manager.dart';
47
import 'package:ensemble_ts_interpreter/invokables/invokable.dart';
8+
import 'package:flutter/services.dart';
59
import 'package:flutter_dotenv/flutter_dotenv.dart';
610

711
import '../ensemble.dart';
@@ -23,6 +27,20 @@ class SecretsStore with Invokable {
2327
Ensemble().getConfig()?.definitionProvider.getSecrets() ?? {};
2428

2529
// add local overrides
30+
try {
31+
String provider = EnsembleConfigService.config["definitions"]?['from'];
32+
if (provider == 'local') {
33+
String path =
34+
EnsembleConfigService.config["definitions"]?['local']?["path"];
35+
final secretsString =
36+
await rootBundle.loadString('${path}/config/secrets.json');
37+
final Map<String, dynamic> appSecretsMap = json.decode(secretsString);
38+
appSecretsMap["secrets"].forEach((key, value) {
39+
secrets![key] = value;
40+
});
41+
}
42+
} catch (_) {}
43+
// add secrets from env
2644
try {
2745
await dotenv.load();
2846
secrets.addAll(dotenv.env);

modules/ensemble/lib/util/utils.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:math';
33
import 'dart:ui';
44
import 'package:ensemble/ensemble.dart';
55
import 'package:ensemble/ensemble_app.dart';
6+
import 'package:ensemble/framework/ensemble_config_service.dart';
67
import 'package:ensemble/framework/stub/location_manager.dart';
78
import 'package:ensemble/framework/theme/theme_manager.dart';
89
import 'package:ensemble_ts_interpreter/invokables/UserLocale.dart';
@@ -921,10 +922,17 @@ class Utils {
921922
return strings[0];
922923
}
923924

924-
/// prefix the asset with the root directory (i.e. ensemble/assets/), plus
925+
/// prefix the asset with the app root directory (i.e. ensemble/apps/<app-name>/assets/), plus
925926
/// stripping any unnecessary query params (e.g. anything after the first ?)
926927
static String getLocalAssetFullPath(String asset) {
927-
return 'ensemble/assets/${stripQueryParamsFromAsset(asset)}';
928+
String provider = EnsembleConfigService.config["definitions"]?['from'];
929+
if(provider == 'local') {
930+
String path = EnsembleConfigService.config["definitions"]?['local']?["path"];
931+
return '${path}/assets/${stripQueryParamsFromAsset(asset)}';
932+
}
933+
else{
934+
return 'ensemble/assets/${stripQueryParamsFromAsset(asset)}';
935+
}
928936
}
929937

930938
static bool isMemoryPath(String path) {

starter/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,53 @@ Ensemble Studio includes an Online Editor for making changes with type-ahead sup
5959
- Follow [iOS](https://docs.flutter.dev/deployment/ios), [Android](https://docs.flutter.dev/deployment/android), [MacOS](https://docs.flutter.dev/deployment/macos), [Windows](https://docs.flutter.dev/deployment/windows) release documentation.
6060

6161
## Misc
62+
### Run with Local Definition
63+
To take advantage of a local definition setup (where UI updates are loaded directly from your local files), follow these steps:
64+
65+
#### 1. Ensure Your App Structure
66+
Inside the `ensemble/apps/` folder, you should have a directory for your app. The folder structure should look like this:
67+
```
68+
ensemble/apps/yourAppName/
69+
├── assets/
70+
├── fonts/
71+
├── scripts/
72+
├── widgets/
73+
├── screens/
74+
├── translations/
75+
├── config/
76+
│ ├── appConfig.json
77+
│ ├── secrets.json
78+
├── theme.yaml
79+
├── .manifest.json # Required for widgets and scripts to work correctly
80+
```
81+
#### 2. Update `ensemble-config.yaml`
82+
- Open `ensemble/ensemble-config.yaml` and set `from: local` under `definitions`.
83+
- Under `local` update the `path` to match your app’s name `ensemble/apps/yourAppName` and also update `appHome` to the name of your App's Home Screen.
84+
- Under `i18n` update the `path` to match your app’s name `ensemble/apps/yourAppName/translations`.
85+
86+
87+
#### 3. Update `pubspec.yaml`
88+
- Add the necessary paths under `flutter -> assets` to ensure your app loads all required files and folders correctly:
89+
90+
```yaml
91+
flutter:
92+
assets:
93+
# list all your Apps directories here. It's a Flutter requirement
94+
- ensemble/apps/helloApp/
95+
- ensemble/apps/helloApp/screens/
96+
- ensemble/apps/helloApp/widgets/
97+
- ensemble/apps/helloApp/scripts/
98+
- ensemble/apps/helloApp/assets/
99+
- ensemble/apps/helloApp/translations/
100+
101+
# # config folder contains appConfig.json and secrets.json
102+
- ensemble/apps/helloApp/config/
103+
```
104+
NOTE: Only add the existing paths under assets.
105+
#### 4. Run the App
106+
- Run the App with `flutter run`. Your App now fetches its pages and resources from local app files.
107+
108+
62109
### Run with remote definition
63110
To take advantage of Server-driven UI (change your UI at anytime from the server), you can host these definitions on your file server.
64111
When hosting on your server, follow the following steps.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"widgets": [
3+
{
4+
"name": "MyCustomWidget1"
5+
},
6+
{
7+
"name": "MyCustomWidget2"
8+
}
9+
],
10+
"scripts": [
11+
{
12+
"name": "common"
13+
},
14+
{
15+
"name": "utils"
16+
}
17+
]
18+
}

0 commit comments

Comments
 (0)