Trying to use dotnet watch with Svelte
Tags: csharp, svelte, backend, javascript
Published: 2024-03-17 07:18:14

## Goal
Use .NET features (especially [dotnet watch](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-watch)) as a setup for a client-side [Svelte](https://svelte.dev/) application, starting from a simple C# console app.

## Why
I was going through [Frontend Masters'](https://frontendmasters.com/) **Svelte Fundamentals** and I wondered "Would it be possible to substitute `npm run dev` with `dotnet watch`, at least to some extend (i.e. without the full fledged functionality that SvelteKit provides)? So, out of curiosity, I shall give it a try...

## IMPORTANT
This is for **_educational purposes only_**. I am just trying to piece together some things and see what happens.😁

### From Console to Web

Creating a console application using the `dotnet CLI` is more or less straightforward - `dotnet new console` with some flags (project name, directory, etc). I shall name the project (surprise, surprise) `server`.

Removing the `Console.WriteLine(...)` and adding

```csharp
var builder = WebApplicaiton.CreateBuilder(args);
```

throws the expected error (i.e. the static class `WebApplication` is nowhere to be found).

In order for this to be fixed, the `.csproj` file should be edited and a `using` directive provided. Adding

```xml
<ItemGroup>
  <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
```

makes the `.csproj` file look like:

```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>_server</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
</Project>
```

and then in the `Program.cs`:

```csharp
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);
```

So far, so good - `dotnet build` executes successfully🙌

#### Trying something else...

Going through the documentation, it appears that the `WebApplication.CreateBuilder` creates an application with a lot of things inside it (i.e., logging, reading configuration from a number of sources, etc.). Not needing any of those, let's try the `WebApplication.CreateEmptyBuilder`.

Just changing the method triggers a compilation error since `CreateEmptyBuilder` does not accept a _string array_ but requires an _object_ of type `WebApplicationOptions`. So, in order to comply, let's pass an instance of it, build the container (i.e. the _builder_) and finally start the web application.

```csharp
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions());
var app = builder.Build();
app.Run();
```

It builds successfully, but let's try actually running it. And... _**Unhandled exception**_😔

```
No service for type 'Microsoft.AspNetCore.Hosting.Server.IServer' has been registered.
```

I guess  that `CreateEmptyBuilder` does what is says, so let's try something else - changing it with `CreateSlimBuilder`. Building, starting and... no errors!

Granted, it does nothing but it's a start😁

### Returning some content

Let's add an `index.html` file in the build directory (i.e. /bin/Debug/{.NET Version}/index.html).

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mega cool app</title>
</head>
<body>
    <h1>Hello world</h1>
</body>
</html>
```

Then, make the server return it.

```csharp
var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions());
var app = builder.Build();
app.MapGet("/{*rest}", async (ctx) =>
{
    ctx.Response.ContentType = "text/html";
    using var file = File.OpenRead("./test.html");
    await file.CopyToAsync(ctx.Response.Body);
});
app.Run();
```

Build, run and any request made should return the `index.html` file🙌

### It's Svelte time

Next to the `server` folder let's create a `src` folder that is going to hold any Svelte files. 

```html
// App.svelte

<h1>Hello from Svelte</h1>

```

Now, the browser would not render this (or rather it renders it like text). Hence, the Svelte application needs to be build. For this, [Rollup](https://rollupjs.org/introduction/) will be used (if I am not mistaken, Rollup is used somewhere in the Svelte's production pipeline).

But first

```bash
npm init -y
```

Then

```bash
npm i -D rollup
```

That is a good start, but in order to make Rollup play nicely with Svelte, a plugin is required:

```bash
npm i -D rollup-plugin-svelte
```

According to its [documentation](https://github.com/sveltejs/rollup-plugin-svelte), one more plugin is required:

```bash
npm i -D @rollup/plugin-node-resolve
```

Thus, this makes the development dependencies in `package.json` look like

```json
"devDependencies": {
  "@rollup/plugin-node-resolve": "^15.2.3",
  "rollup": "^4.10.0",
  "rollup-plugin-svelte": "^7.1.6"
}
```

The next step is configuring Rollup. Using a configuration file seems like a good option - the result of going through Rollup's and Svelte's pulgin documentation is:

```javascript

//rollup.cofig.js in the root foder

import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/main.js',
  output: [
    {
      file: 'public/build/bundle.js',
      format: 'iife',
      name: 'app',
    },
  ],
  plugins: [
    svelte({
      include: 'src/**/*.svelte',
    }),
    resolve({ browser: true }),
  ]
};

```

Since the entry point is `src/main.js` let's create it.

```javascript
import App from './App.svelte';

const app = new App({
    target: document.body
});

export default app;

