Using npm workspaces with ReactJS(Typescript) and .NET
Tags: typescript, javascript, react, dotnet
Published: 2022-01-07 16:41:40

This article explains how to leverage the existing .NET SPA template to work with [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces). explanation on what npm workspaces are is not addressed in this article. for any one who is new to npm workspaces its recommended to check [npm official documentation](https://docs.npmjs.com/cli/v7/using-npm/workspaces). npm workspaces is a nice way of organizing code but at the time being in order to use workspaces in .NET some customization are required, which will be explained in the following sections of this article.
#### Content
* [Creating .NET project](#creating-.net-project)
* [Setting up SPA](#Setting-up-spa)
* [Modifying .NET Project](#modifying-.net-project)
* [Configuring Publish Profiles](#Configuring-publish-profiles)

#### Creating .NET project
.NET project with react can be created by running the following command
```
dotnet new react -n SampleApp
```

#### Setting up SPA
Once the `SampleApp` project is created by default it will contain `ClientApp` directory, which is where the SPA(in this case React App) resides. as the default SPA template doesn't fit the required scenario delete everything inside `ClientApp` directory. 

To setup workspaces open terminal inside the `ClientApp` directory first run the following command
```
npm init -y
```
Running this command will generate `package.json` file which will contain the workspace information. for this example I want to create four workspaces named
* @clientapp/table : contains React app that displays information in tabular format 
* @clientapp/card : contains React app that displays information in card
* @clientapp/config : contains shared configurations(eg. tsconfig) 
* @clientapp/core : contains shared components and functionalities

The `ClientApp` will now look like the following
  
![ClientApp folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7yph6pw4izp7kxi282p6.png)
Now `package.json` inside `ClientApp` have to be updated to configure the workspaces as shown bellow
```
{
  "name": "@clientapp/root",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start:table": "npm run start -w @clientapp/table",
    "start:card": "npm run start -w @clientapp/card",
    "build:table": "npm run build -w @clientapp/table",
    "build:card": "npm run build -w @clientapp/card"
  },
  "workspaces": [
    "workspaces/*/**"
  ]
}
```
To create the two applications inside `ClientApp\workspaces\apps` directory run the following commands consecutively
1. @clientapp/table
```
npx create-react-app table --template typescript
```
updated `name` field inside `ClientApp\workspaces\apps\table\package.json` to 
```
"name": "@clientapp/table"
```
2. @clientapp/card
```
npx create-react-app card --template typescript
```
updated `name` field inside `ClientApp\workspaces\apps\card\package.json` to 
```
"name": "@clientapp/card"
```
#### changes for both apps 
By default in both `@clientapp/table` & `@clientapp/card` we will not be able to use the typescript libraries from other workspaces. in order to support typescript I will use `craco` instead of `react-scripts`. the changes in this section must be applied in both `@clientapp/table` & `@clientapp/card`.

Install `craco` as dev dependency
```
 npm install craco --save-dev
```  
Create file name `craco.config.js`
```

const path = require("path");
const { getLoader, loaderByName } = require("craco");

const packages = [];
/**
 * add the typescript workspaces this project is dependent up on
 */
packages.push(path.join(__dirname, "../../libs/core"));

module.exports = {
  webpack: {
    configure: (webpackConfig,  { env, paths }) => {
      /**
       * Overriding the output directory of build to fit with default configuration of .NET wrapper
       */
      paths.appBuild = webpackConfig.output.path = path.resolve('../../../build');
      const { isFound, match } = getLoader(webpackConfig, loaderByName("babel-loader"));
      if (isFound) {
        const include = Array.isArray(match.loader.include)
          ? match.loader.include
          : [match.loader.include];

        match.loader.include = include.concat(packages);
      }
      return webpackConfig;
    },
  },
};

``` 
Update the `scrpts` section inside `package.json` of both `@clientapp/table` & `@clientapp/card` as shown below:
```
{
  ...
  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "craco eject"
  },
  ...
}
```
3. @clientapp/core

From `ClientApp\workspaces\libs` open terminal and run the following command
```
npx create-react-app core --template typescript
```
updated `name` field inside `ClientApp\workspaces\apps\card\package.json` to 
```
"name": "@clientapp/core"
```
Since @clientapp/core is not dependent on another workspace there is no need to configure `craco`.

> From all application delete `node_modules` directory 


To install the `@clientapp/core` workspace into `@clientapp/table` & `@clientapp/card` run the following commands from `ClientApp` directory
```
npm install @clientapp/core -w @clientapp/table  
```
```
npm install @clientapp/core -w @clientapp/card  
```
To install the dependency packages run `npm install` from `ClientApp` directory.

At this point the SPA workspace configuration is completed & can be tested by running either of the following commands
```
npm run start:table
``` 
or 
```
npm run start:card
```

#### Modifying .NET Project

For development update `Configure` method inside `Startup.cs` by replacing 
```
spa.UseReactDevelopmentServer(npmScript: "start");
```
By 
```
spa.UseReactDevelopmentServer(npmScript: "run start:table");
```
To start @clientapp/table. & replace it by
```
spa.UseReactDevelopmentServer(npmScript: "run start:card");
```
To start @clientapp/card

For publish update `SampleApp.csproj` by replacing 
```

  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
      <DistFiles Include="$(SpaRoot)build\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>%(DistFiles.Identity)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
        <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>

```
By 
```
  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <Error Condition="'$(SpaBuildScript)' == ''" Text="Spa build script is not specified." />
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="$(SpaBuildScript)" />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
      <DistFiles Include="$(SpaRoot)build\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>%(DistFiles.Identity)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
        <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>
```

Add Two publish profiles one for @clientapp/card & one for @clientapp/table 
```
CardAppProfile.pubxml
```
```
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <DeleteExistingFiles>False</DeleteExistingFiles>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <PublishProvider>FileSystem</PublishProvider>
    <PublishUrl>bin\Release\net5.0\publish\</PublishUrl>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <SpaBuildScript>npm run build:card</SpaBuildScript>
  </PropertyGroup>
</Project>
```

```
TableAppProfile.pubxml
```
```
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <DeleteExistingFiles>False</DeleteExistingFiles>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <PublishProvider>FileSystem</PublishProvider>
    <PublishUrl>bin\Release\net5.0\publish\</PublishUrl>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <SpaBuildScript>npm run build:table</SpaBuildScript>
  </PropertyGroup>
</Project>
```
After adding these publish profiles, @cilentapp/table can be published by running the following command for 

```
dotnet pubilsh /p:PublishProfile="Properties\PublishProfiles\TableAppProfile.pubxml"
```
And for @cilentapp/card

```
dotnet pubilsh /p:PublishProfile="Properties\PublishProfiles\CardAppProfile.pubxml"
```

That is one way of using npm workspaces with .NET, full source code can be found on [GitHub](https://github.com/bazen-teklehaymanot/dotnet-npm-workspace-sample).


Thanks for reading, Happy coding!