DEV Community

Bazen
Bazen

Posted on • Edited on

Using npm Workspaces With ReactJS and .NET

This article explains how to leverage the existing .NET SPA template to work with 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. 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

.NET project with react can be created by running the following command

dotnet new react -n SampleApp
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
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/*/**"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

updated name field inside ClientApp\workspaces\apps\table\package.json to

"name": "@clientapp/table"
Enter fullscreen mode Exit fullscreen mode
  1. @clientapp/card
npx create-react-app card --template typescript
Enter fullscreen mode Exit fullscreen mode

updated name field inside ClientApp\workspaces\apps\card\package.json to

"name": "@clientapp/card"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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"
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode
  1. @clientapp/core

From ClientApp\workspaces\libs open terminal and run the following command

npx create-react-app core --template typescript
Enter fullscreen mode Exit fullscreen mode

updated name field inside ClientApp\workspaces\apps\card\package.json to

"name": "@clientapp/core"
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode
npm install @clientapp/core -w @clientapp/card  
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

or

npm run start:card
Enter fullscreen mode Exit fullscreen mode

Modifying .NET Project

For development update Configure method inside Startup.cs by replacing

spa.UseReactDevelopmentServer(npmScript: "start");
Enter fullscreen mode Exit fullscreen mode

By

spa.UseReactDevelopmentServer(npmScript: "run start:table");
Enter fullscreen mode Exit fullscreen mode

To start @clientapp/table. & replace it by

spa.UseReactDevelopmentServer(npmScript: "run start:card");
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Add Two publish profiles one for @clientapp/card & one for @clientapp/table

CardAppProfile.pubxml
Enter fullscreen mode Exit fullscreen mode
<?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>
Enter fullscreen mode Exit fullscreen mode
TableAppProfile.pubxml
Enter fullscreen mode Exit fullscreen mode
<?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>
Enter fullscreen mode Exit fullscreen mode

After adding these publish profiles, @cilentapp/table can be published by running the following command for

dotnet pubilsh /p:PublishProfile="Properties\PublishProfiles\TableAppProfile.pubxml"
Enter fullscreen mode Exit fullscreen mode

And for @cilentapp/card

dotnet pubilsh /p:PublishProfile="Properties\PublishProfiles\CardAppProfile.pubxml"
Enter fullscreen mode Exit fullscreen mode

That is one way of using npm workspaces with .NET, full source code can be found on GitHub.

Thanks for reading, Happy coding!

Top comments (0)