3

I have a .NET Core 3.1 with ReactJs frontend (I am using the switch bionic framework) that I am trying to run in a sub folder (subapp) on IIS. Installing the app on IIS as a website works fine. The application runs without problems. But, when installing the application as a subapp under the IIS default page, will always give a 404 on any resources that react needs. This happens since the baseUrl used by React defaults to the root url.

So far I have not found a way to change this behavior, short of adding the sub-folder name to the baseurl itself. This is not a solution for me, as this one application will be installed in at least 43 sub-folders under IIS on 1 server. I can't build and deploy 43+ apps just because the sub-folder has to be updated!

Does anybody have ANY idea how I can get the react frontend to recognize the current url (which includes the sub-folder) in use and not just default to the root url?

I have tried setting "homepage": "." and "homepage": "./" in package.json, according to the post from 'gaearon' (https://github.com/facebook/create-react-app/issues/527), but it had no affect.

My config file (default.tsx) looks like this (edited on 2021/04/09):

import Project from '../src/globals/interfaces/Project';

const isDevelopment = Object.is(process.env.NODE_ENV, 'development');

const baseUrl = isDevelopment ? 'http://localhost:5105' : '.';

const config: IConfigData<Project, {}> = {
  core: {
    i18n: {
      defaultLocale: 'en',
    },
  },
  project: {
    baseUrl,
  },
  runtime: {},
};

export default config;

My package.json:

{
  "name": "MyApp",
  "version": "3.4.2",
  "description": "My Application",
  "license": "Whatever",
  "author": {
    "name": "Werner"
  },
  "config": {
    "AppIcon": "./src/assets/appIcon/logo.png",
    "title": "My App Title",
    "devServer": {
      "host": "0.0.0.0",
      "port": "5170",
      "https": false,
      "publicPath": "/"
    },
    "publicPath": "",
    "homepage": ".",
    "functionalTestBrowsers": [
      "chrome",
      "firefox",
      "internet explorer",
      "edge"
    ]
  },
  "scripts": {
    "test": "jest --no-cache",
    "test:update": "jest --updateSnapshot",
    "start": "webpack-dev-server --env.build=dev",
    "start:4110": "webpack-dev-server --env.build=dev --env.config=4110",
    "start:4120": "webpack-dev-server --env.build=dev --env.config=4120",
    "start:4130": "webpack-dev-server --env.build=dev --env.config=4130",
    "start:4140": "webpack-dev-server --env.build=dev --env.config=4140",
    "start:4150": "webpack-dev-server --env.build=dev --env.config=4150",
    "start:legacy": "webpack-dev-server --env.build=dev --env.legacy=true --env.REDUX_TOOLS=logger",
    "start:swidget": "webpack-dev-server --port 7070 --env.build=dev --env.swidget=true --env.legacy=true --env.REDUX_TOOLS=logger",
    "start:host": "webpack-dev-server --env.build=dev --env.legacy=true --env.exposed=true --env.REDUX_TOOLS=logger",
    "build": "webpack --env.build=prod --env.verbose=false",
    "build:ci": "webpack --env.build=prod --env.verbose=true --env.release=true",
    "build:legacy": "webpack --env.build=prod --env.legacy=true",
    "build:host": "webpack --env.build=prod --env.legacy=true --env.exposed=true",
    "build:swidget": "webpack --env.build=prod --env.swidget=true --env.legacy=true",
    "build:multi": "webpack --env.build=multi",
    "build:test:functional": "tsc -p test/functional/tsconfig.json",
    "lint": "nyr lint:eslint && nyr lint:stylelint && nyr lint:prettier",
    "lint:staged": "lint-staged",
    "lint:eslint": "eslint --ext ts,tsx src",
    "lint:prettier": "prettier --check \"./**/*\"",
    "lint:stylelint": "stylelint \"src/**/*.(css|scss)\" --syntax scss",
    "lint:fix": "nyr lint:fix:eslint && nyr lint:fix:stylelint && nyr lint:fix:postcss && nyr lint:fix:prettier",
    "lint:fix:prettier": "prettier --write \"./**/*\"",
    "lint:fix:postcss": "postcss --config postcss.config.js --env sort-only --no-map --replace \"src/**/*.(css|scss)\"",
    "lint:fix:stylelint": "stylelint \"src/**/*.(css|scss)\" --syntax scss --fix",
    "lint:fix:eslint": "eslint --fix --ext ts,tsx src",
    "clean": "rimraf dist && rimraf coverage",
    "storybook": "cross-env build=dev start-storybook -p 9001 -c .build/storybook",
    "storybook:static": "cross-env build=prod build-storybook -c .build/storybook -o dist/storybook",
    "test:functional": "run-p build:test:functional test:functional:selenium && run-p --race start wdio",
    "test:functional:headless": "run-p build:test:functional test:functional:selenium && run-p --race start wdio:headless",
    "test:functional:selenium": "selenium-standalone install --silent",
    "wdio": "wdio .build/wdio.conf.js",
    "wdio:headless": "cross-env WDIO_HEADLESS=true wdio .build/wdio.conf.js"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "post-commit": "git update-index --again"
    }
  },
  "repository": {
    "type": "git",
    "url": "Don't worry! Taken out for security!"
  },
  "engines": {
    "node": ">=8.11.3",
    "npm": ">=5.6.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "Firefox ESR",
    "ie >= 11"
  ],
  "dependencies": {
    "@daimler/material-ui-comps": "0.0.4-release-84.0",
    "@daimler/material-ui-theme": "0.0.9",
    "@daimler/typeface-daimler-cs-web": "^1.0.0",
    "@material-ui/core": "^4.11.0",
    "@material-ui/icons": "^4.9.1",
    "@material-ui/lab": "^4.0.0-alpha.53",
    "@switch/core": "2.0.0-beta.2",
    "@types/react-csv": "^1.1.1",
    "@types/react-router-dom": "^5.1.3",
    "@types/redux-logger": "^3.0.7",
    "axios": "^0.19.0",
    "core-js": "~3.2.1",
    "domtokenlist-shim": "~1.2.0",
    "file-saver": "^2.0.2",
    "inversify": "~4.3.0",
    "joi-browser": "~13.4.0",
    "jwt-decode": "^2.2.0",
    "material-table": "^1.69.1",
    "material-ui-dropzone": "^3.2.1",
    "react": "~16.9.0",
    "react-csv": "^2.0.3",
    "react-dom": "~16.9.0",
    "react-hot-loader": "~4.12.11",
    "react-promise-tracker": "^2.0.5",
    "react-redux": "^7.2.0",
    "react-router-dom": "^5.1.2",
    "react-switch": "^5.0.1",
    "react-table": "^7.5.1",
    "react-transition-group-v2": "^4.3.0",
    "redux": "^4.0.5",
    "redux-devtools-extension": "^2.13.8",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.3.0",
    "regenerator-runtime": "~0.13.3",
    "tslib": "~1.10.0",
    "typesafe-actions": "^5.1.0",
    "universal-cookie": "^4.0.3",
    "whatwg-fetch": "~3.0.0"
  },
  "devDependencies": {
    "@babel/core": "~7.5.5",
    "@babel/preset-env": "~7.5.5",
    "@babel/runtime": "~7.5.5",
    "@hot-loader/react-dom": "~16.9.0",
    "@storybook/addon-actions": "~5.1.11",
    "@storybook/addon-info": "~5.1.11",
    "@storybook/addon-knobs": "~5.1.11",
    "@storybook/addon-links": "~5.1.11",
    "@storybook/addons": "~5.1.11",
    "@storybook/cli": "~5.1.11",
    "@storybook/react": "~5.1.11",
    "@types/enzyme": "~3.10.3",
    "@types/jasmine": "~3.4.0",
    "@types/jest": "~24.0.18",
    "@types/jwt-decode": "^2.2.1",
    "@types/node": "~12.0.0",
    "@types/prop-types": "~15.7.1",
    "@types/react": "~16.9.2",
    "@types/react-dom": "~16.9.0",
    "@types/react-redux": "^7.1.7",
    "@types/react-test-renderer": "~16.9.0",
    "@types/storybook__addon-actions": "~3.4.3",
    "@types/storybook__addon-info": "~4.1.2",
    "@types/storybook__addon-knobs": "~5.0.3",
    "@types/storybook__addon-links": "~3.3.5",
    "@types/storybook__react": "~4.0.2",
    "@typescript-eslint/eslint-plugin": "~2.0.0",
    "@typescript-eslint/parser": "~2.0.0",
    "@wdio/cli": "~5.12.4",
    "@wdio/dot-reporter": "~5.12.1",
    "@wdio/jasmine-framework": "~5.12.1",
    "@wdio/local-runner": "~5.12.4",
    "@wdio/selenium-standalone-service": "~5.12.1",
    "@wdio/spec-reporter": "~5.12.1",
    "@wdio/sync": "~5.12.3",
    "babel-loader": "~8.0.6",
    "clean-webpack-plugin": "~3.0.0",
    "copy-webpack-plugin": "~5.0.4",
    "cross-env": "~5.2.0",
    "css-loader": "~3.2.0",
    "css-modules-typescript-loader": "~3.0.0",
    "cssjson": "~2.1.3",
    "duplicate-package-checker-webpack-plugin": "~3.0.0",
    "enzyme": "~3.10.0",
    "enzyme-adapter-react-16": "~1.14.0",
    "enzyme-to-json": "~3.4.0",
    "eslint": "~6.2.1",
    "eslint-config-prettier": "~6.1.0",
    "eslint-plugin-prettier": "~3.1.0",
    "eslint-plugin-react": "~7.14.3",
    "expose-loader": "~0.7.5",
    "fork-ts-checker-webpack-plugin": "~1.5.0",
    "html-webpack-multi-build-plugin": "~1.0.0",
    "html-webpack-plugin": "~3.2.0",
    "husky": "~3.0.4",
    "identity-obj-proxy": "~3.0.0",
    "imagemin-lint-staged": "~0.4.0",
    "jasmine": "~3.4.0",
    "jest": "~24.9.0",
    "lint-staged": "~9.2.3",
    "mini-css-extract-plugin": "~0.8.0",
    "mock-local-storage": "~1.1.8",
    "npm-run-all": "~4.1.5",
    "nyr": "1.1.0",
    "optimize-css-assets-webpack-plugin": "~5.0.3",
    "postcss": "~7.0.17",
    "postcss-cli": "~6.1.3",
    "postcss-extend": "~1.0.5",
    "postcss-import": "~12.0.1",
    "postcss-import-sync": "~7.1.4",
    "postcss-loader": "~3.0.0",
    "postcss-nested": "~4.1.2",
    "postcss-preset-env": "~6.7.0",
    "postcss-remove-prefixes": "~1.2.0",
    "postcss-sorting": "~5.0.1",
    "postcss-unprefix": "~2.1.4",
    "prettier": "~1.18.2",
    "react-ace": "^7.0.4",
    "react-docgen-typescript-loader": "~3.1.1",
    "react-test-renderer": "~16.9.0",
    "rimraf": "~3.0.0",
    "simple-progress-webpack-plugin": "~1.1.2",
    "style-loader": "~1.0.0",
    "stylelint": "~10.1.0",
    "stylelint-config-css-modules": "~1.4.0",
    "stylelint-config-recommended": "~2.2.0",
    "terser-webpack-plugin": "~1.4.1",
    "ts-jest": "~24.0.2",
    "ts-loader": "~6.0.4",
    "typescript": "~3.5.3",
    "url-loader": "~2.1.0",
    "webapp-webpack-plugin": "~2.7.1",
    "webpack": "~4.39.2",
    "webpack-cli": "~3.3.7",
    "webpack-dev-server": "~3.8.0",
    "webpack-merge": "~4.2.1"
  }
}