```

Editing the `script` section of `package.json` (removing the never useful `test` one 😁):

```json
"scripts": {
  "dev": "rollup -c -w"
}
```
(the `-c` flag tells Rollup to use a config file (i.e. rollup.config.js by default) and the `-w` flag tells Rollup to create a new bundle when the source files change).

The moment of truth:

```bash
npm run dev
```

...and...

```
Cannot use import statement outside a module
```
😱

Fortunately, the error description is quite human readable. Introducing a change to `package.json` solves the problem:

```json
...
"type": "module",
...
```

Running `npm run dev` once again and there is the `bundle.js` file created in the `/public/build` path.🙌

Opening the file and scrolling to line 482(😅) I see

```javascript
h1 = element("h1");
h1.textContent = "Hello from Svelte";
```
🥳

### Displaying Svelte's content

In order to display what has been generated, let's edit the `index.html` file.

```html
<head>
...
    <script src="./public/bundle.js"><script>
...
</head>
```

Firing the server, making a request and... a blank page.

Going into the 'Network' tab of the Development tools of the browser I see that a request is made for `bundle.js` and the response's header `Content-Type` has the value of `text/html`. This makes perfect sense, since this is how the server has been implemented, i.e. for every type of request it returns the contents of `index.html` file. Let's change that.

First, adding the capability to serve static content with `.UseStaticFiles()` (grated, it is possible to check what is being requested, read a file and return it - as with everything else, there are multiple ways to achieve the same result).
```csharp
var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions());
var app = builder.Build();
app.MapGet("/{*rest}", async (ctx) =>
{
    ctx.Response.ContentType = "text/html";
    Console.WriteLine(app.Environment.ContentRootPath);
    using var file = File.OpenRead("./index.html");
    await file.CopyToAsync(ctx.Response.Body);
});
app.UseStaticFiles(); // added capability
app.Run();
```

Trying anew and... again a blank page. Could it be that the request is served before reaching the serving static file capability? Let's try moving it before the `app.MapGet...`. Still nothing... It seems that the problem is having a _catch all_ route (i.e. `"/{*rest}"` matches _everything_) and serving of static content (but more on that later). Let's try changing this to match everything _without_ requests beginning with `public` in the path.

```csharp
app.UseStaticFiles();
app.MapGet("/{path:regex([^public])}/{*rest}", async (ctx) =>
{
...
}
```


Trying it and... in the server logs is a red `fail` message with a great stack trace. Fortunately, in it it is explained what one must do so let's copy-paste it in the code.

```csharp
// Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing; // newly added
using Microsoft.AspNetCore.Routing.Constraints; // newly added 
using Microsoft.Extensions.DependencyInjection; // newly added

var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions());
builder.Services.Configure<RouteOptions>(opt => opt.SetParameterPolicy<RegexInlineRouteConstraint>("regex")); // newly added
var app = builder.Build();
app.UseStaticFiles();
app.MapGet("/{path:regex([^public])}/{*rest}", async (ctx) =>
{
...
}
```

(route constraints can be registered by calling `app.Services.AddRouting()` as well)

Now the server can use RegEx patterns when figuring out what endpoint to execute for a given request.

Making a request and... the response is _404_ for both `"/"` path and the `bundle.js` resource. Trying `"/foo"` as a path and... partial success!!! The `html` is returned (seen in the _Response_  tab under _Network_ in the browser's development tools) and 404 is received for `bundle.js`. Let's add an _empty string_ as the default value for the path so that _404_ is _not_ returned for `"/"`.

```csharp
...
app.MapGet("/{path:regex([^public])=\"\"}/{*rest}", async (ctx) =>
{
...
}
```
Now, time to figure out where `bundle.js` has been hiding all along.

### Serving `bundle.js` as a static file

Currently, when the server is started, a well formed warning is logged:

```
The WebRootPath was not found: {path}. Static files may be unavailable.
```

This seems to be because by default the folder from which static content is served is `wwwroot` - and there is no such folder. Let's do some configuration for `UseStaticFiles`:

```csharp
//Program.cs
...
using Microsoft.Extensions.FileProviders; //newly added
...
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(""), //the absolute path to the build directory, where bundle.js resides
    RequestPath = "/public"
});
```

This configuration serves static content when the request path has `public` from the directory that is passed in the constructor of the `PhysicalFileProvider`.

We run the server (notice that the warning is not there anymore), make a request to "/" and... a I stare at a blank page.

Looking at the network tab in the browser's developer tools I see status code 200 for everything I am interested in, which is nice... But the `Console` in the browser has the not uncommon `Cannot read properties of undefined (reading '$$')` (well, apart from the '$$') painted in red. Not sure what this is all about.

Giving some thought of what I am exactly doing, though, suggests what might the problem be. In the `index.html` file the `bundle.js` (which, from the rollup.config.js is actually an IIFE - Immediately Invoked Function Expression) is in the `<head>`. This basically means that the browser gets the file, parses it and _executes_ it, right there and then.

This in itself means, that the JavaScript code is appending the heading to the `<body>` element. The problem is that there is _no_ ~~spoon~~ body element at that moment because the HTML has not yet been parsed by the browser.

This can be solved either by using the old school technique of moving the script tag at the end of the html file, or adding the `defer` boolean attribute (which will cause the script to be downloaded now, but executed after parsing the HTML) to the script tag. Let's go with the second approach:

```html
//index.html
...
<head>
...
    <script defer src="./public/bundle.js"></script>
