Cross platform dotnet scripts

Often most of our Project Templates need to include scripts to perform different tasks utilized in each project type, e.g. for generating DTOs, running a dev server, publishing release builds, etc.

To surface these common tasks to the developer we initially used Gulp and Grunt JS Tasks so they would show up in VS .NET's Task Runner Explorer UI however this has become dated over time with both Grunt & Gulp.js seeing their usage decline in favor of more advanced build systems like Webpack and rollup.js and at the other end targeting to a single IDE like VS.NET no longer makes sense in a post .NET 6 world which needs to support for multiple platforms and IDEs.

No formal task solution in dotnet projects

Unfortunately .NET doesn't have a formal way to define common tasks for a project as custom XML MS Build tasks are to clunky and hidden to be useful and littering your project with multiple .bat and .sh scripts for each task is tacky in a modern development workflow.

Using npm scripts

As this use-case is well covered in npm using npm scripts, the natural choice for our Single Page App Project Templates (which require npm) is to use npm scripts in package.json:

Thanks to Node.js's popularity this convention available in all node installations is both ubiquitous & UX friendly as every developer knows where to look to find tasks available for each project, how each script is implemented, sorted in the order in which they're generally run and all executed the same way with npm run, e.g:

$ npm run dtos

As this default convention is so prevalent it has the advantage that modern IDEs like Rider includes UI support where each task can be directly in the UI.

What about .NET Apps?

Without a better alternative available to .NET projects we've also resorted to using npm scripts for our project templates that don't use npm like our vuedesktop.com project template which only uses package.json to maintain scripts of its built-in functionality:

Where they can be run from Rider's IDE or using npm's run.

Using dotnet tools to run package.json scripts

Whilst we can assume node to be a ubiquitous dependency installed on most Developer workstations, it may not be available in all environments like CI build agents, Docker containers, new VM workspaces, etc.

So if we're going to standardize on package.json scripts for encapsulating a project template's functionality we thought it also prudent to offer a .NET only solution to support environments where an npm dependency is not desirable, so we've added support for executing npm package.json scripts in our x and app dotnet tools using x script, e.g:

$ x scripts dtos

That's also available from the more wrist-friendly alias x s, e.g:

$ x s dtos
$ app s dtos

Which can be used interchangeably with npm run to execute command scripts on Windows and bash scripts on macOS and Linux or WSL.

Cross platform scripts

Whilst we now have a pure .NET alternative for running package.json scripts should we need it, we still have the issue of maintaining scripts to support multiple platforms. Most of the time this isn't an issue when calling cross-platform tools like x or tsc which supports the same command syntax on all platforms, but it starts to become an issue if also needing to perform some file operations.

An example of this is the script to generate JS DTOs in our pure JS dep-free Project templates which uses TypeScript to transpile the DTOs then needs to move the generated dtos.js to wwwroot. If only needing to support Windows the script would simply be:

"scripts": {
    "dtos": "x ts && tsc -m umd dtos.ts && move dtos.js wwwroot/dtos.js"
}

But we'd need to have a different script that uses mv to support macOS & Linux, we could maintain a separate script per platform, e.g:

"scripts": {
    "dtos:win":  "x ts && tsc -m umd dtos.ts && move dtos.js wwwroot/dtos.js",
    "dtos:unix": "x ts && tsc -m umd dtos.ts && mv dtos.js wwwroot/dtos.js"
}

But then anything calling it would also need to be platform specific including docs needing to having to differentiate between which platform-specific scripts to run.

One solution is to evaluate a node expression that performs the required file operations, e.g:

"scripts": {
    "dtos": "x ts && tsc -m umd dtos.ts && node -e \"require('fs').renameSync('dtos.js','wwwroot/dtos.js')\""
}

In a similar vein we can also evaluate a #Script Expression to perform the cross-platform operations, e.g:

"scripts": {
    "dtos": "x ts && tsc -m umd dtos.ts && x -e \"mv('dtos.js','wwwroot/dtos.js')\""
}

Which our latest templates have adopted, that can be run with either npm run, x scripts or its x s alias:

$ npm run dtos
$ x scripts dtos
$ x s dtos

Shell Script Methods

#Script lets you evaluate 1000+ .NET #Script Methods using JavaScript syntax including a number of common Windows and Bash shell commands:

#Script Windows Unix
mv(from,to) MOVE /Y from to mv -f from to
cp(from,to) COPY /Y from to cp -f from to
xcopy(from,to) XCOPY /E /H from to cp -R from to
rm(from,to) DEL /Y from to rm -f from to
rmdir(target) RMDIR /Q /S target rm -rf target
mkdir(target) MKDIR target mkdir -p target
cat(target) type target cat target
touch(target) CALL >> target touch target

Using Unix / Path separators are replaced to use \ in Windows commands.

File and Directory APIs

Alternatively you can also call .NET's File and Directory static methods, e.g:

"scripts": {
    "dtos": "x ts && tsc -m umd dtos.ts && x -e \"File.Move('dtos.js','wwwroot/dtos.js')\""
}
#Script
File.Copy(from,to)
File.Create(path)
File.Decrypt(path)
File.Delete(path)
File.Encrypt(path)
File.Exists(path)
File.Move(from,to)
File.Replace(from,to,backup)
File.ReadAllBytes(path)
File.ReadAllLines(path)
File.ReadAllText(path)
File.WriteAllBytes(path,bytes)
File.WriteAllLines(path,lines)
File.WriteAllText(path,text)
File.AppendAllLines(path,lines)
File.AppendAllText(path,text)
Directory.CreateDirectory(path)
Directory.Delete(path)
Directory.Exists(path)
Directory.GetDirectories(path)
Directory.GetFiles(path)
Directory.GetLogicalDrives()
Directory.GetFileSystemEntries(path)
Directory.GetParent(path)
Directory.GetCurrentDirectory()
Directory.GetDirectoryRoot(path)
Directory.Move(from,to)
Directory.Copy(from,to)