Any help will be greatly appreciated.

user5763204
  • 99
  • 1
  • 11
  • I think you can add the sub-folder name to the baseurl itself through URL Rewrite Module. – Ding Peng Apr 09 '21 at 06:24
  • Unfortunately that is not an option I would like to use. Some time ago (3-4 months) I had this setup running like a dream. It was just a test as I suspected that we will be moving away from using separate ports for each installation of the application (because of the firewall changes needed for each new installation --> external connections). Now, I can't get it working again as I just can't remember how I did it in the first place. So, I KNOW it should be working without rewriting the URL, since I know I did not do it like that the first time. But how? – user5763204 Apr 09 '21 at 08:00
  • You can also try to add a virtual path: https://learn.microsoft.com/en-us/iis/configuration/system.applicationhost/sites/site/application/virtualdirectory – Ding Peng Apr 13 '21 at 09:04
  • I have seen a lot of posts and comments warning people that virtual paths do not work with the React frontend. I am now 99% sure that I had some hard-coded setup for my React frontend when I got it working the last time. For now I am researching the url rewrite option that you mentioned. Also, I am thinking of writing a script that I can run after installation to change all the paths in the various files to reflect the subfolder. – user5763204 Apr 14 '21 at 21:40
  • I think writing a script is a good choice. – Ding Peng Apr 21 '21 at 08:41
  • Solved the problem with some coding and added an answer. – user5763204 Apr 26 '21 at 19:43