...
</head>
...
```

Restarting the server, requesting "/" and... SUCCESS! We see a beautiful heading containing our text!

### Making use of `dotnet watch`

Let's now try to use some hot reloading, provided by `dotnet watch`. Going into the root directory of the server (i.e. where the `.sln` file is located) using the command line and execuing `dotnet watch`. Good, all seems OK for the moment, no errors are in the logs (well, nothing much is done, but still...). Trying to access the "/" URL and... an exception in the server. Not a good start...

Going through the error message, I understand that it was a mistake to place the `index.html` file directly in the `Debug` folder because now the code is trying to access is in a different location - the root folder of the server project. 

Because of this, let's copy the file from the `Debug` folder, place in the root and just for good measure edit it's properties and change the `Copy to Output directory` one to `Copy if newer`.

Restarting the server (Ctrl + C, then `dotnet watch`), requesting "/"... looks way better now - no errors and the heading is there!

### Making Svelte changes

Let's see whether some code changes will be picked up and automatically displayed in the browser (since this is the final goal, after all).

```html
// /scr/App.svelte

<h1>Hello from Svelte!!!</h1>
```

Saving the file... and nothing, still the old text. Doing a hard reload from the browser serves the new content. So, the manual hot reload seems to be working, but not the automatic one.

According to `dotnet watch`'s documentation, the command injects a script tag in the HTML that takes care of refreshing the browser. Looking at the received HTML, there seems to be only the `bundle.js` and nothing else. Manually inserting the script (as suggested by a certain AI tool) did not help.

This seems to be a server infrastructure issue. Could it be that something that `dotnet watch` is requiring is not present because of the `WebApplication.CreateSlimBuilder` call? Let's change that part to `WebApplication.CreateBuilder`

```csharp
//var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions());
var builder = WebApplication.CreateBuilder();

```

Stopping `dotnet watch`, starting it, checking the generated HTML - still nothing...

Giving it a little thought, this is a console application with ASP.NET Core capabilities - could this be the issue - this not being a 'genuine' web application? Let's change the `.csproj` file to make this a 'real' web application.

```xml
<Project Sdk="Microsoft.NET.Sdk.Web"> // .Web is added

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>_server</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <!--<ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>-->
  <ItemGroup>
    <None Update="index.html">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>
```

----------------------------------------------------------------
## IMPORTANT
After making this change and executing `dotnet watch` you default browser will be fired up and request made to the server - if you are like me and have a ton of open tabs with information that you want to go through later (like that is going to happen) be careful not to close everything by accident. While I was testing this thing I changed my default browser just to be on the safe side.
----------------------------------------------------------------


Stopping the server, running `dotnet watch` checking the response - and there it is, we have it

```html
<body>
  <script src="/_framework/aspnetcore-browser-refresh.js"></script>
...
<body>
```

Let's do some changes to `App.svelte`

```html
// App.svelte

<h1>Hello from Svelte</h1>
```

Saving the changes... and they are _not_ reflected in the browser...

Well, according to `dotnet watch`'s documentation, it 'observes' some files by default and the files that we are changing are not on the list. Adding them requires changes to the '.csproj' file

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>_server</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <None Update="index.html">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
  <ItemGroup>
	<Watch Include="{absolute path to the build directory, where bundle.js gets generated upon changing the App.svelte file}\*.js" />
   </ItemGroup>
</Project>
```

Stopping, starting, checking the browser and making a change to `App.svelte` - and the change is still not there... Looking at the terminal, though, `dotnet watch` is asking something interesting - it has detected that the `bundle.js` file has been changed and there are some options present - pressing 'a' tells `dotnet watch` to always restart the application when it detects changes to `bundle.js`. Now the changes get reflected in the browser whenever the `App.svelte` file is changed!

### Almost there

So, we have 'hot reload' working with text but what about styling? Let's increase the awesomeness of our awesome application by underlying the heading!

```html
// App.svelte

<h1>Hello from Svelte!!!</h1>

<style>
h1 {
  text-decoration: underline;
}
</style>
```

Saving the changes and...nothing... Looking at the logs from the server - no errors... But what about `roolup`? Yep, something definitely is wrong with the build process...

Going through svelte-plugin's documentation, there is an option `emitCss` - not sure what exactly it does but I assume it allowing the CSS to be consumed by other tools in the pipeline. Since there are none, let's give this option the value of `false` which makes the 'rollup.config.js' look like

```js
// rollup.config.js

import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/main.js',
  output: [
	  {
	    file: 'public/build/bundle.js',
	    format: 'iife',
	    name: 'app',
	  },
  ],
  plugins: [
    svelte({
      include: 'src/**/*.svelte',
      emitCss: false // new option
    }),
    resolve({browser: true})
  ]
};


```

Saving and... now even the styling works🙌

## Conclusion

Starting from a console application we managed to make use of `dotnet watch` to observe changes in a JavaScript file, generated by Rollup from Svelte files. 

Was this a meaningful exercise - I am not sure... But it definitely was a fun one😁

Cheers!