// Get everything that should be stored as a file in the .msapp. private static IEnumerable <FileEntry> GetMsAppFiles(this CanvasDocument app, ErrorContainer errors) { // Loose files foreach (var file in app._unknownFiles.Values) { yield return(file); } yield return(ToFile(FileKind.Themes, app._themes)); var header = app._header.JsonClone(); header.LastSavedDateTimeUTC = app._entropy.GetHeaderLastSaved(); yield return(ToFile(FileKind.Header, header)); var props = app._properties.JsonClone(); if (app._connections != null) { var json = Utilities.JsonSerialize(app._connections); props.LocalConnectionReferences = json; } if (app._dataSourceReferences != null) { var json = Utilities.JsonSerialize(app._dataSourceReferences); // Some formats serialize empty as "", some serialize as "{}" if (app._dataSourceReferences.Count == 0) { if (app._entropy.LocalDatabaseReferencesAsEmpty) { json = ""; } else { json = "{}"; } } props.LocalDatabaseReferences = json; } if (app._libraryReferences != null) { var json = Utilities.JsonSerialize(app._libraryReferences); props.LibraryDependencies = json; } yield return(ToFile(FileKind.Properties, props)); var(publishInfo, logoFile) = app.TransformLogoOnSave(); yield return(logoFile); if (publishInfo != null) { yield return(ToFile(FileKind.PublishInfo, publishInfo)); } // "DataComponent" data sources are not part of DataSource.json, and instead in their own file var dataSources = new DataSourcesJson { DataSources = app.GetDataSources() .SelectMany(x => x.Value) .Where(x => !x.IsDataComponent) .OrderBy(x => app._entropy.GetOrder(x)) .ToArray() }; yield return(ToFile(FileKind.DataSources, dataSources)); var sourceFiles = new List <SourceFile>(); var idRestorer = new UniqueIdRestorer(app._entropy); var maxPublishOrderIndex = app._entropy.PublishOrderIndices.Any() ? app._entropy.PublishOrderIndices.Values.Max() : 0; // Rehydrate sources before yielding any to be written, processing component defs first foreach (var controlData in app._screens.Concat(app._components) .OrderBy(source => (app._editorStateStore.TryGetControlState(source.Value.Name.Identifier, out var control) && (control.IsComponentDefinition ?? false)) ? -1 : 1)) { var sourceFile = IRStateHelpers.CombineIRAndState(controlData.Value, errors, app._editorStateStore, app._templateStore, idRestorer, app._entropy); // Offset the publishOrderIndex based on Entropy.json foreach (var ctrl in sourceFile.Flatten()) { if (app._entropy.PublishOrderIndices.TryGetValue(ctrl.Name, out var index)) { ctrl.PublishOrderIndex = index; } else { ctrl.PublishOrderIndex = ++maxPublishOrderIndex; } } sourceFiles.Add(sourceFile); } CheckUniqueIds(errors, sourceFiles); // Repair order when screens are unchanged if (sourceFiles.Where(file => !ExcludeControlFromScreenOrdering(file)).Count() == app._screenOrder.Count && sourceFiles.Where(file => !ExcludeControlFromScreenOrdering(file)).All(file => app._screenOrder.Contains(file.ControlName))) { double i = 0.0; foreach (var screen in app._screenOrder) { sourceFiles.First(file => file.ControlName == screen).Value.TopParent.Index = i; i += 1; } } else { // Make up an order, it doesn't really matter. double i = 0.0; foreach (var sourceFile in sourceFiles) { if (ExcludeControlFromScreenOrdering(sourceFile)) { continue; } sourceFile.Value.TopParent.Index = i; i += 1; } } RepairComponentInstanceIndex(app._entropy?.ComponentIndexes ?? new Dictionary <string, double>(), sourceFiles); // This ordering is essential, we need to match the order in which Studio writes the files to replicate certain order-dependent behavior. foreach (var sourceFile in sourceFiles.OrderBy(file => file.GetMsAppFilename())) { yield return(sourceFile.ToMsAppFile()); } var componentTemplates = new List <TemplateMetadataJson>(); foreach (var template in app._templateStore.Contents.Where(template => template.Value.IsComponentTemplate ?? false)) { if (((template.Value.CustomProperties?.Any() ?? false) || template.Value.ComponentAllowCustomization.HasValue) && !(template.Value.ComponentManifest?.IsDataComponent ?? false)) { componentTemplates.Add(template.Value.ToTemplateMetadata(app._entropy)); } } app._templates = new TemplatesJson() { ComponentTemplates = componentTemplates.Any() ? componentTemplates.OrderBy(x => app._entropy.GetComponentOrder(x)).ToArray() : null, UsedTemplates = app._templates.UsedTemplates.OrderBy(x => app._entropy.GetOrder(x)).ToArray() }; yield return(ToFile(FileKind.Templates, app._templates)); var componentsMetadata = new List <ComponentsMetadataJson.Entry>(); var dctemplate = new List <TemplateMetadataJson>(); foreach (var componentTemplate in app._templateStore.Contents.Values.Where(state => state.ComponentManifest != null)) { var manifest = componentTemplate.ComponentManifest; componentsMetadata.Add(new ComponentsMetadataJson.Entry { Name = manifest.Name, TemplateName = manifest.TemplateGuid, ExtensionData = manifest.ExtensionData }); if (manifest.IsDataComponent) { var controlId = GetDataComponentDefinition(sourceFiles.Select(source => source.Value), manifest.TemplateGuid, errors).ControlUniqueId; var template = new TemplateMetadataJson { Name = manifest.TemplateGuid, ComponentType = ComponentType.DataComponent, Version = app._entropy.GetTemplateVersion(manifest.TemplateGuid), IsComponentLocked = false, ComponentChangedSinceFileImport = true, ComponentAllowCustomization = true, CustomProperties = componentTemplate.CustomProperties, DataComponentDefinitionKey = manifest.DataComponentDefinitionKey }; // Rehydrate fields. template.DataComponentDefinitionKey.ControlUniqueId = controlId; dctemplate.Add(template); } } if (componentsMetadata.Count > 0) { // If the components file is present, then write out all files. yield return(ToFile(FileKind.ComponentsMetadata, new ComponentsMetadataJson { Components = componentsMetadata .OrderBy(x => app._entropy.GetOrder(x)) .ToArray() })); } if (dctemplate.Count > 0) { yield return(ToFile(FileKind.DataComponentTemplates, new DataComponentTemplatesJson { ComponentTemplates = dctemplate .OrderBy(x => app._entropy.GetOrder(x)) .ToArray() })); } // Rehydrate the DataComponent DataSource file. { IEnumerable <DataComponentSourcesJson.Entry> ds = from item in app.GetDataSources().SelectMany(x => x.Value).Where(x => x.IsDataComponent) select item.DataComponentDetails; var dsArray = ds.ToArray(); // backcompat-nit: if we have any DC, then always emit the DC Sources file, even if empty. // if (dcmetadataList.Count > 0) if (dctemplate.Count > 0 || dsArray.Length > 0) { yield return(ToFile(FileKind.DataComponentSources, new DataComponentSourcesJson { DataSources = dsArray })); } } if (app._appCheckerResultJson != null) { yield return(ToFile(FileKind.AppCheckerResult, app._appCheckerResultJson)); } if (app._resourcesJson != null) { var resources = app._resourcesJson.JsonClone(); foreach (var resource in resources.Resources) { if (resource.ResourceKind == "LocalFile") { var rootPath = string.Empty; if (app._entropy?.LocalResourceRootPaths.TryGetValue(resource.Name, out rootPath) ?? false) { resource.RootPath = rootPath; } else { resource.RootPath = string.Empty; } } } yield return(ToFile(FileKind.Resources, resources)); } foreach (var assetFile in app._assetFiles) { yield return(new FileEntry { Name = FilePath.RootedAt("Assets", assetFile.Value.Name), RawBytes = assetFile.Value.RawBytes }); } }