1 Answers1

3

There are 100s of posts on the internet on how to get the ReactJS front-end, as part of an asp.net core back-end, running without problems under an IIS sub-folder. Most of them are the same. Some of them do work properly while others are just plain rubbish.

Should you want to run an application (back-end and front-end) in a sub-folder, then the only way to do it is to hard-code the base url and public path in React.

Since I am using React with typescript and the switch framework I can do it like this: default.tsx:

const baseUrl = isDevelopment ? 'http://localhost:5105' : 'http://localhost/MySubFolderName';

Then in package.json:

"publicPath": "MySubFolderName",

After building for production the app will run without problems in an IIS sub-folder.

But my goal is to have this one app running in 40+ IIS sub-folders, many of them on the same server. And it is definitely NOT an option to build the front-end separately for each installation. Think of the maintenance nightmare!!

I therefor decided to write a script that will change the required files for me. Automatically!

The first option I tried was to set the base tag, <base href="/">, in index.html, as given as a solution by many posts on the internet. My script opened this index.html and replaced the tag to included the sub-folder name: <base href="/MySubFolderName/">.

And 'lo-and-behold', the site runs! However, great was my disappointment when 2 days later I finally figured out that this is not enough. The site runs, but the routing is broken! Irrespective of which router package you use.

So, BEWARE, just setting href in a base tag is not enough.

