/// This writes out the IR, editor state cache, and potentially component templates /// for a single top level control, such as the App object, a screen, or component /// Name refers to the control name private static void WriteTopParent( DirectoryWriter dir, CanvasDocument app, string name, BlockNode ir, string subDir) { var controlName = name; var text = PAWriterVisitor.PrettyPrint(ir); string filename = controlName + ".pa.yaml"; dir.WriteAllText(subDir, filename, text); var extraData = new Dictionary <string, ControlState>(); foreach (var item in app._editorStateStore.GetControlsWithTopParent(controlName)) { extraData.Add(item.Name, item); } // Write out of all the other state for roundtripping string extraContent = controlName + ".editorstate.json"; dir.WriteAllJson(EditorStateDir, extraContent, extraData); // Write out component templates next to the component if (app._templateStore.TryGetTemplate(name, out var templateState)) { dir.WriteAllJson(subDir, controlName + ".json", templateState); } }
/// This writes out the IR, editor state cache, and potentially component templates /// for a single top level control, such as the App object, a screen, or component /// Name refers to the control name /// Only in case of AppTest, the topParentName is passed down, since for AppTest the TestSuites are sharded into individual files. /// We truncate the control names to limit it to 50 charactes length (escaped name). private static void WriteTopParent( DirectoryWriter dir, CanvasDocument app, string name, BlockNode ir, string subDir, string topParentName = null) { var controlName = name; var newControlName = Utilities.TruncateNameIfTooLong(controlName); string filename = newControlName + ".fx.yaml"; // For AppTest control shard each test suite into individual files. if (controlName == AppTestControlName) { foreach (var child in ir.Children) { WriteTopParent(dir, app, child.Properties.FirstOrDefault(x => x.Identifier == "DisplayName").Expression.Expression.Trim(new char[] { '"' }), child, subDir, controlName); } // Clear the children since they have already been sharded into their individual files. ir.Children.Clear(); } var text = PAWriterVisitor.PrettyPrint(ir); dir.WriteAllText(subDir, filename, text); // For TestSuite controls, only the top parent control has an editor state created. // For other control types, create an editor state. if (string.IsNullOrEmpty(topParentName)) { string editorStateFilename = $"{newControlName}.editorstate.json"; var controlStates = new Dictionary <string, ControlState>(); foreach (var item in app._editorStateStore.GetControlsWithTopParent(controlName)) { controlStates.Add(item.Name, item); } ControlTreeState editorState = new ControlTreeState { ControlStates = controlStates, TopParentName = controlName }; // Write out of all the other state properties on the control for roundtripping. dir.WriteAllJson(EditorStateDir, editorStateFilename, editorState); } // Write out component templates next to the component if (app._templateStore.TryGetTemplate(name, out var templateState)) { dir.WriteAllJson(subDir, newControlName + ".json", templateState); } }
/// This writes out the IR, editor state cache, and potentially component templates /// for a single top level control, such as the App object, a screen, or component /// Name refers to the control name /// Only in case of AppTest, the topParentName is passed down, since for AppTest the TestSuites are sharded into individual files. /// We truncate the control names to limit it to 50 charactes length (escaped name). private static void WriteTopParent( DirectoryWriter dir, CanvasDocument app, string name, BlockNode ir, string subDir, string topParentname = null) { var controlName = name; var newControlName = Utilities.TruncateNameIfTooLong(controlName); string filename = newControlName + ".fx.yaml"; // For AppTest control shard each test suite into individual file. if (controlName == AppTestControlName) { foreach (var child in ir.Children) { WriteTopParent(dir, app, child.Properties.FirstOrDefault(x => x.Identifier == "DisplayName").Expression.Expression.Trim(new char[] { '"' }), child, subDir, controlName); } // Clear the children since they have already been sharded into their individual files. ir.Children.Clear(); } var text = PAWriterVisitor.PrettyPrint(ir); dir.WriteAllText(subDir, filename, text); var extraData = new Dictionary <string, ControlState>(); foreach (var item in app._editorStateStore.GetControlsWithTopParent(topParentname ?? controlName)) { extraData.Add(item.Name, item); } // Write out of all the other state for roundtripping string extraContent = (topParentname ?? newControlName) + ".editorstate.json"; // We write editorstate.json file per top parent control, and hence for the TestSuite control since it is not a top parent // use the top parent name (i.e. Test_7F478737223C4B69) to create the editorstate.json file. if (!dir.FileExists(EditorStateDir, extraContent)) { dir.WriteAllJson(EditorStateDir, extraContent, extraData); } // Write out component templates next to the component if (app._templateStore.TryGetTemplate(name, out var templateState)) { dir.WriteAllJson(subDir, newControlName + ".json", templateState); } }
private static void WriteDataSources(DirectoryWriter dir, CanvasDocument app, ErrorContainer errors) { // Data Sources - write out each individual source. HashSet <string> filenames = new HashSet <string>(); foreach (var kvp in app.GetDataSources()) { // Filename doesn't actually matter, but careful to avoid collisions and overwriting. // Also be determinstic. string filename = kvp.Key + ".json"; if (!filenames.Add(filename.ToLower())) { int index = 1; var altFileName = kvp.Key + "_" + index + ".json"; while (!filenames.Add(altFileName.ToLower())) { ++index; } errors.GenericWarning("Data source name collision: " + filename + ", writing as " + altFileName + " to avoid."); filename = altFileName; } var dataSourceStateToWrite = kvp.Value.JsonClone().OrderBy(ds => ds.Name, StringComparer.Ordinal); DataSourceDefinition dataSourceDef = null; // Split out the changeable parts of the data source. foreach (var ds in dataSourceStateToWrite.Where(ds => ds.Type != "ViewInfo")) { // CDS DataSource if (ds.TableDefinition != null) { dataSourceDef = new DataSourceDefinition(); dataSourceDef.TableDefinition = Utility.JsonParse <DataSourceTableDefinition>(ds.TableDefinition); dataSourceDef.DatasetName = ds.DatasetName; dataSourceDef.EntityName = ds.RelatedEntityName ?? ds.Name; ds.DatasetName = null; ds.TableDefinition = null; } // CDP DataSource else if (ds.DataEntityMetadataJson != null) { if (ds.ApiId == "/providers/microsoft.powerapps/apis/shared_commondataservice") { // This is the old CDS connector, we can't support it since it's optionset format is incompatable with the newer one errors.ValidationError($"Connection {ds.Name} is using the old CDS connector which is incompatable with this tool"); throw new DocumentException(); } dataSourceDef = new DataSourceDefinition(); dataSourceDef.DataEntityMetadataJson = ds.DataEntityMetadataJson; dataSourceDef.EntityName = ds.Name; dataSourceDef.TableName = ds.TableName; ds.TableName = null; ds.DataEntityMetadataJson = null; } else if (ds.Type == "OptionSetInfo") { // This looks like a left over from previous versions of studio, account for it by // tracking optionsets with empty dataset names ds.DatasetName = ds.DatasetName == null ? string.Empty : null; } else if (ds.WadlMetadata != null) { // For some reason some connectors have both, investigate if one could be discarded by the server? if (ds.WadlMetadata.WadlXml != null) { dir.WriteAllXML(WadlPackageDir, filename.Replace(".json", ".xml"), ds.WadlMetadata.WadlXml); } if (ds.WadlMetadata.SwaggerJson != null) { dir.WriteAllJson(SwaggerPackageDir, filename, JsonSerializer.Deserialize <SwaggerDefinition>(ds.WadlMetadata.SwaggerJson, Utility._jsonOpts)); } ds.WadlMetadata = null; } } if (dataSourceDef != null) { TrimViewNames(dataSourceStateToWrite, dataSourceDef.DatasetName); } if (dataSourceDef?.DatasetName != null && app._dataSourceReferences.TryGetValue(dataSourceDef.DatasetName, out var referenceJson)) { // copy over the localconnectionreference if (referenceJson.dataSources.TryGetValue(dataSourceDef.EntityName, out var dsRef)) { dataSourceDef.LocalReferenceDSJson = dsRef; } dataSourceDef.InstanceUrl = referenceJson.instanceUrl; dataSourceDef.ExtensionData = referenceJson.ExtensionData; } if (dataSourceDef != null) { dir.WriteAllJson(DataSourcePackageDir, filename, dataSourceDef); } dir.WriteAllJson(DataSourcesDir, filename, dataSourceStateToWrite); } }
// Write out to a directory (this shards it) public static void SaveAsSource(CanvasDocument app, string directory2, ErrorContainer errors) { var dir = new DirectoryWriter(directory2); dir.DeleteAllSubdirs(); // Shard templates, parse for default values var templateDefaults = new Dictionary <string, ControlTemplate>(); foreach (var template in app._templates.UsedTemplates) { var filename = $"{template.Name}_{template.Version}.xml"; dir.WriteAllXML(PackagesDir, filename, template.Template); if (!ControlTemplateParser.TryParseTemplate(app._templateStore, template.Template, app._properties.DocumentAppType, templateDefaults, out _, out _)) { throw new NotSupportedException($"Unable to parse template file {template.Name}"); } } // Also add Screen and App templates (not xml, constructed in code on the server) GlobalTemplates.AddCodeOnlyTemplates(app._templateStore, templateDefaults, app._properties.DocumentAppType); var importedComponents = app.GetImportedComponents(); foreach (var control in app._screens) { string controlName = control.Key; var isTest = controlName == AppTestControlName; var subDir = isTest ? TestDir : CodeDir; WriteTopParent(dir, app, control.Key, control.Value, subDir); } foreach (var control in app._components) { string controlName = control.Key; app._templateStore.TryGetTemplate(controlName, out var templateState); bool isImported = importedComponents.Contains(templateState.TemplateOriginalName); var subDir = (isImported) ? ComponentPackageDir : ComponentCodeDir; WriteTopParent(dir, app, control.Key, control.Value, subDir); } // Write out control templates at top level, skipping component templates which are written alongside components var nonComponentControlTemplates = app._templateStore.Contents.Where(kvp => !(kvp.Value.IsComponentTemplate ?? false)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); dir.WriteAllJson("", "ControlTemplates.json", nonComponentControlTemplates); if (app._checksum != null) { app._checksum.ClientBuildDetails = _buildVerJson; dir.WriteAllJson(EntropyDir, FileKind.Checksum, app._checksum); } if (app._appCheckerResultJson != null) { dir.WriteAllJson(EntropyDir, FileKind.AppCheckerResult, app._appCheckerResultJson); } foreach (var file in app._assetFiles.Values) { dir.WriteAllBytes(AssetsDir, file.Name, file.RawBytes); } if (app._logoFile != null) { dir.WriteAllBytes(AssetsDir, app._logoFile.Name, app._logoFile.RawBytes); } if (app._themes != null) { dir.WriteAllJson(CodeDir, "Themes.json", app._themes); } if (app._resourcesJson != null) { dir.WriteAllJson(AssetsDir, "Resources.json", app._resourcesJson); } WriteDataSources(dir, app, errors); // Loose files. foreach (FileEntry file in app._unknownFiles.Values) { // Standardize the .json files so they're determinsitc and comparable if (file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { ReadOnlyMemory <byte> span = file.RawBytes; var je = JsonDocument.Parse(span).RootElement; var jsonStr = JsonNormalizer.Normalize(je); dir.WriteAllText(OtherDir, file.Name, jsonStr); } else { dir.WriteAllBytes(OtherDir, file.Name, file.RawBytes); } } var manifest = new CanvasManifestJson { FormatVersion = CurrentSourceVersion, Properties = app._properties, Header = app._header, PublishInfo = app._publishInfo, ScreenOrder = app._screenOrder }; dir.WriteAllJson("", FileKind.CanvasManifest, manifest); if (app._connections != null) { dir.WriteAllJson(ConnectionDir, FileKind.Connections, app._connections); } if (app._libraryReferences != null) { dir.WriteAllJson("", FileKind.ComponentReferences, app._libraryReferences); } dir.WriteAllJson(EntropyDir, FileKind.Entropy, app._entropy); }