From 3fff16b4a92a9a1e8af0dfd62a62337b5ac2367f Mon Sep 17 00:00:00 2001 From: Tomas Prokop Date: Sat, 30 May 2026 17:21:23 +0200 Subject: [PATCH 1/7] feat: redesign GenPage build support Implement filename-based multipage GenPage discovery, solution-owned uxagentproject declarations, and obj-only native filecontent projection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Dataverse/GenPage/README.md | 65 +---- ...XIS.DevKit.Build.Dataverse.GenPage.targets | 151 ++++++++---- ....Build.Dataverse.Solution.GenPages.targets | 100 ++++---- src/Dataverse/Tasks/Tasks/DiscoverGenPages.cs | 75 ++++++ .../Tasks/Tasks/EnsureGenPageDeclarations.cs | 230 ++++++++++++++++++ .../Tasks/Tasks/GenerateGenPageConfigJson.cs | 59 ----- .../Tasks/Tasks/GenerateGenPageFileXml.cs | 112 --------- .../Tasks/Tasks/GenerateGenPageProjectXml.cs | 63 ----- .../Tasks/Tasks/GenerateRuntimeTypes.cs | 88 +++++++ .../Tasks/Tasks/PatchGenPageCompiledCode.cs | 61 ----- .../Tasks/Tasks/ProjectGenPageNativeTree.cs | 74 ++++++ .../Tasks/Tasks/ValidateGenPageBundle.cs | 74 ++++++ ...ALXIS.DevKit.Build.Dataverse.Tasks.targets | 9 +- 13 files changed, 708 insertions(+), 453 deletions(-) create mode 100644 src/Dataverse/Tasks/Tasks/DiscoverGenPages.cs create mode 100644 src/Dataverse/Tasks/Tasks/EnsureGenPageDeclarations.cs delete mode 100644 src/Dataverse/Tasks/Tasks/GenerateGenPageConfigJson.cs delete mode 100644 src/Dataverse/Tasks/Tasks/GenerateGenPageFileXml.cs delete mode 100644 src/Dataverse/Tasks/Tasks/GenerateGenPageProjectXml.cs create mode 100644 src/Dataverse/Tasks/Tasks/GenerateRuntimeTypes.cs delete mode 100644 src/Dataverse/Tasks/Tasks/PatchGenPageCompiledCode.cs create mode 100644 src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs create mode 100644 src/Dataverse/Tasks/Tasks/ValidateGenPageBundle.cs diff --git a/src/Dataverse/GenPage/README.md b/src/Dataverse/GenPage/README.md index 8207b11..5d28a4d 100644 --- a/src/Dataverse/GenPage/README.md +++ b/src/Dataverse/GenPage/README.md @@ -1,63 +1,24 @@ -# TALXIS.DevKit.Build.Dataverse.GenPage +# TALXIS DevKit Dataverse GenPage build support -MSBuild integration for Dataverse generative page (GenPage) projects. Transpiles TSX source files via TypeScript, patches in RuntimeTypes, generates config metadata, and exposes output targets for Solution projects to discover and integrate GenPages as `uxagentprojects`. +MSBuild integration for Dataverse GenPage projects. -## Installation +A GenPage project is a project-type marker only. It carries no Dataverse IDs or page metadata. Every `*.tsx` file at the project root is treated as a page, and the page name is the file name without extension. Subfolders are regular source folders and never become pages. -```xml - -``` - -Or use the SDK approach: +## Project contract ```xml - - - GenPage - {your-genpage-guid} - datasource1,datasource2 - - + + GenPage + ``` -## Prerequisites - -- **Node.js** must be available on `PATH` -- **npx** must be available on `PATH` - -The build will fail with a descriptive error if either is missing. - -## How It Works - -The package sets `ProjectType` to `GenPage` and disables `GenerateAssemblyInfo` by default since this is not a traditional .NET assembly project. - -### Build-time targets - -1. **CheckGenPagePrereqs** — validates that the main TSX file exists and `node`/`npx` are on `PATH`. -2. **TranspileGenPage** (runs before `Build`) — executes `tsc` via `npx` to transpile TSX to JS, then patches the output by stripping `RuntimeTypes` imports and prepending `RuntimeTypes.js` content if present. -3. **GenerateGenPageConfig** — generates `config.json` from project properties (`GenPageDataSources`). -4. **CopyGenPageOutputs** (runs after `Build`) — copies `page.tsx`, `page.compiled`, and `config.json` to the output directory. - -### Integration targets - -Called by `TALXIS.DevKit.Build.Dataverse.Solution` via `ProjectReference`: - -- **GetProjectType** — returns `GenPage`. -- **GetGenPageOutputs** — exposes the compiled output folder and metadata for the solution to copy into `uxagentprojects/`. +Optional source files: -## MSBuild Properties +- `.config.json`, otherwise shared `genpage.config.json` +- `.firstPrompt.json`, otherwise shared `firstPrompt.json` -| Property | Default | Description | -|----------|---------|-------------| -| `ProjectType` | `GenPage` | Marks the project for reference discovery by Solution projects. | -| `GenPageMainFile` | `page.tsx` | Main TSX source file to transpile. | -| `GenPageName` | `$(MSBuildProjectName)` | Name used for the output folder and metadata. | -| `GenPageId` | _(required)_ | GUID identifying this GenPage in Dataverse. | -| `GenPageDataSources` | _(empty)_ | Comma-separated list of data source identifiers. | -| `LangVersion` | `latest` | C# language version for the project. | -| `GenerateAssemblyInfo` | `false` | Disables auto-generated assembly info. | +Build output is normalized to `$(TargetDir).js` for each root page. -## Related Packages +## Solution integration -- **Depends on**: `TALXIS.DevKit.Build.Dataverse.Tasks` -- **Consumed by**: `TALXIS.DevKit.Build.Dataverse.Solution` projects via `ProjectReference` +Solution projects discover referenced GenPage projects, call `GetGenPageOutputs`, ensure XML-only `uxagentprojects//...` declarations exist in solution source, then project native `filecontent/` only into the SolutionPackager metadata working directory under `obj`. diff --git a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets index 02e24d1..013a8e6 100644 --- a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets +++ b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets @@ -2,79 +2,126 @@ - page.tsx - $([System.IO.Path]::ChangeExtension('$(GenPageMainFile)', '.js')) - $(MSBuildProjectName) - - <_NormalizedGenPageId>$([System.String]::Copy('$(GenPageId)').Trim().Replace('{','').Replace('}','').ToLowerInvariant()) - genpage.config.json + true + false + $(MSBuildProjectDirectory)/RuntimeTypes.ts + react;react-dom;react-dom/client;react/jsx-runtime;@fluentui/react-components;@fluentui/react-icons - - - - - + + + <_ProjectType Include="$(MSBuildProjectFullPath)"> + $(ProjectType) + + - - + + + <_DiscoveredGenPage Remove="@(_DiscoveredGenPage)" /> + + + + + - + + + + + + + - + + + + + - - - - + Command="npm install" + Condition="'$(_GenPageNpmCiExitCode)'!='0'" /> + + + + + + + + + - - + + + + - + + + + <_GenPageBundle Remove="@(_GenPageBundle)" /> + - + - - + Condition="!Exists('$(MSBuildProjectDirectory)/dist/%(_DiscoveredGenPage.PageName).js') and Exists('$(MSBuildProjectDirectory)/build/%(_DiscoveredGenPage.PageName).js')" /> + + - - <_ProjectType Include="$(MSBuildProjectFullPath)"> - $(ProjectType) - + <_GenPageBundle Include="$(TargetDir)%(_DiscoveredGenPage.PageName).js"> + %(_DiscoveredGenPage.PageName) + $(TargetDir)%(_DiscoveredGenPage.PageName).js + + + - <_GenPageOutputs Include="$(MSBuildProjectDirectory)/$(OutputPath)$(GenPageName)"> - $(GenPageName) - $(_NormalizedGenPageId) - $(MSBuildProjectDirectory)/$(GenPageConfigFile) + <_GenPageOutputs Remove="@(_GenPageOutputs)" /> + <_GenPageOutputs Include="$(TargetDir)%(_DiscoveredGenPage.PageName).js"> + %(_DiscoveredGenPage.PageName) + $(TargetDir)%(_DiscoveredGenPage.PageName).js + %(_DiscoveredGenPage.EntryFile) + %(_DiscoveredGenPage.ConfigJsonPath) + %(_DiscoveredGenPage.FirstPromptJsonPath) + $(MSBuildProjectFullPath) diff --git a/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets b/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets index f162de1..e59142c 100644 --- a/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets +++ b/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets @@ -1,14 +1,10 @@ - - <_ProjectTypeFromReferences Remove="@(_ProjectTypeFromReferences)" /> @@ -47,70 +43,74 @@ DependsOnTargets="ProbeGenPages" Condition="'@(_GenPageProjects)'!=''"> + + <_GenPageOutput Remove="@(_GenPageOutput)" /> + + + - + + + + %(_GenPageOutput.PageName) + %(_GenPageOutput.CompiledJsPath) + %(_GenPageOutput.EntrySourcePath) + %(_GenPageOutput.ConfigJsonPath) + %(_GenPageOutput.FirstPromptJsonPath) + %(_GenPageOutput.OwningProjectPath) + + + + Text="GenPage outputs: @(GenPageRef->'%(PageName) from %(OwningProjectPath)', ', ')" + Condition="'@(GenPageRef)'!=''" /> - + Condition="'@(GenPageRef)'!=''"> - <_MetadataUxAgentProjectsDir>$(SolutionPackagerMetadataWorkingDirectory)/uxagentprojects/ + <_GenPageSolutionRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/$(SolutionRootPath)')) - + + <_DeclaredGenPage Remove="@(_DeclaredGenPage)" /> + + + + + + + + CustomizationsXmlFile="$(_GenPageSolutionRoot)/Other/Customizations.xml" + NodeName="uxagentprojects" + Condition="'@(_DeclaredGenPage)'!=''" /> + - - - - - - - - - - - - - + + diff --git a/src/Dataverse/Tasks/Tasks/DiscoverGenPages.cs b/src/Dataverse/Tasks/Tasks/DiscoverGenPages.cs new file mode 100644 index 0000000..541d4ab --- /dev/null +++ b/src/Dataverse/Tasks/Tasks/DiscoverGenPages.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public sealed class DiscoverGenPages : Task +{ + [Required] + public string ProjectDirectory { get; set; } = ""; + + [Output] + public ITaskItem[] GenPages { get; private set; } = Array.Empty(); + + public override bool Execute() + { + try + { + var root = Path.GetFullPath(ProjectDirectory); + if (!Directory.Exists(root)) + throw new DirectoryNotFoundException($"GenPage project directory not found: {root}"); + + var files = Directory.GetFiles(root, "*.tsx", SearchOption.TopDirectoryOnly) + .OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var duplicates = files + .GroupBy(f => Path.GetFileNameWithoutExtension(f), StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToArray(); + + if (duplicates.Length > 0) + { + Log.LogError($"Duplicate GenPage page name(s): {string.Join(", ", duplicates)}"); + return false; + } + + var items = new List(); + foreach (var file in files) + { + var pageName = Path.GetFileNameWithoutExtension(file); + if (string.IsNullOrWhiteSpace(pageName)) + { + Log.LogError($"Invalid GenPage page file name: {file}"); + continue; + } + + var siblingConfig = Path.Combine(root, pageName + ".config.json"); + var sharedConfig = Path.Combine(root, "genpage.config.json"); + var siblingPrompt = Path.Combine(root, pageName + ".firstPrompt.json"); + var sharedPrompt = Path.Combine(root, "firstPrompt.json"); + + var item = new TaskItem(file); + item.SetMetadata("PageName", pageName); + item.SetMetadata("EntryFile", file); + item.SetMetadata("ConfigJsonPath", File.Exists(siblingConfig) ? siblingConfig : (File.Exists(sharedConfig) ? sharedConfig : "")); + item.SetMetadata("FirstPromptJsonPath", File.Exists(siblingPrompt) ? siblingPrompt : (File.Exists(sharedPrompt) ? sharedPrompt : "")); + items.Add(item); + } + + if (items.Count == 0) + Log.LogWarning($"No GenPage root *.tsx files found in {root}."); + + GenPages = items.ToArray(); + return !Log.HasLoggedErrors; + } + catch (Exception ex) + { + Log.LogErrorFromException(ex, true, true, null); + return false; + } + } +} diff --git a/src/Dataverse/Tasks/Tasks/EnsureGenPageDeclarations.cs b/src/Dataverse/Tasks/Tasks/EnsureGenPageDeclarations.cs new file mode 100644 index 0000000..b3961b7 --- /dev/null +++ b/src/Dataverse/Tasks/Tasks/EnsureGenPageDeclarations.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public sealed class EnsureGenPageDeclarations : Task +{ + [Required] + public string SolutionRoot { get; set; } = ""; + + [Required] + public ITaskItem[] Pages { get; set; } = Array.Empty(); + + public string CustomizationsXmlPath { get; set; } = ""; + + [Output] + public ITaskItem[] DeclaredPages { get; private set; } = Array.Empty(); + + public override bool Execute() + { + try + { + var solutionRoot = Path.GetFullPath(SolutionRoot); + var uxRoot = Path.Combine(solutionRoot, "uxagentprojects"); + Directory.CreateDirectory(uxRoot); + + var duplicates = Pages.GroupBy(p => p.GetMetadata("PageName"), StringComparer.OrdinalIgnoreCase) + .Where(g => string.IsNullOrWhiteSpace(g.Key) || g.Count() > 1) + .Select(g => string.IsNullOrWhiteSpace(g.Key) ? "" : g.Key) + .ToArray(); + if (duplicates.Length > 0) + { + Log.LogError($"Duplicate GenPage page name(s) across referenced projects: {string.Join(", ", duplicates)}"); + return false; + } + + var pagesByName = Pages.ToDictionary(p => p.GetMetadata("PageName"), StringComparer.OrdinalIgnoreCase); + var existing = ReadExistingDeclarations(uxRoot); + + foreach (var group in existing.Values.GroupBy(d => d.PageName, StringComparer.OrdinalIgnoreCase).Where(g => g.Count() > 1)) + Log.LogError($"Duplicate uxagentproject schema name '{group.Key}' found in solution source."); + + foreach (var declaration in existing.Values) + { + if (!pagesByName.ContainsKey(declaration.PageName)) + Log.LogError($"Orphan GenPage declaration '{declaration.PageName}' at {declaration.ProjectXmlPath}; no referenced GenPage project emits this page."); + } + + if (Log.HasLoggedErrors) + return false; + + ValidateSitemapReferences(pagesByName.Keys.ToHashSet(StringComparer.OrdinalIgnoreCase)); + if (Log.HasLoggedErrors) + return false; + + var declared = new List(); + foreach (var page in Pages.OrderBy(p => p.GetMetadata("PageName"), StringComparer.OrdinalIgnoreCase)) + { + var pageName = page.GetMetadata("PageName"); + if (!existing.TryGetValue(pageName, out var declaration)) + { + declaration = CreateDeclaration(uxRoot, pageName); + existing.Add(pageName, declaration); + } + + var item = new TaskItem(page.ItemSpec); + page.CopyMetadataTo(item); + item.SetMetadata("PageGuid", declaration.PageGuid); + item.SetMetadata("FileGuid", declaration.FileGuid); + item.SetMetadata("ProjectXmlPath", declaration.ProjectXmlPath); + item.SetMetadata("FileXmlPath", declaration.FileXmlPath); + declared.Add(item); + } + + DeclaredPages = declared.ToArray(); + return true; + } + catch (Exception ex) + { + Log.LogErrorFromException(ex, true, true, null); + return false; + } + } + + private Dictionary ReadExistingDeclarations(string uxRoot) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (!Directory.Exists(uxRoot)) + return result; + + foreach (var projectXml in Directory.GetFiles(uxRoot, "uxagentproject.xml", SearchOption.AllDirectories)) + { + var doc = XDocument.Load(projectXml); + var root = doc.Root; + if (root == null || root.Name.LocalName != "uxagentproject") + continue; + + var pageName = root.Element("name")?.Value?.Trim(); + if (string.IsNullOrWhiteSpace(pageName)) + { + Log.LogError($"GenPage declaration is missing a name: {projectXml}"); + continue; + } + + var pageGuid = NormalizeGuid(root.Attribute("uxagentprojectid")?.Value); + if (string.IsNullOrWhiteSpace(pageGuid)) + pageGuid = NormalizeGuid(Path.GetFileName(Path.GetDirectoryName(projectXml) ?? "")); + if (string.IsNullOrWhiteSpace(pageGuid)) + { + Log.LogError($"GenPage declaration has no valid page GUID: {projectXml}"); + continue; + } + + var fileXml = Directory.GetFiles(Path.GetDirectoryName(projectXml) ?? uxRoot, "uxagentprojectfile.xml", SearchOption.AllDirectories).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).FirstOrDefault(); + if (fileXml == null) + { + Log.LogError($"GenPage declaration '{pageName}' has no uxagentprojectfile.xml under {Path.GetDirectoryName(projectXml)}."); + continue; + } + + var fileDoc = XDocument.Load(fileXml); + var fileGuid = NormalizeGuid(fileDoc.Root?.Attribute("uxagentprojectfileid")?.Value) ?? NormalizeGuid(Path.GetFileName(Path.GetDirectoryName(fileXml) ?? "")); + if (string.IsNullOrWhiteSpace(fileGuid)) + { + Log.LogError($"GenPage declaration '{pageName}' has no valid file GUID: {fileXml}"); + continue; + } + + if (result.ContainsKey(pageName)) + Log.LogError($"Duplicate GenPage declaration name '{pageName}' at {projectXml}."); + else + result.Add(pageName, new Declaration(pageName, pageGuid, fileGuid, projectXml, fileXml)); + } + + return result; + } + + private Declaration CreateDeclaration(string uxRoot, string pageName) + { + var pageGuid = Guid.NewGuid().ToString("D"); + var fileGuid = Guid.NewGuid().ToString("D"); + var pageDir = Path.Combine(uxRoot, pageGuid); + var fileDir = Path.Combine(pageDir, fileGuid); + Directory.CreateDirectory(fileDir); + + var projectXml = Path.Combine(pageDir, "uxagentproject.xml"); + var fileXml = Path.Combine(fileDir, "uxagentprojectfile.xml"); + + SaveXml(new XDocument(new XElement("uxagentproject", + new XAttribute("uxagentprojectid", pageGuid), + new XElement("iscustomizable", "1"), + new XElement("name", pageName), + new XElement("statecode", "0"), + new XElement("statuscode", "1"))), projectXml); + + SaveXml(new XDocument(new XElement("uxagentprojectfile", + new XAttribute("uxagentprojectfileid", fileGuid), + new XElement("filecontent", new XAttribute("mimetype", "application/octet-stream"), "src/pages/page.compiled"), + new XElement("filename", "src/pages/page.compiled"), + new XElement("filetype", "200000001"), + new XElement("iscustomizable", "1"), + new XElement("statecode", "0"), + new XElement("statuscode", "1"))), fileXml); + + Log.LogMessage(MessageImportance.High, $"Created GenPage declaration '{pageName}' ({pageGuid}) in {pageDir}"); + return new Declaration(pageName, pageGuid, fileGuid, projectXml, fileXml); + } + + private void ValidateSitemapReferences(HashSet knownPageNames) + { + var path = string.IsNullOrWhiteSpace(CustomizationsXmlPath) ? Path.Combine(SolutionRoot, "Other", "Customizations.xml") : CustomizationsXmlPath; + if (!File.Exists(path)) + return; + + var doc = XDocument.Load(path); + foreach (var element in doc.Descendants().Where(e => string.Equals(e.Name.LocalName, "SubArea", StringComparison.OrdinalIgnoreCase))) + { + foreach (var attrName in new[] { "PageName", "GenPage" }) + { + var value = element.Attributes().FirstOrDefault(a => string.Equals(a.Name.LocalName, attrName, StringComparison.OrdinalIgnoreCase))?.Value; + if (!string.IsNullOrWhiteSpace(value) && !knownPageNames.Contains(value.Trim())) + Log.LogError($"Sitemap GenPage reference '{value}' in {path} does not match any referenced GenPage page."); + } + } + } + + private static void SaveXml(XDocument doc, string path) + { + var settings = new XmlWriterSettings + { + Encoding = new UTF8Encoding(false), + Indent = true, + OmitXmlDeclaration = true, + NewLineChars = Environment.NewLine, + NewLineHandling = NewLineHandling.Replace + }; + using var writer = XmlWriter.Create(path, settings); + doc.Save(writer); + } + + private static string NormalizeGuid(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return ""; + return Guid.TryParse(value.Trim().Trim('{', '}'), out var guid) ? guid.ToString("D") : ""; + } + + private sealed class Declaration + { + public Declaration(string pageName, string pageGuid, string fileGuid, string projectXmlPath, string fileXmlPath) + { + PageName = pageName; + PageGuid = pageGuid; + FileGuid = fileGuid; + ProjectXmlPath = projectXmlPath; + FileXmlPath = fileXmlPath; + } + + public string PageName { get; } + public string PageGuid { get; } + public string FileGuid { get; } + public string ProjectXmlPath { get; } + public string FileXmlPath { get; } + } +} diff --git a/src/Dataverse/Tasks/Tasks/GenerateGenPageConfigJson.cs b/src/Dataverse/Tasks/Tasks/GenerateGenPageConfigJson.cs deleted file mode 100644 index 89db340..0000000 --- a/src/Dataverse/Tasks/Tasks/GenerateGenPageConfigJson.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -public class GenerateGenPageConfigJson : Task -{ - public string DataSources { get; set; } - - [Required] - public string OutputPath { get; set; } - - public override bool Execute() - { - try - { - var sources = new JArray(); - - if (!string.IsNullOrWhiteSpace(DataSources)) - { - var items = DataSources - .Split(',') - .Select(s => s.Trim()) - .Where(s => s.Length > 0); - - foreach (var item in items) - { - sources.Add(item); - } - } - - var config = new JObject - { - ["dataSources"] = sources, - ["model"] = "" - }; - - var directory = Path.GetDirectoryName(OutputPath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(OutputPath, config.ToString(Formatting.Indented), new UTF8Encoding(false)); - - Log.LogMessage(MessageImportance.High, $"Generated GenPage config.json: {OutputPath}"); - return true; - } - catch (Exception ex) - { - Log.LogErrorFromException(ex, true); - return false; - } - } -} diff --git a/src/Dataverse/Tasks/Tasks/GenerateGenPageFileXml.cs b/src/Dataverse/Tasks/Tasks/GenerateGenPageFileXml.cs deleted file mode 100644 index d6e3308..0000000 --- a/src/Dataverse/Tasks/Tasks/GenerateGenPageFileXml.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using System.Xml; -using System.Xml.Linq; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -public class GenerateGenPageFileXml : Task -{ - [Required] - public string ProjectId { get; set; } - - [Required] - public string SourceFilePath { get; set; } - - [Required] - public string FileName { get; set; } - - [Required] - public string FileType { get; set; } - - [Required] - public string MimeType { get; set; } - - [Required] - public string OutputDir { get; set; } - - [Output] - public string GeneratedFileId { get; set; } - - public override bool Execute() - { - try - { - if (!File.Exists(SourceFilePath)) - { - Log.LogError($"Source file not found: {SourceFilePath}"); - return false; - } - - var normalizedId = ProjectId.Trim().Trim('{', '}').ToLowerInvariant(); - - // Generate deterministic GUID from ProjectId + FileName - var seed = normalizedId + "-" + FileName; - GeneratedFileId = DeterministicGuid(seed); - - var fileDir = Path.Combine(OutputDir, GeneratedFileId); - var fileContentDir = Path.Combine(fileDir, "filecontent"); - Directory.CreateDirectory(fileContentDir); - - // Copy source file preserving original filename - var sourceFileName = Path.GetFileName(SourceFilePath); - var destPath = Path.Combine(fileContentDir, sourceFileName); - File.Copy(SourceFilePath, destPath, true); - - // Generate uxagentprojectfile.xml - var doc = new XDocument( - new XElement("uxagentprojectfile", - new XAttribute("uxagentprojectfileid", GeneratedFileId), - new XElement("filecontent", - new XAttribute("mimetype", MimeType), - sourceFileName), - new XElement("filename", FileName), - new XElement("filetype", FileType), - new XElement("iscustomizable", "1"), - new XElement("statecode", "0"), - new XElement("statuscode", "1") - ) - ); - - var xmlPath = Path.Combine(fileDir, "uxagentprojectfile.xml"); - - var settings = new XmlWriterSettings - { - Encoding = new UTF8Encoding(false), - Indent = true, - OmitXmlDeclaration = true, - NewLineChars = Environment.NewLine, - NewLineHandling = NewLineHandling.Replace - }; - - using (var writer = XmlWriter.Create(xmlPath, settings)) - { - doc.Save(writer); - } - - Log.LogMessage(MessageImportance.High, $"Generated GenPage file XML: {xmlPath} (FileId={GeneratedFileId})"); - return true; - } - catch (Exception ex) - { - Log.LogErrorFromException(ex, true); - return false; - } - } - - private static string DeterministicGuid(string seed) - { - using (var md5 = MD5.Create()) - { - var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(seed)); - var hex = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - return hex.Substring(0, 8) + "-" - + hex.Substring(8, 4) + "-" - + hex.Substring(12, 4) + "-" - + hex.Substring(16, 4) + "-" - + hex.Substring(20, 12); - } - } -} diff --git a/src/Dataverse/Tasks/Tasks/GenerateGenPageProjectXml.cs b/src/Dataverse/Tasks/Tasks/GenerateGenPageProjectXml.cs deleted file mode 100644 index c935470..0000000 --- a/src/Dataverse/Tasks/Tasks/GenerateGenPageProjectXml.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.IO; -using System.Text; -using System.Xml; -using System.Xml.Linq; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -public class GenerateGenPageProjectXml : Task -{ - [Required] - public string ProjectId { get; set; } - - [Required] - public string PageName { get; set; } - - [Required] - public string OutputDir { get; set; } - - public override bool Execute() - { - try - { - Directory.CreateDirectory(OutputDir); - - var normalizedId = ProjectId.Trim().Trim('{', '}').ToLowerInvariant(); - - var doc = new XDocument( - new XElement("uxagentproject", - new XAttribute("uxagentprojectid", normalizedId), - new XElement("iscustomizable", "1"), - new XElement("name", PageName), - new XElement("statecode", "0"), - new XElement("statuscode", "1") - ) - ); - - var outputPath = Path.Combine(OutputDir, "uxagentproject.xml"); - - var settings = new XmlWriterSettings - { - Encoding = new UTF8Encoding(false), - Indent = true, - OmitXmlDeclaration = true, - NewLineChars = Environment.NewLine, - NewLineHandling = NewLineHandling.Replace - }; - - using (var writer = XmlWriter.Create(outputPath, settings)) - { - doc.Save(writer); - } - - Log.LogMessage(MessageImportance.High, $"Generated GenPage project XML: {outputPath}"); - return true; - } - catch (Exception ex) - { - Log.LogErrorFromException(ex, true); - return false; - } - } -} diff --git a/src/Dataverse/Tasks/Tasks/GenerateRuntimeTypes.cs b/src/Dataverse/Tasks/Tasks/GenerateRuntimeTypes.cs new file mode 100644 index 0000000..3979a4b --- /dev/null +++ b/src/Dataverse/Tasks/Tasks/GenerateRuntimeTypes.cs @@ -0,0 +1,88 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public sealed class GenerateRuntimeTypes : Task +{ + [Required] + public string ProjectDirectory { get; set; } = ""; + + [Required] + public string OutputPath { get; set; } = ""; + + public string Command { get; set; } = ""; + + public override bool Execute() + { + try + { + var projectDirectory = Path.GetFullPath(ProjectDirectory); + var outputPath = Path.GetFullPath(OutputPath); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? projectDirectory); + + var command = string.IsNullOrWhiteSpace(Command) + ? $"pac generate-types --output \"{outputPath}\"" + : Command.Replace("$(OutputPath)", outputPath).Replace("$(ProjectDirectory)", projectDirectory); + + if (TryRun(command, projectDirectory) && File.Exists(outputPath)) + { + Log.LogMessage(MessageImportance.High, $"Generated GenPage runtime types: {outputPath}"); + return true; + } + + if (!File.Exists(outputPath)) + { + File.WriteAllText(outputPath, + "// Generated placeholder. Run PAC GenPage type generation to refresh runtime types.\n" + + "export {};\n", + new UTF8Encoding(false)); + Log.LogWarning($"PAC GenPage runtime type generation did not produce {outputPath}; wrote a placeholder RuntimeTypes.ts."); + } + + return true; + } + catch (Exception ex) + { + Log.LogErrorFromException(ex, true, true, null); + return false; + } + } + + private bool TryRun(string command, string workingDirectory) + { + try + { + var shell = Environment.OSVersion.Platform == PlatformID.Win32NT ? "cmd.exe" : "/bin/sh"; + var args = Environment.OSVersion.Platform == PlatformID.Win32NT ? $"/c {command}" : $"-c \"{command.Replace("\"", "\\\"")}\""; + using var process = new Process + { + StartInfo = new ProcessStartInfo(shell, args) + { + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.OutputDataReceived += (_, e) => { if (!string.IsNullOrEmpty(e.Data)) Log.LogMessage(MessageImportance.Low, e.Data); }; + process.ErrorDataReceived += (_, e) => { if (!string.IsNullOrEmpty(e.Data)) Log.LogMessage(MessageImportance.Low, e.Data); }; + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + if (process.ExitCode != 0) + Log.LogMessage(MessageImportance.Low, $"GenPage runtime type command exited with code {process.ExitCode}: {command}"); + return process.ExitCode == 0; + } + catch (Exception ex) + { + Log.LogMessage(MessageImportance.Low, $"Could not run GenPage runtime type command '{command}': {ex.Message}"); + return false; + } + } +} diff --git a/src/Dataverse/Tasks/Tasks/PatchGenPageCompiledCode.cs b/src/Dataverse/Tasks/Tasks/PatchGenPageCompiledCode.cs deleted file mode 100644 index c535017..0000000 --- a/src/Dataverse/Tasks/Tasks/PatchGenPageCompiledCode.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -public class PatchGenPageCompiledCode : Task -{ - [Required] - public string CompiledJsPath { get; set; } - - public string RuntimeTypesJsPath { get; set; } - - [Required] - public string OutputPath { get; set; } - - public override bool Execute() - { - try - { - if (!File.Exists(CompiledJsPath)) - { - Log.LogError($"Compiled JS file not found: {CompiledJsPath}"); - return false; - } - - var js = File.ReadAllText(CompiledJsPath, Encoding.UTF8); - - // Strip RuntimeTypes import lines (single or double quotes) - js = Regex.Replace(js, @"import\s+.*?from\s+['""]\.\/RuntimeTypes['""];?\s*", ""); - - var result = js; - - if (!string.IsNullOrEmpty(RuntimeTypesJsPath) && File.Exists(RuntimeTypesJsPath)) - { - var rt = File.ReadAllText(RuntimeTypesJsPath, Encoding.UTF8); - result = "// --- BEGIN GENERATED RUNTIME TYPES ---\n\n" - + rt - + "\n// --- END GENERATED RUNTIME TYPES ---\n\n" - + js; - } - - var directory = Path.GetDirectoryName(OutputPath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(OutputPath, result, new UTF8Encoding(false)); - - Log.LogMessage(MessageImportance.High, $"Patched GenPage compiled code: {OutputPath}"); - return true; - } - catch (Exception ex) - { - Log.LogErrorFromException(ex, true); - return false; - } - } -} diff --git a/src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs b/src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs new file mode 100644 index 0000000..c129588 --- /dev/null +++ b/src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public sealed class ProjectGenPageNativeTree : Task +{ + [Required] + public string MetadataRoot { get; set; } = ""; + + [Required] + public ITaskItem[] Pages { get; set; } = Array.Empty(); + + public override bool Execute() + { + try + { + var metadataRoot = Path.GetFullPath(MetadataRoot); + foreach (var page in Pages) + ProjectPage(metadataRoot, page); + return !Log.HasLoggedErrors; + } + catch (Exception ex) + { + Log.LogErrorFromException(ex, true, true, null); + return false; + } + } + + private void ProjectPage(string metadataRoot, ITaskItem page) + { + var pageName = page.GetMetadata("PageName"); + var pageGuid = page.GetMetadata("PageGuid"); + var fileGuid = page.GetMetadata("FileGuid"); + var projectXml = page.GetMetadata("ProjectXmlPath"); + var fileXml = page.GetMetadata("FileXmlPath"); + var compiledJs = page.GetMetadata("CompiledJsPath"); + var entrySource = page.GetMetadata("EntrySourcePath"); + var config = page.GetMetadata("ConfigJsonPath"); + var firstPrompt = page.GetMetadata("FirstPromptJsonPath"); + + RequireFile(projectXml, $"GenPage project XML for {pageName}"); + RequireFile(fileXml, $"GenPage file XML for {pageName}"); + RequireFile(compiledJs, $"compiled GenPage bundle for {pageName}"); + RequireFile(entrySource, $"GenPage source entry for {pageName}"); + if (!string.IsNullOrWhiteSpace(config)) RequireFile(config, $"GenPage config for {pageName}"); + if (!string.IsNullOrWhiteSpace(firstPrompt)) RequireFile(firstPrompt, $"GenPage firstPrompt for {pageName}"); + if (Log.HasLoggedErrors) return; + + var pageDir = Path.Combine(metadataRoot, "uxagentprojects", pageGuid); + var fileDir = Path.Combine(pageDir, fileGuid); + var fileContent = Path.Combine(fileDir, "filecontent"); + if (Directory.Exists(fileContent)) + Directory.Delete(fileContent, true); + + Directory.CreateDirectory(fileDir); + Directory.CreateDirectory(Path.Combine(fileContent, "src", "pages")); + + File.Copy(projectXml, Path.Combine(pageDir, "uxagentproject.xml"), true); + File.Copy(fileXml, Path.Combine(fileDir, "uxagentprojectfile.xml"), true); + File.Copy(compiledJs, Path.Combine(fileContent, "src", "pages", "page.compiled"), true); + File.Copy(entrySource, Path.Combine(fileContent, "src", "pages", "page.tsx"), true); + if (!string.IsNullOrWhiteSpace(config)) File.Copy(config, Path.Combine(fileContent, "config.json"), true); + if (!string.IsNullOrWhiteSpace(firstPrompt)) File.Copy(firstPrompt, Path.Combine(fileContent, "firstPrompt.json"), true); + + Log.LogMessage(MessageImportance.High, $"Projected GenPage '{pageName}' native tree to {pageDir}"); + } + + private void RequireFile(string path, string description) + { + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + Log.LogError($"Missing {description}: {path}"); + } +} diff --git a/src/Dataverse/Tasks/Tasks/ValidateGenPageBundle.cs b/src/Dataverse/Tasks/Tasks/ValidateGenPageBundle.cs new file mode 100644 index 0000000..cbee5ae --- /dev/null +++ b/src/Dataverse/Tasks/Tasks/ValidateGenPageBundle.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public sealed class ValidateGenPageBundle : Task +{ + private static readonly Regex DefaultExportRegex = new(@"\bexport\s+default\b", RegexOptions.Compiled); + private static readonly Regex FromImportRegex = new(@"\b(?:import|export)\s+(?:[^'"";]+?\s+from\s+)?['""](?[^'""]+)['""]", RegexOptions.Compiled); + private static readonly Regex DynamicImportRegex = new(@"\bimport\s*\(\s*['""](?[^'""]+)['""]\s*\)", RegexOptions.Compiled); + + [Required] + public ITaskItem[] Bundles { get; set; } = Array.Empty(); + + public string AllowedBareImports { get; set; } = "react;react-dom;react-dom/client;react/jsx-runtime;@fluentui/react-components;@fluentui/react-icons"; + + public override bool Execute() + { + var allowed = AllowedBareImports + .Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => s.Length > 0) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var item in Bundles) + { + var path = item.GetMetadata("CompiledJsPath"); + if (string.IsNullOrWhiteSpace(path)) + path = item.ItemSpec; + + var pageName = item.GetMetadata("PageName"); + if (!File.Exists(path)) + { + Log.LogError($"GenPage bundle not found for '{pageName}': {path}"); + continue; + } + + var content = File.ReadAllText(path); + if (!DefaultExportRegex.IsMatch(content)) + Log.LogError($"GenPage bundle '{path}' must contain an ESM default export."); + + foreach (var module in FindModules(content)) + { + if (IsBareImport(module) && !IsAllowed(module, allowed)) + Log.LogError($"GenPage bundle '{path}' contains unsupported bare import '{module}'. Bundle dependencies or add a supported external."); + } + } + + return !Log.HasLoggedErrors; + } + + private static IEnumerable FindModules(string content) + { + foreach (Match match in FromImportRegex.Matches(content)) + yield return match.Groups["module"].Value; + foreach (Match match in DynamicImportRegex.Matches(content)) + yield return match.Groups["module"].Value; + } + + private static bool IsBareImport(string module) + { + return !module.StartsWith(".", StringComparison.Ordinal) + && !module.StartsWith("/", StringComparison.Ordinal) + && !module.Contains("://", StringComparison.Ordinal); + } + + private static bool IsAllowed(string module, HashSet allowed) + { + return allowed.Contains(module) || allowed.Any(a => module.StartsWith(a + "/", StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets b/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets index 05d3dad..14ee28a 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets +++ b/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets @@ -40,8 +40,9 @@ - - - - + + + + + From 258b37d9c7290cb0c7b7cae72e4a2f31a5b97136 Mon Sep 17 00:00:00 2001 From: Tomas Prokop Date: Sun, 31 May 2026 15:28:46 +0200 Subject: [PATCH 2/7] fix: run GenPage build hooks on plain build Move GenPage page-discovery checks from target conditions to task conditions so plain dotnet build executes the hook chain, and accept Rollup named default export bundles while ignoring comments during import validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...XIS.DevKit.Build.Dataverse.GenPage.targets | 31 ++++--- .../Tasks/Tasks/ValidateGenPageBundle.cs | 89 ++++++++++++++++++- 2 files changed, 102 insertions(+), 18 deletions(-) diff --git a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets index 013a8e6..012f3af 100644 --- a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets +++ b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets @@ -61,27 +61,25 @@ + BeforeTargets="Build"> + Command="$(GenPageRuntimeTypesCommand)" + Condition="'@(_DiscoveredGenPage)'!=''" /> - - + DependsOnTargets="DiscoverGenPageEntries;InstallGenPagePackagesWithLock;InstallGenPagePackagesWithoutLock;GenerateGenPageRuntimeTypes" + Condition="'$(RunNodeBuild)'=='true'"> + + - + DependsOnTargets="DiscoverGenPageEntries"> + <_GenPageBundle Remove="@(_GenPageBundle)" /> @@ -89,17 +87,17 @@ + Condition="'@(_DiscoveredGenPage)'!='' and Exists('$(MSBuildProjectDirectory)/dist/%(_DiscoveredGenPage.PageName).js')" /> + Condition="'@(_DiscoveredGenPage)'!='' and !Exists('$(MSBuildProjectDirectory)/dist/%(_DiscoveredGenPage.PageName).js') and Exists('$(MSBuildProjectDirectory)/build/%(_DiscoveredGenPage.PageName).js')" /> - - + <_GenPageBundle Include="$(TargetDir)%(_DiscoveredGenPage.PageName).js"> %(_DiscoveredGenPage.PageName) $(TargetDir)%(_DiscoveredGenPage.PageName).js @@ -107,7 +105,8 @@ + AllowedBareImports="$(GenPageAllowedBareImports)" + Condition="'@(_GenPageBundle)'!=''" /> [^'""]+)['""]", RegexOptions.Compiled); private static readonly Regex DynamicImportRegex = new(@"\bimport\s*\(\s*['""](?[^'""]+)['""]\s*\)", RegexOptions.Compiled); @@ -38,8 +40,8 @@ public override bool Execute() continue; } - var content = File.ReadAllText(path); - if (!DefaultExportRegex.IsMatch(content)) + var content = StripComments(File.ReadAllText(path)); + if (!HasDefaultExport(content)) Log.LogError($"GenPage bundle '{path}' must contain an ESM default export."); foreach (var module in FindModules(content)) @@ -52,6 +54,11 @@ public override bool Execute() return !Log.HasLoggedErrors; } + private static bool HasDefaultExport(string content) + { + return DefaultExportRegex.IsMatch(content) || NamedDefaultExportRegex.IsMatch(content); + } + private static IEnumerable FindModules(string content) { foreach (Match match in FromImportRegex.Matches(content)) @@ -71,4 +78,82 @@ private static bool IsAllowed(string module, HashSet allowed) { return allowed.Contains(module) || allowed.Any(a => module.StartsWith(a + "/", StringComparison.OrdinalIgnoreCase)); } + + private static string StripComments(string content) + { + var result = new StringBuilder(content.Length); + var inSingleQuote = false; + var inDoubleQuote = false; + var inTemplate = false; + var inLineComment = false; + var inBlockComment = false; + var escaped = false; + + for (var i = 0; i < content.Length; i++) + { + var c = content[i]; + var next = i + 1 < content.Length ? content[i + 1] : '\0'; + + if (inLineComment) + { + if (c == '\r' || c == '\n') + { + inLineComment = false; + result.Append(c); + } + continue; + } + + if (inBlockComment) + { + if (c == '*' && next == '/') + { + inBlockComment = false; + i++; + } + else if (c == '\r' || c == '\n') + { + result.Append(c); + } + continue; + } + + if (!inSingleQuote && !inDoubleQuote && !inTemplate && c == '/' && next == '/') + { + inLineComment = true; + i++; + continue; + } + + if (!inSingleQuote && !inDoubleQuote && !inTemplate && c == '/' && next == '*') + { + inBlockComment = true; + i++; + continue; + } + + result.Append(c); + + if (escaped) + { + escaped = false; + continue; + } + + if ((inSingleQuote || inDoubleQuote || inTemplate) && c == '\\') + { + escaped = true; + continue; + } + + if (!inDoubleQuote && !inTemplate && c == '\'') + inSingleQuote = !inSingleQuote; + else if (!inSingleQuote && !inTemplate && c == '"') + inDoubleQuote = !inDoubleQuote; + else if (!inSingleQuote && !inDoubleQuote && c == '`') + inTemplate = !inTemplate; + } + + return result.ToString(); + } } From 4ed088dbd5c3bfeeeb6f0d52f7a4d07a172bf525 Mon Sep 17 00:00:00 2001 From: Tomas Prokop Date: Mon, 1 Jun 2026 10:43:36 +0200 Subject: [PATCH 3/7] Fix GenPage uxagentproject export layout Align GenPage projection with the SolutionPackager-compatible SCF ground truth from scf-ground-truth.md: emit uxagentprojectfiles child folders, flatten payload filecontent paths, write the verified XML shapes, and remove fake root/customizations declarations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ....Build.Dataverse.Solution.GenPages.targets | 11 - .../Tasks/Tasks/EnsureGenPageDeclarations.cs | 252 +++++++++++++++--- .../Tasks/Tasks/ProjectGenPageNativeTree.cs | 48 ++-- 3 files changed, 242 insertions(+), 69 deletions(-) diff --git a/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets b/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets index e59142c..5c19dd2 100644 --- a/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets +++ b/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets @@ -90,17 +90,6 @@ - - - p.GetMetadata("PageName"), StringComparer.OrdinalIgnoreCase) .Where(g => string.IsNullOrWhiteSpace(g.Key) || g.Count() > 1) .Select(g => string.IsNullOrWhiteSpace(g.Key) ? "" : g.Key) @@ -43,7 +51,7 @@ public override bool Execute() var existing = ReadExistingDeclarations(uxRoot); foreach (var group in existing.Values.GroupBy(d => d.PageName, StringComparer.OrdinalIgnoreCase).Where(g => g.Count() > 1)) - Log.LogError($"Duplicate uxagentproject schema name '{group.Key}' found in solution source."); + Log.LogError($"Duplicate uxagentproject name '{group.Key}' found in solution source."); foreach (var declaration in existing.Values) { @@ -68,12 +76,25 @@ public override bool Execute() existing.Add(pageName, declaration); } + NormalizeDeclaration(declaration, pageName, GetDesiredFiles(page).ToArray()); + var item = new TaskItem(page.ItemSpec); page.CopyMetadataTo(item); item.SetMetadata("PageGuid", declaration.PageGuid); - item.SetMetadata("FileGuid", declaration.FileGuid); item.SetMetadata("ProjectXmlPath", declaration.ProjectXmlPath); - item.SetMetadata("FileXmlPath", declaration.FileXmlPath); + foreach (var file in declaration.Files.Values) + { + item.SetMetadata(file.Definition.Prefix + "FileGuid", file.FileGuid); + item.SetMetadata(file.Definition.Prefix + "FileXmlPath", file.FileXmlPath); + } + + // Backwards-compatible metadata for the compiled payload. + if (declaration.Files.TryGetValue(CompiledFile.LogicalPath, out var compiled)) + { + item.SetMetadata("FileGuid", compiled.FileGuid); + item.SetMetadata("FileXmlPath", compiled.FileXmlPath); + } + declared.Add(item); } @@ -116,25 +137,31 @@ private Dictionary ReadExistingDeclarations(string uxRoot) continue; } - var fileXml = Directory.GetFiles(Path.GetDirectoryName(projectXml) ?? uxRoot, "uxagentprojectfile.xml", SearchOption.AllDirectories).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).FirstOrDefault(); - if (fileXml == null) + var declaration = new Declaration(pageName, pageGuid, projectXml); + var projectDir = Path.GetDirectoryName(projectXml) ?? uxRoot; + foreach (var fileXml in Directory.GetFiles(projectDir, "uxagentprojectfile.xml", SearchOption.AllDirectories).OrderBy(p => p, StringComparer.OrdinalIgnoreCase)) { - Log.LogError($"GenPage declaration '{pageName}' has no uxagentprojectfile.xml under {Path.GetDirectoryName(projectXml)}."); - continue; - } + var fileDoc = XDocument.Load(fileXml); + var fileGuid = NormalizeGuid(fileDoc.Root?.Attribute("uxagentprojectfileid")?.Value) ?? NormalizeGuid(Path.GetFileName(Path.GetDirectoryName(fileXml) ?? "")); + if (string.IsNullOrWhiteSpace(fileGuid)) + { + Log.LogError($"GenPage declaration '{pageName}' has no valid file GUID: {fileXml}"); + continue; + } - var fileDoc = XDocument.Load(fileXml); - var fileGuid = NormalizeGuid(fileDoc.Root?.Attribute("uxagentprojectfileid")?.Value) ?? NormalizeGuid(Path.GetFileName(Path.GetDirectoryName(fileXml) ?? "")); - if (string.IsNullOrWhiteSpace(fileGuid)) - { - Log.LogError($"GenPage declaration '{pageName}' has no valid file GUID: {fileXml}"); - continue; + var logicalPath = NormalizeLogicalPath(fileDoc.Root?.Element("filename")?.Value, fileDoc.Root?.Element("filecontent")?.Value); + var definition = GetDefinition(logicalPath); + if (definition == null) + continue; + + if (!declaration.Files.ContainsKey(definition.LogicalPath)) + declaration.Files.Add(definition.LogicalPath, new FileDeclaration(definition, fileGuid, fileXml)); } if (result.ContainsKey(pageName)) Log.LogError($"Duplicate GenPage declaration name '{pageName}' at {projectXml}."); else - result.Add(pageName, new Declaration(pageName, pageGuid, fileGuid, projectXml, fileXml)); + result.Add(pageName, declaration); } return result; @@ -143,32 +170,62 @@ private Dictionary ReadExistingDeclarations(string uxRoot) private Declaration CreateDeclaration(string uxRoot, string pageName) { var pageGuid = Guid.NewGuid().ToString("D"); - var fileGuid = Guid.NewGuid().ToString("D"); var pageDir = Path.Combine(uxRoot, pageGuid); - var fileDir = Path.Combine(pageDir, fileGuid); - Directory.CreateDirectory(fileDir); + Directory.CreateDirectory(pageDir); var projectXml = Path.Combine(pageDir, "uxagentproject.xml"); - var fileXml = Path.Combine(fileDir, "uxagentprojectfile.xml"); + var declaration = new Declaration(pageName, pageGuid, projectXml); - SaveXml(new XDocument(new XElement("uxagentproject", - new XAttribute("uxagentprojectid", pageGuid), - new XElement("iscustomizable", "1"), - new XElement("name", pageName), - new XElement("statecode", "0"), - new XElement("statuscode", "1"))), projectXml); + Log.LogMessage(MessageImportance.High, $"Created GenPage declaration '{pageName}' ({pageGuid}) in {pageDir}"); + return declaration; + } - SaveXml(new XDocument(new XElement("uxagentprojectfile", - new XAttribute("uxagentprojectfileid", fileGuid), - new XElement("filecontent", new XAttribute("mimetype", "application/octet-stream"), "src/pages/page.compiled"), - new XElement("filename", "src/pages/page.compiled"), - new XElement("filetype", "200000001"), - new XElement("iscustomizable", "1"), - new XElement("statecode", "0"), - new XElement("statuscode", "1"))), fileXml); + private void NormalizeDeclaration(Declaration declaration, string pageName, GenPageFileDefinition[] desiredFiles) + { + var pageDir = Path.Combine(Path.GetDirectoryName(declaration.ProjectXmlPath) ?? Path.Combine(Path.GetFullPath(SolutionRoot), "uxagentprojects"), ""); + Directory.CreateDirectory(pageDir); - Log.LogMessage(MessageImportance.High, $"Created GenPage declaration '{pageName}' ({pageGuid}) in {pageDir}"); - return new Declaration(pageName, pageGuid, fileGuid, projectXml, fileXml); + SaveProjectXml(declaration.ProjectXmlPath, declaration.PageGuid, pageName); + + var desired = new HashSet(desiredFiles.Select(f => f.LogicalPath), StringComparer.OrdinalIgnoreCase); + foreach (var definition in desiredFiles) + { + if (!declaration.Files.TryGetValue(definition.LogicalPath, out var file)) + { + var fileGuid = Guid.NewGuid().ToString("D"); + var fileXml = Path.Combine(pageDir, "uxagentprojectfiles", fileGuid, "uxagentprojectfile.xml"); + file = new FileDeclaration(definition, fileGuid, fileXml); + declaration.Files.Add(definition.LogicalPath, file); + } + else + { + file.Definition = definition; + file.FileXmlPath = Path.Combine(pageDir, "uxagentprojectfiles", file.FileGuid, "uxagentprojectfile.xml"); + } + + SaveFileXml(file.FileXmlPath, file.FileGuid, definition); + } + + foreach (var stale in declaration.Files.Values.Where(f => !desired.Contains(f.Definition.LogicalPath)).ToArray()) + { + TryDeleteDirectory(Path.GetDirectoryName(stale.FileXmlPath)); + declaration.Files.Remove(stale.Definition.LogicalPath); + } + + foreach (var legacyFileXml in Directory.GetFiles(pageDir, "uxagentprojectfile.xml", SearchOption.AllDirectories)) + { + var normalized = Path.GetFullPath(legacyFileXml); + if (!declaration.Files.Values.Any(f => string.Equals(Path.GetFullPath(f.FileXmlPath), normalized, StringComparison.OrdinalIgnoreCase))) + TryDeleteDirectory(Path.GetDirectoryName(legacyFileXml)); + } + } + + private IEnumerable GetDesiredFiles(ITaskItem page) + { + yield return CompiledFile; + yield return SourceFile; + yield return ConfigFile; + yield return FirstPromptFile; } private void ValidateSitemapReferences(HashSet knownPageNames) @@ -189,13 +246,87 @@ private void ValidateSitemapReferences(HashSet knownPageNames) } } - private static void SaveXml(XDocument doc, string path) + private void RemoveUxAgentProjectsPlaceholder() + { + var path = string.IsNullOrWhiteSpace(CustomizationsXmlPath) ? Path.Combine(SolutionRoot, "Other", "Customizations.xml") : CustomizationsXmlPath; + if (!File.Exists(path)) + return; + + var doc = XDocument.Load(path, LoadOptions.PreserveWhitespace); + var nodes = doc.Root?.Elements().Where(e => string.Equals(e.Name.LocalName, "uxagentprojects", StringComparison.OrdinalIgnoreCase)).ToArray() ?? Array.Empty(); + if (nodes.Length == 0) + return; + + foreach (var node in nodes) + node.Remove(); + SaveXml(doc, path, omitDeclaration: false); + Log.LogMessage(MessageImportance.High, $"Removed uxagentprojects placeholder from {path}"); + } + + private void RemoveGenPageRootComponents(string solutionRoot) + { + var path = Path.Combine(solutionRoot, "Other", "Solution.xml"); + if (!File.Exists(path)) + return; + + var doc = XDocument.Load(path, LoadOptions.PreserveWhitespace); + var nodes = doc.Descendants() + .Where(e => string.Equals(e.Name.LocalName, "RootComponent", StringComparison.OrdinalIgnoreCase) + && string.Equals(e.Attribute("type")?.Value, "10090", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + if (nodes.Length == 0) + return; + + foreach (var node in nodes) + node.Remove(); + SaveXml(doc, path, omitDeclaration: false); + Log.LogMessage(MessageImportance.High, $"Removed {nodes.Length} GenPage root component declaration(s) from {path}"); + } + + private static GenPageFileDefinition? GetDefinition(string logicalPath) + { + return new[] { CompiledFile, SourceFile, ConfigFile, FirstPromptFile } + .FirstOrDefault(f => string.Equals(f.LogicalPath, logicalPath, StringComparison.OrdinalIgnoreCase) + || string.Equals(f.BaseFileName, logicalPath, StringComparison.OrdinalIgnoreCase)); + } + + private static string NormalizeLogicalPath(string? filename, string? filecontent) + { + var value = string.IsNullOrWhiteSpace(filename) ? filecontent : filename; + value = (value ?? "").Trim().Replace('\\', '/'); + return GetDefinition(value)?.LogicalPath ?? value; + } + + private static void SaveProjectXml(string path, string pageGuid, string pageName) + { + SaveXml(new XDocument(new XElement("uxagentproject", + new XAttribute("uxagentprojectid", pageGuid), + new XElement("iscustomizable", "1"), + new XElement("name", pageName.ToLowerInvariant()), + new XElement("statecode", "0"), + new XElement("statuscode", "1"))), path, omitDeclaration: true); + } + + private static void SaveFileXml(string path, string fileGuid, GenPageFileDefinition definition) + { + SaveXml(new XDocument(new XElement("uxagentprojectfile", + new XAttribute("uxagentprojectfileid", fileGuid), + new XElement("filecontent", new XAttribute("mimetype", definition.MimeType), definition.BaseFileName), + new XElement("filename", definition.LogicalPath), + new XElement("filetype", definition.FileType), + new XElement("iscustomizable", "1"), + new XElement("statecode", "0"), + new XElement("statuscode", "1"))), path, omitDeclaration: true); + } + + private static void SaveXml(XDocument doc, string path, bool omitDeclaration) { + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? "."); var settings = new XmlWriterSettings { Encoding = new UTF8Encoding(false), Indent = true, - OmitXmlDeclaration = true, + OmitXmlDeclaration = omitDeclaration, NewLineChars = Environment.NewLine, NewLineHandling = NewLineHandling.Replace }; @@ -203,7 +334,13 @@ private static void SaveXml(XDocument doc, string path) doc.Save(writer); } - private static string NormalizeGuid(string value) + private static void TryDeleteDirectory(string? directory) + { + if (!string.IsNullOrWhiteSpace(directory) && Directory.Exists(directory)) + Directory.Delete(directory, true); + } + + private static string NormalizeGuid(string? value) { if (string.IsNullOrWhiteSpace(value)) return ""; @@ -212,19 +349,48 @@ private static string NormalizeGuid(string value) private sealed class Declaration { - public Declaration(string pageName, string pageGuid, string fileGuid, string projectXmlPath, string fileXmlPath) + public Declaration(string pageName, string pageGuid, string projectXmlPath) { PageName = pageName; PageGuid = pageGuid; - FileGuid = fileGuid; ProjectXmlPath = projectXmlPath; - FileXmlPath = fileXmlPath; } public string PageName { get; } public string PageGuid { get; } - public string FileGuid { get; } public string ProjectXmlPath { get; } - public string FileXmlPath { get; } + public Dictionary Files { get; } = new(StringComparer.OrdinalIgnoreCase); + } + + private sealed class FileDeclaration + { + public FileDeclaration(GenPageFileDefinition definition, string fileGuid, string fileXmlPath) + { + Definition = definition; + FileGuid = fileGuid; + FileXmlPath = fileXmlPath; + } + + public GenPageFileDefinition Definition { get; set; } + public string FileGuid { get; } + public string FileXmlPath { get; set; } + } + + private sealed class GenPageFileDefinition + { + public GenPageFileDefinition(string prefix, string logicalPath, string baseFileName, string mimeType, string fileType) + { + Prefix = prefix; + LogicalPath = logicalPath; + BaseFileName = baseFileName; + MimeType = mimeType; + FileType = fileType; + } + + public string Prefix { get; } + public string LogicalPath { get; } + public string BaseFileName { get; } + public string MimeType { get; } + public string FileType { get; } } } diff --git a/src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs b/src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs index c129588..e3302fe 100644 --- a/src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs +++ b/src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs @@ -31,16 +31,15 @@ private void ProjectPage(string metadataRoot, ITaskItem page) { var pageName = page.GetMetadata("PageName"); var pageGuid = page.GetMetadata("PageGuid"); - var fileGuid = page.GetMetadata("FileGuid"); var projectXml = page.GetMetadata("ProjectXmlPath"); - var fileXml = page.GetMetadata("FileXmlPath"); var compiledJs = page.GetMetadata("CompiledJsPath"); var entrySource = page.GetMetadata("EntrySourcePath"); var config = page.GetMetadata("ConfigJsonPath"); var firstPrompt = page.GetMetadata("FirstPromptJsonPath"); RequireFile(projectXml, $"GenPage project XML for {pageName}"); - RequireFile(fileXml, $"GenPage file XML for {pageName}"); + RequireFile(page.GetMetadata("CompiledFileXmlPath"), $"compiled GenPage file XML for {pageName}"); + RequireFile(page.GetMetadata("SourceFileXmlPath"), $"source GenPage file XML for {pageName}"); RequireFile(compiledJs, $"compiled GenPage bundle for {pageName}"); RequireFile(entrySource, $"GenPage source entry for {pageName}"); if (!string.IsNullOrWhiteSpace(config)) RequireFile(config, $"GenPage config for {pageName}"); @@ -48,24 +47,43 @@ private void ProjectPage(string metadataRoot, ITaskItem page) if (Log.HasLoggedErrors) return; var pageDir = Path.Combine(metadataRoot, "uxagentprojects", pageGuid); - var fileDir = Path.Combine(pageDir, fileGuid); - var fileContent = Path.Combine(fileDir, "filecontent"); - if (Directory.Exists(fileContent)) - Directory.Delete(fileContent, true); - - Directory.CreateDirectory(fileDir); - Directory.CreateDirectory(Path.Combine(fileContent, "src", "pages")); + if (Directory.Exists(pageDir)) + Directory.Delete(pageDir, true); + Directory.CreateDirectory(pageDir); File.Copy(projectXml, Path.Combine(pageDir, "uxagentproject.xml"), true); - File.Copy(fileXml, Path.Combine(fileDir, "uxagentprojectfile.xml"), true); - File.Copy(compiledJs, Path.Combine(fileContent, "src", "pages", "page.compiled"), true); - File.Copy(entrySource, Path.Combine(fileContent, "src", "pages", "page.tsx"), true); - if (!string.IsNullOrWhiteSpace(config)) File.Copy(config, Path.Combine(fileContent, "config.json"), true); - if (!string.IsNullOrWhiteSpace(firstPrompt)) File.Copy(firstPrompt, Path.Combine(fileContent, "firstPrompt.json"), true); + + ProjectFile(pageDir, page, "Compiled", compiledJs, "page.compiled", pageName, ""); + ProjectFile(pageDir, page, "Source", entrySource, "page.tsx", pageName, ""); + ProjectFile(pageDir, page, "Config", config, "config.json", pageName, "{\"dataSources\":[],\"model\":\"\"}"); + ProjectFile(pageDir, page, "FirstPrompt", firstPrompt, "firstPrompt.json", pageName, "{\"userMessage\":\"\",\"agentMessage\":\"\"}"); Log.LogMessage(MessageImportance.High, $"Projected GenPage '{pageName}' native tree to {pageDir}"); } + private void ProjectFile(string pageDir, ITaskItem page, string prefix, string payloadPath, string payloadBaseName, string pageName, string defaultPayload) + { + var fileGuid = page.GetMetadata(prefix + "FileGuid"); + var fileXml = page.GetMetadata(prefix + "FileXmlPath"); + RequireFile(fileXml, $"{prefix} GenPage file XML for {pageName}"); + if (string.IsNullOrWhiteSpace(defaultPayload)) + RequireFile(payloadPath, $"{prefix} GenPage payload for {pageName}"); + else if (!string.IsNullOrWhiteSpace(payloadPath)) + RequireFile(payloadPath, $"{prefix} GenPage payload for {pageName}"); + if (Log.HasLoggedErrors) return; + + var fileDir = Path.Combine(pageDir, "uxagentprojectfiles", fileGuid); + var fileContent = Path.Combine(fileDir, "filecontent"); + Directory.CreateDirectory(fileContent); + File.Copy(fileXml, Path.Combine(fileDir, "uxagentprojectfile.xml"), true); + + var destination = Path.Combine(fileContent, payloadBaseName); + if (!string.IsNullOrWhiteSpace(payloadPath)) + File.Copy(payloadPath, destination, true); + else + File.WriteAllText(destination, defaultPayload); + } + private void RequireFile(string path, string description) { if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) From ce80d3e4b6e2a8a1ec1c2928b5fd69dc9c5b456a Mon Sep 17 00:00:00 2001 From: Tomas Prokop Date: Mon, 1 Jun 2026 17:55:00 +0200 Subject: [PATCH 4/7] refactor(GenPage): prefix project-specific task names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...XIS.DevKit.Build.Dataverse.GenPage.targets | 46 +++++++++---------- ....Build.Dataverse.Solution.GenPages.targets | 24 +++++----- ...verGenPages.cs => GenPageDiscoverPages.cs} | 2 +- ...ations.cs => GenPageEnsureDeclarations.cs} | 2 +- ...ypes.cs => GenPageGenerateRuntimeTypes.cs} | 2 +- ...iveTree.cs => GenPageProjectNativeTree.cs} | 2 +- ...PageBundle.cs => GenPageValidateBundle.cs} | 2 +- ...ALXIS.DevKit.Build.Dataverse.Tasks.targets | 10 ++-- 8 files changed, 45 insertions(+), 45 deletions(-) rename src/Dataverse/Tasks/Tasks/{DiscoverGenPages.cs => GenPageDiscoverPages.cs} (98%) rename src/Dataverse/Tasks/Tasks/{EnsureGenPageDeclarations.cs => GenPageEnsureDeclarations.cs} (99%) rename src/Dataverse/Tasks/Tasks/{GenerateRuntimeTypes.cs => GenPageGenerateRuntimeTypes.cs} (98%) rename src/Dataverse/Tasks/Tasks/{ProjectGenPageNativeTree.cs => GenPageProjectNativeTree.cs} (98%) rename src/Dataverse/Tasks/Tasks/{ValidateGenPageBundle.cs => GenPageValidateBundle.cs} (99%) diff --git a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets index 012f3af..ce20221 100644 --- a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets +++ b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets @@ -16,17 +16,17 @@ - + <_DiscoveredGenPage Remove="@(_DiscoveredGenPage)" /> - + - + - @@ -36,8 +36,8 @@ - @@ -50,8 +50,8 @@ Condition="'$(_GenPageNpmCiExitCode)'!='0'" /> - @@ -59,26 +59,26 @@ - - + - - + <_GenPageBundle Remove="@(_GenPageBundle)" /> @@ -104,13 +104,13 @@ - - <_GenPageOutputs Remove="@(_GenPageOutputs)" /> diff --git a/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets b/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets index 5c19dd2..ed2fcb0 100644 --- a/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets +++ b/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.GenPages.targets @@ -1,8 +1,8 @@ - @@ -38,9 +38,9 @@ Condition="'@(_GenPageProjects)'!=''" /> - @@ -49,7 +49,7 @@ @@ -71,9 +71,9 @@ Condition="'@(GenPageRef)'!=''" /> - @@ -84,21 +84,21 @@ <_DeclaredGenPage Remove="@(_DeclaredGenPage)" /> - - + - - diff --git a/src/Dataverse/Tasks/Tasks/DiscoverGenPages.cs b/src/Dataverse/Tasks/Tasks/GenPageDiscoverPages.cs similarity index 98% rename from src/Dataverse/Tasks/Tasks/DiscoverGenPages.cs rename to src/Dataverse/Tasks/Tasks/GenPageDiscoverPages.cs index 541d4ab..3c823aa 100644 --- a/src/Dataverse/Tasks/Tasks/DiscoverGenPages.cs +++ b/src/Dataverse/Tasks/Tasks/GenPageDiscoverPages.cs @@ -5,7 +5,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -public sealed class DiscoverGenPages : Task +public sealed class GenPageDiscoverPages : Task { [Required] public string ProjectDirectory { get; set; } = ""; diff --git a/src/Dataverse/Tasks/Tasks/EnsureGenPageDeclarations.cs b/src/Dataverse/Tasks/Tasks/GenPageEnsureDeclarations.cs similarity index 99% rename from src/Dataverse/Tasks/Tasks/EnsureGenPageDeclarations.cs rename to src/Dataverse/Tasks/Tasks/GenPageEnsureDeclarations.cs index 2e4b49f..f9c8ead 100644 --- a/src/Dataverse/Tasks/Tasks/EnsureGenPageDeclarations.cs +++ b/src/Dataverse/Tasks/Tasks/GenPageEnsureDeclarations.cs @@ -8,7 +8,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -public sealed class EnsureGenPageDeclarations : Task +public sealed class GenPageEnsureDeclarations : Task { private static readonly GenPageFileDefinition CompiledFile = new("Compiled", "src/pages/page.compiled", "page.compiled", "application/octet-stream", "200000001"); private static readonly GenPageFileDefinition SourceFile = new("Source", "src/pages/page.tsx", "page.tsx", "application/octet-stream", "200000000"); diff --git a/src/Dataverse/Tasks/Tasks/GenerateRuntimeTypes.cs b/src/Dataverse/Tasks/Tasks/GenPageGenerateRuntimeTypes.cs similarity index 98% rename from src/Dataverse/Tasks/Tasks/GenerateRuntimeTypes.cs rename to src/Dataverse/Tasks/Tasks/GenPageGenerateRuntimeTypes.cs index 3979a4b..098ad6a 100644 --- a/src/Dataverse/Tasks/Tasks/GenerateRuntimeTypes.cs +++ b/src/Dataverse/Tasks/Tasks/GenPageGenerateRuntimeTypes.cs @@ -5,7 +5,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -public sealed class GenerateRuntimeTypes : Task +public sealed class GenPageGenerateRuntimeTypes : Task { [Required] public string ProjectDirectory { get; set; } = ""; diff --git a/src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs b/src/Dataverse/Tasks/Tasks/GenPageProjectNativeTree.cs similarity index 98% rename from src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs rename to src/Dataverse/Tasks/Tasks/GenPageProjectNativeTree.cs index e3302fe..a22a6b6 100644 --- a/src/Dataverse/Tasks/Tasks/ProjectGenPageNativeTree.cs +++ b/src/Dataverse/Tasks/Tasks/GenPageProjectNativeTree.cs @@ -3,7 +3,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -public sealed class ProjectGenPageNativeTree : Task +public sealed class GenPageProjectNativeTree : Task { [Required] public string MetadataRoot { get; set; } = ""; diff --git a/src/Dataverse/Tasks/Tasks/ValidateGenPageBundle.cs b/src/Dataverse/Tasks/Tasks/GenPageValidateBundle.cs similarity index 99% rename from src/Dataverse/Tasks/Tasks/ValidateGenPageBundle.cs rename to src/Dataverse/Tasks/Tasks/GenPageValidateBundle.cs index f5f2ba6..4c83eac 100644 --- a/src/Dataverse/Tasks/Tasks/ValidateGenPageBundle.cs +++ b/src/Dataverse/Tasks/Tasks/GenPageValidateBundle.cs @@ -7,7 +7,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -public sealed class ValidateGenPageBundle : Task +public sealed class GenPageValidateBundle : Task { private static readonly Regex DefaultExportRegex = new(@"\bexport\s+default\b", RegexOptions.Compiled); private static readonly Regex NamedDefaultExportRegex = new(@"\bexport\s*\{\s*[^}]*\bas\s+default\b[^}]*\}", RegexOptions.Compiled); diff --git a/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets b/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets index 14ee28a..8f2a2ae 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets +++ b/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets @@ -40,9 +40,9 @@ - - - - - + + + + + From d26cff7410ac8a4cd3110ae926a93617da312255 Mon Sep 17 00:00:00 2001 From: Tomas Prokop Date: Wed, 3 Jun 2026 15:29:58 +0200 Subject: [PATCH 5/7] feat(Tasks): add RunNodeTool task, unify Node/npm invocation across all project types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add dedicated cross-cutting RunNodeTool MSBuild task with shared NodeProcessRunner helper (RunNodeTool.cs) - Direct executable invocation via PATH resolution — no cmd.exe/sh wrapper, eliminating orphan process risk - Kill-tree on timeout/exception: Process.Kill(entireProcessTree:true) on net6.0+; taskkill /T /F (Windows) / SIGTERM process-group (Unix) on net472, guarded with #if NET6_0_OR_GREATER - Async stdout/stderr streaming at Normal/High importance (was Low, hiding failures) - TimeoutSeconds (default 600), [Output] ExitCode and TimedOut properties - Route all npm ci/install/run build and node/npm --version probes through RunNodeTool in GenPage, ScriptLibrary, PCF, and CodeApp targets - npm ci fallback now emits a Warning before falling back to npm install (was silently swallowed) - GenPageGenerateRuntimeTypes: replace shell-wrapper TryRun with NodeProcessRunner; pac generate-types failures now warn at Normal/High importance instead of being hidden at Low Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...XIS.DevKit.Build.Dataverse.CodeApp.targets | 16 +- ...XIS.DevKit.Build.Dataverse.GenPage.targets | 28 +- .../TALXIS.DevKit.Build.Dataverse.Pcf.targets | 2 +- ...vKit.Build.Dataverse.ScriptLibrary.targets | 16 +- .../Tasks/GenPageGenerateRuntimeTypes.cs | 56 ++- src/Dataverse/Tasks/Tasks/RunNodeTool.cs | 395 ++++++++++++++++++ ...ALXIS.DevKit.Build.Dataverse.Tasks.targets | 1 + 7 files changed, 460 insertions(+), 54 deletions(-) create mode 100644 src/Dataverse/Tasks/Tasks/RunNodeTool.cs diff --git a/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets b/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets index 1e6dd98..3c54a86 100644 --- a/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets +++ b/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets @@ -9,10 +9,14 @@ - - - - + + + + + + + + - + - + - - - - + + + + + + + + - + - - + + + - + - + - + diff --git a/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets b/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets index fc30977..b1694c7 100644 --- a/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets +++ b/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets @@ -13,18 +13,22 @@ BeforeTargets="Build" Condition="'$(RunNodeBuild)'=='true'"> - - + + - - - - + + + + + + + + diff --git a/src/Dataverse/Tasks/Tasks/GenPageGenerateRuntimeTypes.cs b/src/Dataverse/Tasks/Tasks/GenPageGenerateRuntimeTypes.cs index 098ad6a..777c780 100644 --- a/src/Dataverse/Tasks/Tasks/GenPageGenerateRuntimeTypes.cs +++ b/src/Dataverse/Tasks/Tasks/GenPageGenerateRuntimeTypes.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.IO; using System.Text; using Microsoft.Build.Framework; @@ -15,6 +14,8 @@ public sealed class GenPageGenerateRuntimeTypes : Task public string Command { get; set; } = ""; + public int TimeoutSeconds { get; set; } = 600; + public override bool Execute() { try @@ -27,12 +28,16 @@ public override bool Execute() ? $"pac generate-types --output \"{outputPath}\"" : Command.Replace("$(OutputPath)", outputPath).Replace("$(ProjectDirectory)", projectDirectory); - if (TryRun(command, projectDirectory) && File.Exists(outputPath)) + var commandSucceeded = TryRun(command, projectDirectory); + if (commandSucceeded && File.Exists(outputPath)) { Log.LogMessage(MessageImportance.High, $"Generated GenPage runtime types: {outputPath}"); return true; } + if (!commandSucceeded) + Log.LogWarning($"PAC GenPage runtime type generation command did not complete successfully: {command}"); + if (!File.Exists(outputPath)) { File.WriteAllText(outputPath, @@ -53,36 +58,27 @@ public override bool Execute() private bool TryRun(string command, string workingDirectory) { - try + if (!NodeProcessRunner.TrySplitCommandLine(command, out var fileName, out var arguments, out var error)) { - var shell = Environment.OSVersion.Platform == PlatformID.Win32NT ? "cmd.exe" : "/bin/sh"; - var args = Environment.OSVersion.Platform == PlatformID.Win32NT ? $"/c {command}" : $"-c \"{command.Replace("\"", "\\\"")}\""; - using var process = new Process - { - StartInfo = new ProcessStartInfo(shell, args) - { - WorkingDirectory = workingDirectory, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - } - }; - - process.OutputDataReceived += (_, e) => { if (!string.IsNullOrEmpty(e.Data)) Log.LogMessage(MessageImportance.Low, e.Data); }; - process.ErrorDataReceived += (_, e) => { if (!string.IsNullOrEmpty(e.Data)) Log.LogMessage(MessageImportance.Low, e.Data); }; - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - if (process.ExitCode != 0) - Log.LogMessage(MessageImportance.Low, $"GenPage runtime type command exited with code {process.ExitCode}: {command}"); - return process.ExitCode == 0; - } - catch (Exception ex) - { - Log.LogMessage(MessageImportance.Low, $"Could not run GenPage runtime type command '{command}': {ex.Message}"); + Log.LogWarning($"Could not parse GenPage runtime type command '{command}': {error}"); return false; } + + var result = NodeProcessRunner.Run( + Log, + fileName, + arguments, + workingDirectory, + TimeoutSeconds, + ignoreExitCode: true, + standardOutputImportance: MessageImportance.Normal, + standardErrorImportance: MessageImportance.High); + + if (result.TimedOut) + Log.LogWarning($"GenPage runtime type command timed out after {TimeoutSeconds} seconds: {command}"); + else if (result.ExitCode != 0) + Log.LogWarning($"GenPage runtime type command exited with code {result.ExitCode}: {command}"); + + return result.Succeeded; } } diff --git a/src/Dataverse/Tasks/Tasks/RunNodeTool.cs b/src/Dataverse/Tasks/Tasks/RunNodeTool.cs new file mode 100644 index 0000000..a5a09d8 --- /dev/null +++ b/src/Dataverse/Tasks/Tasks/RunNodeTool.cs @@ -0,0 +1,395 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public sealed class RunNodeTool : Task +{ + [Required] + public string FileName { get; set; } = ""; + + public string Arguments { get; set; } = ""; + + public string WorkingDirectory { get; set; } = ""; + + public int TimeoutSeconds { get; set; } = 600; + + public bool IgnoreExitCode { get; set; } + + public string StandardOutputImportance { get; set; } = "Normal"; + + public string StandardErrorImportance { get; set; } = "High"; + + [Output] + public int ExitCode { get; set; } + + [Output] + public bool TimedOut { get; set; } + + public override bool Execute() + { + var result = NodeProcessRunner.Run( + Log, + FileName, + Arguments, + WorkingDirectory, + TimeoutSeconds, + IgnoreExitCode, + NodeProcessRunner.ParseImportance(StandardOutputImportance, MessageImportance.Normal), + NodeProcessRunner.ParseImportance(StandardErrorImportance, MessageImportance.High)); + + ExitCode = result.ExitCode; + TimedOut = result.TimedOut; + + return result.Succeeded || IgnoreExitCode; + } +} + +internal sealed class NodeProcessResult +{ + public int ExitCode { get; set; } = -1; + + public bool TimedOut { get; set; } + + public bool Succeeded { get; set; } +} + +internal static class NodeProcessRunner +{ + public static NodeProcessResult Run( + TaskLoggingHelper log, + string fileName, + string arguments, + string workingDirectory, + int timeoutSeconds, + bool ignoreExitCode, + MessageImportance standardOutputImportance, + MessageImportance standardErrorImportance) + { + var result = new NodeProcessResult(); + Process process = null; + + try + { + if (string.IsNullOrWhiteSpace(fileName)) + { + LogFailure(log, ignoreExitCode, "Node tool FileName is required."); + return result; + } + + var effectiveWorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) + ? Directory.GetCurrentDirectory() + : Path.GetFullPath(workingDirectory); + + if (!Directory.Exists(effectiveWorkingDirectory)) + { + LogFailure(log, ignoreExitCode, $"Working directory does not exist: {effectiveWorkingDirectory}"); + return result; + } + + var resolvedFileName = ResolveExecutable(fileName, effectiveWorkingDirectory); + var timeoutMilliseconds = timeoutSeconds <= 0 + ? 600000 + : checked(timeoutSeconds * 1000); + + log.LogMessage(MessageImportance.High, $"Running {resolvedFileName} {arguments}".Trim()); + + process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = resolvedFileName, + Arguments = arguments ?? string.Empty, + WorkingDirectory = effectiveWorkingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }, + EnableRaisingEvents = false + }; + + process.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + log.LogMessage(standardOutputImportance, e.Data); + }; + process.ErrorDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + log.LogMessage(standardErrorImportance, e.Data); + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + if (!process.WaitForExit(timeoutMilliseconds)) + { + result.TimedOut = true; + KillProcessTree(process, log); + process.WaitForExit(10000); + LogFailure(log, ignoreExitCode, $"Command timed out after {timeoutSeconds} seconds: {resolvedFileName} {arguments}".Trim()); + return result; + } + + process.WaitForExit(); + result.ExitCode = process.ExitCode; + result.Succeeded = process.ExitCode == 0; + + if (!result.Succeeded) + LogFailure(log, ignoreExitCode, $"Command exited with code {process.ExitCode}: {resolvedFileName} {arguments}".Trim()); + + return result; + } + catch (Exception ex) + { + if (process != null) + KillProcessTree(process, log); + + LogFailure(log, ignoreExitCode, $"Could not run command '{fileName} {arguments}'. {ex.Message}".Trim()); + return result; + } + finally + { + if (process != null) + process.Dispose(); + } + } + + public static MessageImportance ParseImportance(string value, MessageImportance defaultValue) + { + MessageImportance parsed; + return Enum.TryParse(value, true, out parsed) ? parsed : defaultValue; + } + + public static bool TrySplitCommandLine(string command, out string fileName, out string arguments, out string error) + { + fileName = string.Empty; + arguments = string.Empty; + error = string.Empty; + + if (string.IsNullOrWhiteSpace(command)) + { + error = "Command is empty."; + return false; + } + + var trimmed = command.Trim(); + if (trimmed[0] == '"') + { + var closingQuote = trimmed.IndexOf('"', 1); + if (closingQuote < 0) + { + error = "Quoted command executable is missing a closing quote."; + return false; + } + + fileName = trimmed.Substring(1, closingQuote - 1); + arguments = trimmed.Substring(closingQuote + 1).TrimStart(); + return !string.IsNullOrWhiteSpace(fileName); + } + + var firstWhitespace = -1; + for (var i = 0; i < trimmed.Length; i++) + { + if (char.IsWhiteSpace(trimmed[i])) + { + firstWhitespace = i; + break; + } + } + + if (firstWhitespace < 0) + { + fileName = trimmed; + return true; + } + + fileName = trimmed.Substring(0, firstWhitespace); + arguments = trimmed.Substring(firstWhitespace + 1).TrimStart(); + return !string.IsNullOrWhiteSpace(fileName); + } + + private static void LogFailure(TaskLoggingHelper log, bool ignoreExitCode, string message) + { + if (ignoreExitCode) + log.LogMessage(MessageImportance.High, message); + else + log.LogError(message); + } + + private static string ResolveExecutable(string fileName, string workingDirectory) + { + var hasDirectory = fileName.IndexOf(Path.DirectorySeparatorChar) >= 0 || + fileName.IndexOf(Path.AltDirectorySeparatorChar) >= 0 || + Path.IsPathRooted(fileName); + + if (hasDirectory) + { + if (File.Exists(fileName)) + return Path.GetFullPath(fileName); + + var rootedCandidate = Path.Combine(workingDirectory, fileName); + if (File.Exists(rootedCandidate)) + return Path.GetFullPath(rootedCandidate); + + if (IsWindows() && string.IsNullOrEmpty(Path.GetExtension(fileName))) + { + foreach (var extension in GetWindowsExecutableExtensions()) + { + if (File.Exists(fileName + extension)) + return Path.GetFullPath(fileName + extension); + + var workingDirectoryCandidate = Path.Combine(workingDirectory, fileName + extension); + if (File.Exists(workingDirectoryCandidate)) + return Path.GetFullPath(workingDirectoryCandidate); + } + } + + return fileName; + } + + foreach (var directory in GetPathDirectories()) + { + if (IsWindows()) + { + var extensions = string.IsNullOrEmpty(Path.GetExtension(fileName)) + ? GetWindowsExecutableExtensions() + : new[] { string.Empty }; + + foreach (var extension in extensions) + { + var candidate = Path.Combine(directory, fileName + extension); + if (File.Exists(candidate)) + return candidate; + } + } + else + { + var candidate = Path.Combine(directory, fileName); + if (File.Exists(candidate)) + return candidate; + } + } + + return fileName; + } + + private static string[] GetPathDirectories() + { + var path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + return path.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + } + + private static string[] GetWindowsExecutableExtensions() + { + var pathExt = Environment.GetEnvironmentVariable("PATHEXT"); + if (string.IsNullOrWhiteSpace(pathExt)) + pathExt = ".COM;.EXE;.BAT;.CMD"; + + return pathExt.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + } + + private static bool IsWindows() + { + return Path.DirectorySeparatorChar == '\\'; + } + + private static void KillProcessTree(Process process, TaskLoggingHelper log) + { + try + { + if (process.HasExited) + return; + +#if NET6_0_OR_GREATER + process.Kill(entireProcessTree: true); +#else + if (IsWindows()) + KillWindowsProcessTree(process.Id, log); + else + KillUnixProcessTree(process, log); +#endif + } + catch (Exception ex) + { + log.LogWarning($"Failed to kill timed-out command process tree for PID {SafeGetProcessId(process)}: {ex.Message}"); + } + } + +#if !NET6_0_OR_GREATER + private static void KillWindowsProcessTree(int processId, TaskLoggingHelper log) + { + using (var taskKill = new Process()) + { + taskKill.StartInfo = new ProcessStartInfo + { + FileName = "taskkill", + Arguments = $"/PID {processId} /T /F", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + taskKill.Start(); + if (!taskKill.WaitForExit(10000)) + { + taskKill.Kill(); + log.LogWarning($"taskkill timed out while killing PID {processId}."); + } + } + } + + private static void KillUnixProcessTree(Process process, TaskLoggingHelper log) + { + TryRunKillCommand($"-TERM -{process.Id}", log); + Thread.Sleep(1000); + + if (!process.HasExited) + process.Kill(); + } + + private static void TryRunKillCommand(string arguments, TaskLoggingHelper log) + { + try + { + using (var kill = new Process()) + { + kill.StartInfo = new ProcessStartInfo + { + FileName = "kill", + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + kill.Start(); + kill.WaitForExit(5000); + } + } + catch (Exception ex) + { + log.LogMessage(MessageImportance.Low, $"Could not signal Unix process group with kill {arguments}: {ex.Message}"); + } + } +#endif + + private static int SafeGetProcessId(Process process) + { + try + { + return process.Id; + } + catch + { + return -1; + } + } +} diff --git a/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets b/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets index 8f2a2ae..28b7b64 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets +++ b/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets @@ -40,6 +40,7 @@ + From 9ef507e20a1b08e7662ab8318c36507676b62358 Mon Sep 17 00:00:00 2001 From: Tomas Prokop Date: Fri, 5 Jun 2026 13:54:00 +0200 Subject: [PATCH 6/7] =?UTF-8?q?fix(GenPage):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20firstPrompt,=20target=20naming,=20npm=20install=20p?= =?UTF-8?q?erf,=20NODE=5FENV,=20recursive=20discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop firstPrompt.json: remove from GenPageDiscoverPages, GenPageEnsureDeclarations, GenPageProjectNativeTree, GenPage.targets, Solution.GenPages.targets, README - Rename Solution.GenPages targets to Probe/Build/CopyToMetadata convention: GenPageProbeProjectReferences→ProbeGenPages, GenPageBuildProjectReferences→BuildGenPages, GenPageProjectToMetadata→CopyGenPagesToMetadata; update all internal DependsOnTargets refs - Fix unconditional npm install in CodeApp/ScriptLibrary/PCF: split into lock/no-lock targets with Inputs/Outputs up-to-date checks (npm ci with fallback, warning on failure), mirroring GenPage pattern that was already correct - Propagate NODE_ENV from MSBuild Configuration to npm run build (only): Release→production, Debug→development; overridable via $(NodeEnvironment); NOT set on npm install/ci to preserve devDependencies - Recursive GenPage page discovery: switch TopDirectoryOnly→AllDirectories, exclude node_modules/dist/build/bin/obj; clearer duplicate error shows relative paths - RunNodeTool: add EnvironmentVariables string[] property plumbed into ProcessStartInfo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...XIS.DevKit.Build.Dataverse.CodeApp.targets | 32 ++++++++++++++++--- src/Dataverse/GenPage/README.md | 5 ++- ...XIS.DevKit.Build.Dataverse.GenPage.targets | 5 +-- .../TALXIS.DevKit.Build.Dataverse.Pcf.targets | 19 +++++++++-- ...vKit.Build.Dataverse.ScriptLibrary.targets | 31 ++++++++++++++++-- ....Build.Dataverse.Solution.GenPages.targets | 13 ++++---- .../Tasks/Tasks/GenPageDiscoverPages.cs | 27 ++++++++++++---- .../Tasks/Tasks/GenPageEnsureDeclarations.cs | 4 +-- .../Tasks/Tasks/GenPageProjectNativeTree.cs | 3 -- src/Dataverse/Tasks/Tasks/RunNodeTool.cs | 22 +++++++++++-- 10 files changed, 125 insertions(+), 36 deletions(-) diff --git a/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets b/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets index 3c54a86..5d00e04 100644 --- a/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets +++ b/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets @@ -4,6 +4,8 @@ true false + production + development - + + + + + + + + + + + + + + - - - + .config.json`, otherwise shared `genpage.config.json` -- `.firstPrompt.json`, otherwise shared `firstPrompt.json` -Build output is normalized to `$(TargetDir).js` for each root page. +Build output is normalized to `$(TargetDir).js` for each page. ## Solution integration diff --git a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets index 9db222b..36e8227 100644 --- a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets +++ b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets @@ -4,6 +4,8 @@ true false + production + development $(MSBuildProjectDirectory)/RuntimeTypes.ts react;react-dom;react-dom/client;react/jsx-runtime;@fluentui/react-components;@fluentui/react-icons @@ -79,7 +81,7 @@ DependsOnTargets="GenPageDiscoverEntries;GenPageInstallPackagesWithLock;GenPageInstallPackagesWithoutLock;GenPageGenerateRuntimeTypes" Condition="'$(RunNodeBuild)'=='true'"> - + $(TargetDir)%(_DiscoveredGenPage.PageName).js %(_DiscoveredGenPage.EntryFile) %(_DiscoveredGenPage.ConfigJsonPath) - %(_DiscoveredGenPage.FirstPromptJsonPath) $(MSBuildProjectFullPath) diff --git a/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets b/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets index 455eb04..5a08a04 100644 --- a/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets +++ b/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets @@ -19,12 +19,27 @@ - + + + + + + + + + - + diff --git a/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets b/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets index b1694c7..b70007d 100644 --- a/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets +++ b/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets @@ -6,15 +6,40 @@ true false + production + development - + + + + + + + + + + + + + + - - + - @@ -38,9 +38,9 @@ Condition="'@(_GenPageProjects)'!=''" /> - @@ -61,7 +61,6 @@ %(_GenPageOutput.CompiledJsPath) %(_GenPageOutput.EntrySourcePath) %(_GenPageOutput.ConfigJsonPath) - %(_GenPageOutput.FirstPromptJsonPath) %(_GenPageOutput.OwningProjectPath) @@ -73,7 +72,7 @@ @@ -92,7 +91,7 @@ - (StringComparer.OrdinalIgnoreCase) + { + "node_modules", "dist", "build", "bin", "obj", ".git" + }; + + var files = Directory.EnumerateFiles(root, "*.tsx", SearchOption.AllDirectories) + .Where(f => + { + var relative = MakeRelative(root, f); + var segments = relative.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return !segments.Take(segments.Length - 1).Any(s => excludedDirs.Contains(s)); + }) .OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase) .ToArray(); var duplicates = files .GroupBy(f => Path.GetFileNameWithoutExtension(f), StringComparer.OrdinalIgnoreCase) .Where(g => g.Count() > 1) - .Select(g => g.Key) .ToArray(); if (duplicates.Length > 0) { - Log.LogError($"Duplicate GenPage page name(s): {string.Join(", ", duplicates)}"); + foreach (var dup in duplicates) + Log.LogError($"Duplicate GenPage page name '{dup.Key}': {string.Join(", ", dup.Select(f => MakeRelative(root, f)))}"); return false; } @@ -49,19 +60,16 @@ public override bool Execute() var siblingConfig = Path.Combine(root, pageName + ".config.json"); var sharedConfig = Path.Combine(root, "genpage.config.json"); - var siblingPrompt = Path.Combine(root, pageName + ".firstPrompt.json"); - var sharedPrompt = Path.Combine(root, "firstPrompt.json"); var item = new TaskItem(file); item.SetMetadata("PageName", pageName); item.SetMetadata("EntryFile", file); item.SetMetadata("ConfigJsonPath", File.Exists(siblingConfig) ? siblingConfig : (File.Exists(sharedConfig) ? sharedConfig : "")); - item.SetMetadata("FirstPromptJsonPath", File.Exists(siblingPrompt) ? siblingPrompt : (File.Exists(sharedPrompt) ? sharedPrompt : "")); items.Add(item); } if (items.Count == 0) - Log.LogWarning($"No GenPage root *.tsx files found in {root}."); + Log.LogWarning($"No *.tsx files found in {root} (excluding node_modules/dist/build/bin/obj)."); GenPages = items.ToArray(); return !Log.HasLoggedErrors; @@ -72,4 +80,9 @@ public override bool Execute() return false; } } + + private static string MakeRelative(string root, string path) + { + return path.Substring(root.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } } diff --git a/src/Dataverse/Tasks/Tasks/GenPageEnsureDeclarations.cs b/src/Dataverse/Tasks/Tasks/GenPageEnsureDeclarations.cs index f9c8ead..dd915e3 100644 --- a/src/Dataverse/Tasks/Tasks/GenPageEnsureDeclarations.cs +++ b/src/Dataverse/Tasks/Tasks/GenPageEnsureDeclarations.cs @@ -13,7 +13,6 @@ public sealed class GenPageEnsureDeclarations : Task private static readonly GenPageFileDefinition CompiledFile = new("Compiled", "src/pages/page.compiled", "page.compiled", "application/octet-stream", "200000001"); private static readonly GenPageFileDefinition SourceFile = new("Source", "src/pages/page.tsx", "page.tsx", "application/octet-stream", "200000000"); private static readonly GenPageFileDefinition ConfigFile = new("Config", "config.json", "config.json", "application/json", "200000000"); - private static readonly GenPageFileDefinition FirstPromptFile = new("FirstPrompt", "firstPrompt.json", "firstPrompt.json", "application/json", "200000000"); [Required] public string SolutionRoot { get; set; } = ""; @@ -225,7 +224,6 @@ private IEnumerable GetDesiredFiles(ITaskItem page) yield return CompiledFile; yield return SourceFile; yield return ConfigFile; - yield return FirstPromptFile; } private void ValidateSitemapReferences(HashSet knownPageNames) @@ -285,7 +283,7 @@ private void RemoveGenPageRootComponents(string solutionRoot) private static GenPageFileDefinition? GetDefinition(string logicalPath) { - return new[] { CompiledFile, SourceFile, ConfigFile, FirstPromptFile } + return new[] { CompiledFile, SourceFile, ConfigFile } .FirstOrDefault(f => string.Equals(f.LogicalPath, logicalPath, StringComparison.OrdinalIgnoreCase) || string.Equals(f.BaseFileName, logicalPath, StringComparison.OrdinalIgnoreCase)); } diff --git a/src/Dataverse/Tasks/Tasks/GenPageProjectNativeTree.cs b/src/Dataverse/Tasks/Tasks/GenPageProjectNativeTree.cs index a22a6b6..c415ac2 100644 --- a/src/Dataverse/Tasks/Tasks/GenPageProjectNativeTree.cs +++ b/src/Dataverse/Tasks/Tasks/GenPageProjectNativeTree.cs @@ -35,7 +35,6 @@ private void ProjectPage(string metadataRoot, ITaskItem page) var compiledJs = page.GetMetadata("CompiledJsPath"); var entrySource = page.GetMetadata("EntrySourcePath"); var config = page.GetMetadata("ConfigJsonPath"); - var firstPrompt = page.GetMetadata("FirstPromptJsonPath"); RequireFile(projectXml, $"GenPage project XML for {pageName}"); RequireFile(page.GetMetadata("CompiledFileXmlPath"), $"compiled GenPage file XML for {pageName}"); @@ -43,7 +42,6 @@ private void ProjectPage(string metadataRoot, ITaskItem page) RequireFile(compiledJs, $"compiled GenPage bundle for {pageName}"); RequireFile(entrySource, $"GenPage source entry for {pageName}"); if (!string.IsNullOrWhiteSpace(config)) RequireFile(config, $"GenPage config for {pageName}"); - if (!string.IsNullOrWhiteSpace(firstPrompt)) RequireFile(firstPrompt, $"GenPage firstPrompt for {pageName}"); if (Log.HasLoggedErrors) return; var pageDir = Path.Combine(metadataRoot, "uxagentprojects", pageGuid); @@ -56,7 +54,6 @@ private void ProjectPage(string metadataRoot, ITaskItem page) ProjectFile(pageDir, page, "Compiled", compiledJs, "page.compiled", pageName, ""); ProjectFile(pageDir, page, "Source", entrySource, "page.tsx", pageName, ""); ProjectFile(pageDir, page, "Config", config, "config.json", pageName, "{\"dataSources\":[],\"model\":\"\"}"); - ProjectFile(pageDir, page, "FirstPrompt", firstPrompt, "firstPrompt.json", pageName, "{\"userMessage\":\"\",\"agentMessage\":\"\"}"); Log.LogMessage(MessageImportance.High, $"Projected GenPage '{pageName}' native tree to {pageDir}"); } diff --git a/src/Dataverse/Tasks/Tasks/RunNodeTool.cs b/src/Dataverse/Tasks/Tasks/RunNodeTool.cs index a5a09d8..590851b 100644 --- a/src/Dataverse/Tasks/Tasks/RunNodeTool.cs +++ b/src/Dataverse/Tasks/Tasks/RunNodeTool.cs @@ -23,6 +23,11 @@ public sealed class RunNodeTool : Task public string StandardErrorImportance { get; set; } = "High"; + /// + /// Additional environment variables to set for the process, in KEY=VALUE format. + /// + public string[] EnvironmentVariables { get; set; } = Array.Empty(); + [Output] public int ExitCode { get; set; } @@ -39,7 +44,8 @@ public override bool Execute() TimeoutSeconds, IgnoreExitCode, NodeProcessRunner.ParseImportance(StandardOutputImportance, MessageImportance.Normal), - NodeProcessRunner.ParseImportance(StandardErrorImportance, MessageImportance.High)); + NodeProcessRunner.ParseImportance(StandardErrorImportance, MessageImportance.High), + EnvironmentVariables); ExitCode = result.ExitCode; TimedOut = result.TimedOut; @@ -67,7 +73,8 @@ public static NodeProcessResult Run( int timeoutSeconds, bool ignoreExitCode, MessageImportance standardOutputImportance, - MessageImportance standardErrorImportance) + MessageImportance standardErrorImportance, + string[] environmentVariables = null) { var result = new NodeProcessResult(); Process process = null; @@ -114,6 +121,17 @@ public static NodeProcessResult Run( EnableRaisingEvents = false }; + foreach (var envVar in environmentVariables ?? Array.Empty()) + { + var eqIdx = envVar.IndexOf('='); + if (eqIdx <= 0) + continue; + + var key = envVar.Substring(0, eqIdx); + var value = envVar.Substring(eqIdx + 1); + process.StartInfo.EnvironmentVariables[key] = value; + } + process.OutputDataReceived += (_, e) => { if (!string.IsNullOrEmpty(e.Data)) From b5e30f1dd0760c2303c26ac889aaf1c2f62c63b6 Mon Sep 17 00:00:00 2001 From: Tomas Prokop Date: Fri, 5 Jun 2026 16:42:01 +0200 Subject: [PATCH 7/7] refactor(Tasks): extract shared NodeBuild.targets; unify node/npm infra across all project types - Add Tasks/msbuild/tasks/Targets/NodeBuild.targets (auto-imported by all project types via the Tasks package wildcard import): - CheckNodePrereqs, NodeInstallPackagesWithLock, NodeInstallPackagesWithoutLock - Unified properties: NodeProjectDirectory, NodeProjectName, RunNodeBuild, NodeEnvironment, NodeBuildScript, NodeBuildOutputDir - GenPage.targets: remove duplicated CheckPrereqs/Install targets; delegate to shared; pass BUILD_OUTPUT_DIR to npm run build; output copy order: NodeBuildOutputDir > dist > build - CodeApp.targets: same removal/delegation; pass BUILD_OUTPUT_DIR; CopyCodeAppDist and CopyCodeAppDistPublish prefer NodeBuildOutputDir then dist fallback - ScriptLibrary.targets: set NodeProjectDirectory=TypeScriptDir; remove duplicated targets; keep TypeScriptDir existence check inline; pass BUILD_OUTPUT_DIR - Pcf.targets: replace NpmInstall/NpmInstallWithoutLock with shared targets in TalxisBeforeBuild - Update CodeApp/ScriptLibrary READMEs for shared target/output conventions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Dataverse/CodeApp/README.md | 10 +- ...XIS.DevKit.Build.Dataverse.CodeApp.targets | 72 +++++--------- ...XIS.DevKit.Build.Dataverse.GenPage.targets | 64 +++---------- .../TALXIS.DevKit.Build.Dataverse.Pcf.targets | 21 +---- src/Dataverse/ScriptLibrary/README.md | 6 +- ...vKit.Build.Dataverse.ScriptLibrary.targets | 50 +--------- ....Build.Dataverse.Solution.CodeApps.targets | 2 +- .../msbuild/tasks/Targets/NodeBuild.targets | 93 +++++++++++++++++++ 8 files changed, 144 insertions(+), 174 deletions(-) create mode 100644 src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild.targets diff --git a/src/Dataverse/CodeApp/README.md b/src/Dataverse/CodeApp/README.md index bdd1f1d..08894e9 100644 --- a/src/Dataverse/CodeApp/README.md +++ b/src/Dataverse/CodeApp/README.md @@ -30,9 +30,9 @@ Or use the SDK approach: ### Build-time targets -1. **CheckCodeAppPrereqs** -- validates that `package.json` exists and that `node` / `npm` are available in PATH. Runs only when `RunNodeBuild` is `true` (auto-detected from the presence of `package.json`). -2. **BuildCodeApp** (runs before `Build`, depends on `CheckCodeAppPrereqs`) -- executes `npm install` followed by `npm run build` in the project root directory. -3. **CopyCodeAppDist** (runs after `Build`) -- copies the `dist/` folder to `$(OutputPath)$(AppName)\`. Fails the build if `dist/` is missing or if `AppName` is not set. +1. **CheckNodePrereqs** / **NodeInstallPackagesWithLock** / **NodeInstallPackagesWithoutLock** -- shared Node.js targets from `TALXIS.DevKit.Build.Dataverse.Tasks` that validate `package.json`, check `node` / `npm`, and install packages in the project root when `RunNodeBuild` is enabled. +2. **BuildCodeApp** (runs before `Build`, depends on the shared Node.js targets) -- executes `npm run $(NodeBuildScript)` in the project root directory and passes `NODE_ENV` plus `BUILD_OUTPUT_DIR` to the npm script. +3. **CopyCodeAppDist** (runs after `Build`) -- copies the npm build output to `$(OutputPath)$(AppName)\`, preferring `$(NodeBuildOutputDir)` and falling back to `dist/`. Fails the build if no output folder is found or if `AppName` is not set. 4. **CopyCodeAppDistPublish** (runs after `Publish`) -- same as above, but copies to `$(PublishDir)` instead. ### Integration targets @@ -40,7 +40,7 @@ Or use the SDK approach: These targets are called by `TALXIS.DevKit.Build.Dataverse.Solution` when it discovers this project via `ProjectReference`: - **GetProjectType** -- returns `CodeApp` so the Solution build knows how to handle this reference. -- **GetCodeAppOutputs** (depends on `Build`) -- returns the path to the compiled `dist/` folder along with `AppName` and `ConfigPath` (location of `power.config.json`) metadata. The Solution project uses this to call `GenerateCodeAppMetaXml` and produce the `.meta.xml` file for PAC packaging. +- **GetCodeAppOutputs** (depends on `Build`) -- returns the path to the compiled npm output folder, preferring `$(NodeBuildOutputDir)` and falling back to `dist/`, along with `AppName` and `ConfigPath` (location of `power.config.json`) metadata. The Solution project uses this to call `GenerateCodeAppMetaXml` and produce the `.meta.xml` file for PAC packaging. ### What happens in the Solution project @@ -49,7 +49,7 @@ When a Solution project has a `ProjectReference` to a CodeApp project, the follo 1. **ProbeCodeApps** discovers the CodeApp reference by calling `GetProjectType`. 2. **BuildCodeApps** calls `GetCodeAppOutputs`, which triggers the full CodeApp build (npm install + build). 3. **PrepareCodeAppsSources** generates `.meta.xml` via `GenerateCodeAppMetaXml`, adds a `RootComponent` entry (Type 300) to `Solution.xml`, and ensures the `CanvasApps` node exists in `Customizations.xml`. -4. **CopyCodeAppsToMetadata** copies the CodeApp dist output into the solution metadata `CanvasApps/` folder before PAC packages the solution. +4. **CopyCodeAppsToMetadata** copies the resolved CodeApp build output into the solution metadata `CanvasApps/` folder before PAC packages the solution. The CodeApp reference is automatically filtered out of the standard `ResolveProjectReferences` pipeline to avoid unnecessary assembly resolution. diff --git a/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets b/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets index 5d00e04..4e06e29 100644 --- a/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets +++ b/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets @@ -1,56 +1,14 @@ - - true - false - production - development - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + + - + @@ -80,7 +41,13 @@ DependsOnTargets="Build" Returns="@(_CodeAppOutputs)"> - <_CodeAppOutputs Include="$(MSBuildProjectDirectory)/dist"> + <_CodeAppOutputs Include="$(NodeBuildOutputDir)" + Condition="Exists('$(NodeBuildOutputDir)')"> + $(AppName) + $(MSBuildProjectDirectory)/power.config.json + + <_CodeAppOutputs Include="$(MSBuildProjectDirectory)/dist" + Condition="'@(_CodeAppOutputs)'=='' and Exists('$(MSBuildProjectDirectory)/dist')"> $(AppName) $(MSBuildProjectDirectory)/power.config.json @@ -93,10 +60,13 @@ - + + - + diff --git a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets index 36e8227..ba5702d 100644 --- a/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets +++ b/src/Dataverse/GenPage/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.GenPage.targets @@ -2,10 +2,6 @@ - true - false - production - development $(MSBuildProjectDirectory)/RuntimeTypes.ts react;react-dom;react-dom/client;react/jsx-runtime;@fluentui/react-components;@fluentui/react-icons @@ -27,46 +23,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -78,10 +34,12 @@ - + + + + + + Condition="'@(_DiscoveredGenPage)'!='' and !Exists('$(NodeBuildOutputDir)%(_DiscoveredGenPage.PageName).js') and Exists('$(MSBuildProjectDirectory)/dist/%(_DiscoveredGenPage.PageName).js')" /> + + Condition="'@(_DiscoveredGenPage)'!='' and !Exists('$(NodeBuildOutputDir)%(_DiscoveredGenPage.PageName).js') and !Exists('$(MSBuildProjectDirectory)/dist/%(_DiscoveredGenPage.PageName).js') and Exists('$(MSBuildProjectDirectory)/build/%(_DiscoveredGenPage.PageName).js')" /> + Text="GenPage build did not produce $(TargetDir)%(_DiscoveredGenPage.PageName).js (or BUILD_OUTPUT_DIR/dist/build equivalent)." /> <_GenPageBundle Include="$(TargetDir)%(_DiscoveredGenPage.PageName).js"> diff --git a/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets b/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets index 5a08a04..9366c62 100644 --- a/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets +++ b/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets @@ -19,27 +19,8 @@ - - - - - - - - - - - - - + diff --git a/src/Dataverse/ScriptLibrary/README.md b/src/Dataverse/ScriptLibrary/README.md index e3fa26d..9b0f0fa 100644 --- a/src/Dataverse/ScriptLibrary/README.md +++ b/src/Dataverse/ScriptLibrary/README.md @@ -33,9 +33,9 @@ The package sets `ProjectType` to `ScriptLibrary` and disables `GenerateAssembly ### Build-time targets -1. **CheckScriptLibraryPrereqs** -- validates that `TypeScriptDir` exists, `package.json` is present, and `node`/`npm` are on `PATH`. -2. **BuildTypeScript** (runs before `Build`) -- executes `npm install` followed by `npm run build` in `TypeScriptDir`. -3. **CopyScriptLibraryMainToOutput** (runs after `Build`) -- copies the main JS file from `TypeScriptDir\build\` to the output directory. +1. **NodeInstallPackagesWithLock** / **NodeInstallPackagesWithoutLock** -- shared Node.js targets from `TALXIS.DevKit.Build.Dataverse.Tasks` that validate `package.json`, check `node` / `npm`, and install packages in `TypeScriptDir`. +2. **BuildTypeScript** (runs before `Build`) -- first validates that `TypeScriptDir` exists, then executes `npm run $(NodeBuildScript)` in `TypeScriptDir` and passes `NODE_ENV` plus `BUILD_OUTPUT_DIR` to the npm script. +3. **CopyScriptLibraryMainToOutput** (runs after `Build`) -- copies the main JS file for `ScriptLibraryName` from `TypeScriptDir` to the output directory, regardless of which subfolder the bundler used. ### Integration targets diff --git a/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets b/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets index b70007d..f156807 100644 --- a/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets +++ b/src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets @@ -3,57 +3,17 @@ $(MSBuildProjectDirectory)/TS - - true - false - production - development + $(TypeScriptDir) - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.CodeApps.targets b/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.CodeApps.targets index d3a9a03..ddf78df 100644 --- a/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.CodeApps.targets +++ b/src/Dataverse/Solution/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Solution.CodeApps.targets @@ -107,7 +107,7 @@ + Text="Copying CodeApp output to $(_MetadataCanvasAppsDir)$(PublisherPrefix)_%(_CodeAppOutputs.AppName)_CodeAppPackages/" /> + + + + + + $(MSBuildProjectDirectory) + $(MSBuildProjectName) + + true + false + + + production + development + + build + + + $(IntermediateOutputPath)node/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +