This is the continuation of a previous post of mine
Automating boilerplate code generation with Node.js and Handlebars
John Kapantzakis ・ Feb 18 '20 ・ 3 min read
In that post we had described the process of creating a set of new files, that contain boilerplate code, using the Handlebars library. Those files are created within the folder structure of a .Net project and they, somehow, have to be included in the project.
We can find them one by one and include them manually, but it would be better if they could automatically get included into the project.
The project file
Each .Net project is built by the Microsoft build engine (MSBuild). In order for the engine to know what files to include into the build process, there is a special XML file called the project file that has a language soecific extension. For C# is .csproj
.
The project file is created automatically by Visual Studio, or it can be created manually if we want to build a project without using Visual Studio. Here is a sample project file for a .Net project written in C#.
Each time we create a new file, Visual Studio includes it to the project, by adding a new entry to the XML document like the following:
<Content Include="file1.ascx"/>
In our case, the new files are created by a Node.js app. They are at the right place but they are not yet part of the build process. We have to add a new entry to the project file for each one of them. We can do it manually, by right clicking on each file and selecting Include In Project, or we can somehow automate this process.
Attempt #1 (xml-js)
As we said at the beginning, this post describes the process of including files generated from a Node.js cli to specific .Net project. We are now going to describe the first attempt of creating an automated process of including those files to the desired project file.
The first idea that came to my mind was to read the .csproj
file in javascript, add the desired entries and recreate it. So, I found xml-js, a powerfull tool that lets you convert xml to js/JSON and vice versa.
The flow was something like this:
After the new files are created, we read the .csproj
file
const convert = require("xml-js");
const xml = fs.readFileSync(path, "utf8");
const js = convert.xml2js(xml);
Then, we manipulate the js
object accordingly and we recreate a new xml structure:
const xml = convert.js2xml(js, {
compact: true,
ignoreComment: true,
spaces: 2
});
Finally, we replace the contents of the .csproj
file with the new xml structure:
fs.writeFileSync(filePath, xml);
After the creation of the new .cproj
file, I could not build the project at all (oops!)
It looks that the project file is broken, and, eventually it seems logical since we are trying to manipulate a specific type of XML file in a way that is not recommended. I have never used xml-js before and I might could have achieved a better result if I tried different conifgurations.
Attempt #2 (Project class
)
After the previous failed attempt, I searched about how to programmatically include files in .net projects and found an answer to this question on stackoverflow:
Background
I'm making a helper application that reformats some code files and creates new code files, which are to be added to my other project, so I could use the new code right away, but I'm having serious trouble adding that new code file into my project automatically. By the…
People suggested to use the class Microsoft.Build.Evaluation.Project
from the .Net framework and its AddItem
method that does exactly what we are looking for.
The problem is that we have to use C# (or VB or whatever .Net compatible language you are working with) in order to use the Project
class. So, we have to write a console application that uses the Project
class and its methods in order to add new entries to the project file.
As soon as we write the console appication, we must find a way to execute it from Node.js, since our new files are created from a Node.js cli.
Console application
Lets now build a simple (C#) console application that will load the project file, add the new items and save the changes. We are not going to cover the whole process in detail, instead we are going to highlight the major points.
If you want to view the whole code you can check the following repo:
kapantzak / csproj-include
Programmatically include items to csproj file
csproj-include
Programmatically include items to csproj file
Inside our Main
method, we check if the desired project is already loaded and if not, we load it.
var p = ProjectCollection
.GlobalProjectCollection
.LoadedProjects
.FirstOrDefault(
x => x.FullPath == projectFullPath
);
if (p == null)
p = new Project(projectFullPath);
As soon as we have a loaded project file, we can iterate over a collection of items that represents the new entries and add them one by one to the loaded file:
items.ForEach(x =>
{
p.AddItemFast(x.itemType, x.unevaluatedInclude, x.metadata);
});
p.Save();
We assume that items
is a collection of objects of type Item
which is defined bellow:
class Item
{
public string itemType { get; set; }
public string unevaluatedInclude { get; set; }
public Dictionary<string, string> metadata { get; set; }
}
Inputs
The program needs some input in order to work properly, like:
- The project file to load
- The entries collection to be inserted
We expect the first argument to be the project file and the second argument to be a JSON string that is going to be deserialised into a list of Item
objects.
Execution
We can now call the executable like this:
> app.exe file.csproj [{"itemType": "Content", "unevaluatedInclude": "myFile.ts"}, {"metadata": null}]
But when I tested, I got the following error.
It seems that there is a missing file or directory! After a lot of searching, I found another stackoverflow question:
I built a Web Forms website using VS2015 where I user Microsoft.Build.Evaluation
so that I can grammatically go through the files in my project
When using VS2017 I get this error:
Microsoft.Build.Exceptions.InvalidProjectFileException: 'The imported project "C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v15.0\WebApplications\Microsoft.WebApplication.targets" was not found. Confirm that the path in the declaration is correct…
I finally found out that $(MSBuildExtensionsPath32)
and $(VisualStudioVersion)
variables of the project file was not correctly set, and that I could apply the desired settings, using an overload of the Project
class constructor that accepts this kind of settings.
So I added the following lines:
var glob = new Dictionary<string, string>();
glob.Add("VisualStudioVersion", "16.0");
glob.Add("MSBuildExtensionsPath32", @"C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild");
p = new Project(projectFullPath, glob, null);
And it worked just fine!
Execute from Node.js
Our final step is to call the console application's executable from inside our Node.js cli. We can achieve that using the execFile()
method of the child_process
module like that:
const exec = require("child_process").execFile;
const child = exec(projectFile, args, (err, data) => {
// ...
});
Workflow
Now lets overview the workflow after our second attempt
As we described earlier, the new files are created by a Node.js cli. As soon as the files have been created, we use execFile
to call the console app we have created in order to add the new items to the desired project file.
Conclusion
During the development process of this Node.js app and the console application, I came accross various problems that I had never faced before. I had to search a lot for the details so, I thought I could write a post about my experience in order to help other people who may face the same or similar problems. I hope you enjoyed reading! 😄
Resources
- Understanding the project file
- https://nodejs.org/api/child_process.html#child_process_child_process_execfile_file_args_options_callback
- https://stackoverflow.com/questions/18544354/how-to-programmatically-include-a-file-in-my-project
- https://stackoverflow.com/questions/47077150/msbuild-15-webapplication-targets-is-missing
- https://stackoverflow.com/questions/48771116/cant-use-microsoft-build-evaluation-in-vs2017
- https://xamarin.github.io/bugzilla-archives/18/18892/bug.html
Top comments (0)