My final solution: EDITED: My first solution was to hard-code the base url and public path, but with my special name: XYXYX.

After some further thinking I decided to not hard-code anything, but leave the front-end as is. The back-end now updates the required files as needed, making sure that they are correct for either a 'port' installation or for a 'subfolder' installation.

In the back-end, in Programs.cs, create a function that will change the index.html and *app.js files depending on whether the app is installed under IIS to run on a port (ex. http://domain:port) or in a subfolder (ex. http://domain/subfolder).

appsettings.json:

"AppAddress": "http://my.domain.name/MySubFolderName",

Program.cs:

...
            IConfiguration Configuration = new ConfigurationBuilder()
                .SetBasePath(pathToContentRoot)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env}.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            try
            {
                var host = CreateHostBuilder(args).Build();
...
...
                try
                {
                    UpdateFilesForSubfolderOrPortUse(Configuration["AppAddress"], pathToContentRoot);
                }
                catch (Exception e)
                {
                    Log.Fatal(e, $"Host terminated. Could not rewrite files! Error: {e.Message}");
                    return 1;
                }

                host.Run();
...
...
        private static void UpdateFilesForSubfolderOrPortUse(string serverAddress, string pathToContentRoot)
        {
            string subFolder = ExtractSubfolderName(serverAddress);

            // First extract the search-string from the *app.js file
            string searchString = ExtractSearchStringFromJsFile(pathToContentRoot);
            string oldSubFolder = "";

            if (searchString != ".")
            {
                oldSubFolder = searchString.Substring(searchString.IndexOf("/", searchString.IndexOf("://") + 3) + 1);
            }

            Dictionary<string, string> searchAndReplaceDict = new Dictionary<string, string>
            {
                { "HtmlSearchString1", "" },
                { "HtmlReplaceString1", "" },
                { "HtmlSearchString2", "" },
                { "HtmlReplaceString2", "" },
                { "JsSearchString1", "" },
                { "JsReplaceString1", "" },
                { "JsSearchString2", "" },
                { "JsReplaceString2", "" },
                { "JsSearchString3", "" },
                { "JsReplaceString3", "" }
            };

            if ((string.IsNullOrEmpty(subFolder) && searchString == ".") || (!string.IsNullOrEmpty(subFolder) && oldSubFolder == subFolder))
            {
                // No conversion is needed
                return;
            }
            else if ((!string.IsNullOrEmpty(subFolder) && searchString == "."))
            {
                // Convert from Port to SubFolder
                searchAndReplaceDict["HtmlSearchString1"] = "link href=\"";
                searchAndReplaceDict["HtmlReplaceString1"] = $"link href=\"{subFolder}/";
                searchAndReplaceDict["HtmlSearchString2"] = "src=\"";
                searchAndReplaceDict["HtmlReplaceString2"] = $"src=\"{subFolder}/";
                searchAndReplaceDict["JsSearchString1"] = "+\"fonts/";
                searchAndReplaceDict["JsReplaceString1"] = "+\"/fonts/";
                searchAndReplaceDict["JsSearchString2"] = "},i.p=\"";
                searchAndReplaceDict["JsReplaceString2"] = "},i.p=\"" + subFolder;
                searchAndReplaceDict["JsSearchString3"] = "\"http://localhost:5105\":\".";
                searchAndReplaceDict["JsReplaceString3"] = $"\"http://localhost:5105\":\"{(serverAddress[serverAddress.Length - 1] == '/' ? serverAddress.Substring(0, serverAddress.Length - 1) : serverAddress)}";
            }
            else if ((!string.IsNullOrEmpty(subFolder) && searchString != "."))
            {
                // Convert from SubFolder to SubFolder
                searchAndReplaceDict["HtmlSearchString1"] = oldSubFolder;
                searchAndReplaceDict["HtmlReplaceString1"] = subFolder;
                searchAndReplaceDict["JsSearchString1"] = "+\"fonts/";
                searchAndReplaceDict["JsReplaceString1"] = "+\"/fonts/";
                searchAndReplaceDict["JsSearchString2"] = $"\"{oldSubFolder}\"";
                searchAndReplaceDict["JsReplaceString2"] = $"\"{subFolder}\"";
                searchAndReplaceDict["JsSearchString3"] = $"/{oldSubFolder}\"";
                searchAndReplaceDict["JsReplaceString3"] = $"/{subFolder}\"";
            }
            else
            {
                // Convert from SubFolder to Port
                searchAndReplaceDict["HtmlSearchString1"] = $"{oldSubFolder}/";
                searchAndReplaceDict["JsSearchString1"] = "+\"/fonts/";
                searchAndReplaceDict["JsReplaceString1"] = "+\"fonts/";
                searchAndReplaceDict["JsSearchString2"] = $"\"{oldSubFolder}\"";
                searchAndReplaceDict["JsReplaceString2"] = "\"\"";
                searchAndReplaceDict["JsSearchString3"] = searchString;
                searchAndReplaceDict["JsReplaceString3"] = $".";
            }

            DoUpdateFiles(searchAndReplaceDict, pathToContentRoot);
        }

        private static string ExtractSubfolderName(string serverAddress)
        {
            string baseAddr = serverAddress.Substring(serverAddress.IndexOf("://") + 3);
            int idx = baseAddr.IndexOf('/');
            string subaddr = "";

            if (idx > 0)
            {
                subaddr = baseAddr.Substring(idx + 1);

                if (subaddr.Length > 0 && subaddr[subaddr.Length - 1] == '/')
                {
                    subaddr = subaddr.Substring(0, subaddr.Length - 1);
                }
            }

            return subaddr;
        }

        private static string ExtractSearchStringFromJsFile(string pathToContentRoot)
        {
            string rootfolder = $"{pathToContentRoot}\\App";
            var exts = new[] { "app.js" };
            IEnumerable<string> files = Directory
                .EnumerateFiles(@rootfolder, "*.*", SearchOption.TopDirectoryOnly)
                .Where(file => exts.Any(x => file.EndsWith(x, StringComparison.OrdinalIgnoreCase)));

            string jsContents = File.ReadAllText(files.First());
            string searchFor = "project:{baseUrl:Object.is(\"production\",\"development\")?\"http://localhost:5105\":\"";

            string tmpSearchStr = jsContents.Substring(jsContents.IndexOf(searchFor) + searchFor.Length);

            if (tmpSearchStr.StartsWith(".\""))
            {
                // This is a Port installation
                tmpSearchStr = ".";
            }
            else
            {
                // This is a SubFolder installation
                tmpSearchStr = tmpSearchStr.Substring(0, tmpSearchStr.IndexOf("\"}"));
            }

            return tmpSearchStr;
        }

        private static void DoUpdateFiles(Dictionary<string, string> searchAndReplaceDict, string pathToContentRoot)
        {
            string rootfolder = $"{pathToContentRoot}\\App";
            var exts = new[] { ".html", "app.js" };
            IEnumerable<string> files = Directory
                .EnumerateFiles(@rootfolder, "*.*", SearchOption.TopDirectoryOnly)
                .Where(file => exts.Any(x => file.EndsWith(x, StringComparison.OrdinalIgnoreCase)));

            foreach (string file in files)
            {
                string contents = File.ReadAllText(file);

                if (Path.GetExtension(@file) == ".html")
                {
                    contents = contents.Replace(searchAndReplaceDict["HtmlSearchString1"], searchAndReplaceDict["HtmlReplaceString1"]);

                    if (!string.IsNullOrEmpty(searchAndReplaceDict["HtmlSearchString2"]))
                    {
                        contents = contents.Replace(searchAndReplaceDict["HtmlSearchString2"], searchAndReplaceDict["HtmlReplaceString2"]);
                    }
                }
                else if (Path.GetExtension(@file) == ".js")
                {
                    contents = contents.Replace(searchAndReplaceDict["JsSearchString1"], searchAndReplaceDict["JsReplaceString1"]);
                    contents = contents.Replace(searchAndReplaceDict["JsSearchString2"], searchAndReplaceDict["JsReplaceString2"]);
                    contents = contents.Replace(searchAndReplaceDict["JsSearchString3"], searchAndReplaceDict["JsReplaceString3"]);
                }

                // Make file writable
                File.SetAttributes(file, FileAttributes.Normal);

                File.WriteAllText(file, contents);
            }
        }

Now an admin can download the release of the application and install it in an IIS sub-folder. This admin can also just copy all files from one sub-folder/port installation to another. In both instances the admin must update the config file with the correct address.

The application will now, during startup, right before it runs the application, read the 2 files and replace the required strings.

The application now runs with a 'standard' front-end for a 'port' installation and with a 'hard-coded' front-end for a 'sub-folder' installation, but one that is dynamic and updated automatically depending on the 'sub-folder name'! And I can just keep on adding new applications in sub-folders or on ports without having to rebuild every time and without having to manually update the required strings to get it running ...

Yes, some people will say this is a hack, but in SW even a hack is a great solution in the absence of anything else!

user5763204
  • 99
  • 1
  • 11