React/vite app with base URL that does not apply to static links in the main HTML file - reactjs

I have a question, around base urls. I have a React/Vite application that will get deployed into a webserver that hosts many other web-properties, and as part of this, all web-properties include shared JS and CSS. These shared JS/CSS are all served off the root do the domain (ex. https://www.my-domain.com/assets/shared.js). These includes are just hard coded into my main HTML file (usually index.html, but in my case root.html)
My React app however is served from https://www.my-domain.com/apps/catalog/root.html.
If i run my app locally with just npm run dev, everything works - but but my app is served off the root. I thought the right way to have this all run off my /apps/catalog/ based URL was to run all my vite commands with --base=/apps/catalog which seems to almost work; the problem is it rewrites thee static shared includes, which break them, since theyre expected to be served off the root.
I think, i need to figure out how to mark specific includes as not being effected by the base URL rewrite? Or, is there a better way to approach this?
My HTML file w/ the static includes of the "shared" assets. I dont want to hardcode the "real" domain in the src URLs since then my local proxy can't trap them and grab them serverside preventing CORS issues.
// /src/root.html
<html>
<head>
<!-- served from: https://www.my-domain.com/assets/shared.js -->
<script src="/assets/shared.js"/>
<!-- served from: https://www.my-domain.com/api/shared.js -->
<script src="/api/shared.js"/>
.. etc ..
</head>
<div id="app"></div>
</html>
My vite.config.js where i start to try to introduce the notion of a base url (and a non-index.html named "main" file)
The local proxy so these static includes resolved without CORS issues, for local dev (it proxies these static assets out to the Shared host, and everything works well locally).
// src/vite.config.js
export default defineConfig({
plugins: [react()],
build: {
assetsDir: 'static-assets',
rollupOptions: {
input: {
app: './root.html',
},
},
},
base: '/apps/catalog/',
server: {
open: '/root.html',
proxy: {
'^/assets/.*|^/api/.*': {
target: 'https://www.my-domain.com',
changeOrigin: true
}
}
}
})
And my package.json where i tried to also set the base URL.
// package.json scripts
"dev": "vite --base=/apps/catalog/",
"build": "vite build --base=/apps/catalog/"

I had the same problem with Laravel 9 + vite v3.2.5.
I fixed it (after hours) with the experimental renderBuiltUrl:
import {defineConfig, loadEnv} from 'vite';
// load env variables to get base url
process.env = Object.assign(process.env, loadEnv('production', process.cwd(), ''));
export default defineConfig({
// ...
experimental: {
renderBuiltUrl(filename) {
// here we set the base url. You might have to change this for react:
return process.env.APP_URL + '/build/' + filename;
}
}
}
Note the warning in the documentation:
This feature is experimental, the API may change in a future minor without following semver. Please always pin Vite's version to a minor when using it.

Related

Dynamically updating index.html file in production build of a create react app

My client app is built with Create React App, which comes which lots of conveniences built-in. However it's not possible to do server side rendering out of the box. As a result, once the build command is run, all of the output is effectively static.
I have a use case where I do NOT want to switch to complete SSR setup, but would like to dynamically add some data to index.html file so that it is immediately available in javascript when client first loads file.
I worked out the following solution:
React app runs as a docker container, using the serve lib to serve static build content
A separate node service runs in a different docker container and has access to the build content from the react app via a shared volume
The node service runs a function every few minutes that reads the contents of index.html file using fs, inserts some additional data into a script tag (e.g. window.myData={someKey: 'someValue'}), and writes the updated string to index.html.
Locally using docker-compose, this works great. However, I'm wondering about possible ramifications of this approach, especially cases where an incoming request for the react app will fail because of some kind of file lock on index.html as it's being read / written by the node service.
I don't think this would be an issue, but I had enough doubt to post this question. The last thing I can afford are failed requests in my production app because of some unforeseen issue.
Any advice, suggestions, anecdotes, etc. are appreciated!
I've successfully added SSR for CRA without ejecting. If that's your only use case, the SSR setup will be super simple as you don't have to deal with webpack or babel configs. If you are interested, follow 3 steps below:
Step 1: Add a basic Express server in a new folder at your root/ project folder (same level as src/)
Step 2: For main app routes, read file build/index.html and edit it as you want before sending.
Step 3: Other than those routes, serve CRA build/ static files as suggested by CRA SSR documentation
TL;DR
// root/server/index.js (your build is at root/build)
const express = require("express");
const fs = require("fs");
const path = require("path");
const app = express();
// step 2
const renderContent = (req, res) => {
fs.readFile(
path.resolve(__dirname, "../build/index.html"), "utf8", (err, htmlData) => {
if (err) {
return res.sendStatus(500);
}
/* Do something with htmlData here */
return res.send(htmlData);
});
}
app.use("^/$", renderContent); // step 2
app.use(express.static(path.resolve(__dirname, "../build"))); // step 3
app.use("*", renderContent); // step 2
// step 1
app.listen(process.env.PORT, () => {
console.log(`Listening on port ${process.env.PORT}`);
});
Then, you just need to run: node server/index.js, and this server will serve your Create React App normally, except for the part that you edit your HTML above.
I don't know if this will help, but as a suggestion. I faced a similar usecase. I solved it very simply using HtmlWebpackPlugin.
Inside the webpack configuration. I write something like:
new htmlWebpackPlugin({
template: path.join(rootpath, '', 'index.html'),
inject: false //<-- Notice here
})
I set the "inject" to false and using this flag it does not append the script's/bundle on index.html and at the same time it allowed us to use ejs templating to loop over the file's we want to append on index.html.
Like on index.html I wrote:
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.files.chunks[chunk].css %>">
<script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
<% } %>
My suggestion would be create a separate chunk/file(js) of that data you want to append dynamically on index.html and using EJS templating, you can achieve the desired result.
How to Create Chunks: READ HERE
Thanks, Hope it Help.

How to serve static generated pages via CDN with Next.js

I'm trying to use Next.js (9.5.2) for both Server Side Rendering and Static Site Generation. SSR works fine for me with assetPrefix. I am able to host all my static assets on CloudFront. However, I'm not sure what the best way is to host the static pages.
I am facing 2 issues.
assetPrefix is not applied to SSG pages. so the link to JS/CSS will be something like this <link rel="preload" href="/_next/static/css/styles.31b6de8d.chunk.css" as="style"/>
Assume we host generated HTML on CDN and we are able to serve assets with assetPrefix, how do I use Next.js Incremental Static Regeneration with fallback: true in getStaticPath. My understanding is that page will generated on the server side if the corresponding HTML is not found.
Thanks everyone for helping.
I have partial answers to my own question.
For issue 1:
The issue is my own fault. assetPrefix worked with SSR but not for SSG because I didn't pass in environmental variables properly. In my situation, we have 2 different CDN URLs for production and staging. So I have something like the following in next.config.js. Because MY_ENV is passed in from PM2, which starts my app, it is guaranteed that MY_ENV will always be available when Next.js needs to access next.config.js.
// next.config.js
const isProd = process.env.MY_ENV === 'production';
const isStaging = process.env.MY_ENV === 'staging';
const isDevelopment = process.env.MY_ENV === 'development';
if (isProd) {
assetPrefix = 'https://mycdn.cloudfront.net/';
} else if (isStaging) {
assetPrefix = 'https://mycdn.cloudfront.net/staging';
}
However, when I run next build for static pages, the build step doesn't use PM2 thus MY_ENV is not available. To make it work, I need to run the build twice with different variables.
"build": "npm-run-all --parallel build:production build:staging",
"build:production": "MY_ENV=production next build",
"build:staging": "MY_ENV=staging next build",
For issue 2:
If I'm able to pre-generate all static pages. I can just put everything on CDN and they will work.
In my situation, ISR is a better fit. The way I get ISR to work is to ask the server to return the HTML for every request instead of hosting on the CDN. Since all other assets are hosted on the CDN, the performance is still pretty good and this solution works out well for my situation.
If you are like me who struggled a bit on this issue, I hope my answer helps you out.

.NET Core 2.X MVC, React, Typescript, Webpack Configuration

I am struggling to revamp my .NET Core application and not sure which way I need to proceed. This is an older website that was originally built using Asp.NET and Razor pages and now I am trying to speed it up and update the UI to conform to new libraries and methods (React, Angular etc.).
Since reprogramming the entire UI is not within the time allowed it seemed that React would be a good fit. So I created a new React application using
create-react-app client-app --scripts-version=react-scripts-ts
under an MVC area folder (each area will have to have its own app) and I can serve and build the client via Power Shell and everything works great.
The problem I have is moving the output directory of the generated scripts to the wwwroot folder. I need to be able to load the scripts from the wwwroot since I can't use SPA Services due to routing, filters and other issues with the existing project.
This is my first attempt using React so I figured it would be easy to change the output of the files but I can't seem to be able to figure out how.
I updated tsconfig.json to change the output directory using
"outDir": "../../../wwwroot/areas",
And this doesn't seem to do a thing, the only way I have been able to get any results is by ejecting the React project using npm run eject and I get a bunch of files including the webpack.config.prod.ts and webpack.config.dev.ts files. When I did this I noticed that the directory is coded in the wepack.config.ts.
After looking online for an answer I noticed that some articles recommend using the webpack-cli instead of npm and specifying a configuration file. But when I do this I get a variety of errors saying the config file is not in the root directory so I tried moving it into the ./src directory then I got another error that the entry module couldn't be found because it couldn't resolve the ./src directory but I can't find where it is referencing src from.
Most of the information I can find on this is either obsolete or plain doesn't work. Even the Typescript.org site is still referencing .NET Core 1.X.
I did find this ReactJS.NET but doesn't look like it is very current and the example they have wouldn't load, gave a ton of errors.
Is there an easy way to configure React for this website or am I forced to fall back on old libraries like AngularJS or KnockOut? Can React be set up to be served like this and compliment the UI allowing me to still use the Razor pages?
I was able to get this setup the way I needed to, including Webpack hot-module replacement.
In order to get control over the configuration that is needed I needed to eject the react-client-app by running:
`npm run eject`
This will create a few directories and files under your app root, these contain the webpack configurations for both production and debugging.
For my project I didn't want the static/ folders, in order to change the output of the files, there are several areas in the webpack.config that controls these for example:
webpack.config.js
output: {
...
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js',
...
},
....
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.svg$/],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'media/[name].[ext]',
},
},
...
For some reason, the default React TS template (create-react-app <name> --typescript) doesn't reference tsconfig.json. One of the Keys to get this going was to install tsconfig-paths-webpack-plugin. This uses the paths defined in the tsconfig.json to resolve alias's and paths, this enables you to include modules from custom paths instead of using relative paths like:
import <name> from 'core/common/<name>';
The next item of business was to add a homepage entry in the package.json:
{
"name": "<AppName>",
"homepage": "/<AreaName>",
"version": "0.1.0",
...
}
This will append the homepage value to the paths and point it to the right area. I was originally thinking that I had to manually build the client instead of using Javascript Services (Spa) but this is not the case. Using Spa for different areas really keeps things nice and neat, there is no reason to output to the wwwroot folder since SpaStaticFiles can be set to any root and path. The next step was to update the Startup.cs to add Javascript Services.
Startup.cs
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
// Adds the path to the React files
configuration.RootPath = "Areas/<AreaName>/ClientApp/build";
});
return services.ConfigureApplicationServices(Configuration);
}
application.UseSpaStaticFiles(new StaticFileOptions
{
// This is the key to getting things working, this is the path of the Area
RequestPath = "/<AreaName>",
});
application.UseSpa(spa =>
{
spa.Options.SourcePath = "Areas/<AreaName>/ClientApp";
if ( env.IsDevelopment() )
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
The React app is now served from the Area. There is no need to copy files to the wwwroot since Javascript Servies takes care of this, just reference any files as they were in root, the homepage value will be prepended to the path.
Finally to load the application I initiated the application through a standard Controller/View and used the compiled index.html to grab the required imports to return.
View
#{
Layout = null;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="/<AreaName>/favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="/<AreaName>/manifest.json" />
<title>React App</title>
<!-- Webpack Styles -->
<link href="/<AreaName>/css/vendors~main.chunk.css" rel="stylesheet">
<link href="/<AreaName>/css/main.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!-- Webpack Scripts -->
<script src="/<AreaName>/js/vendors~main.chunk.js"></script>
<script src="/<AreaName>/js/main.js"></script>
</body>
</html>
In order to get react-router-dom to function properly with MVC and its routes you need to use the HashRouter instead of BrowserRouter.
Note:
If you use npm run build before launching a development session the production built files will be used not the debug compiled files. This will cause some confusion as nothing will update. To get everything to update when a change is made just remove the ./build directory and launch again.
I hope I included everything if not just post a comment I and will add what I missed.

Issue generating Service worker with sw-precache in react

I have created a react app using create-react-app.
Then I decided I need to edit/update the service worker code as per my need.
I am trying to build the app now using sw-precache (as its supposed to help building the custom service worker and automatically refer to the hashed static files).
This is the config I am using in 'sw-precache-config.js' -
module.exports = {
staticFileGlobs: [
'build/static/css/**.css',
'build/static/js/**.js',
'index.html',
'/'
],
swFilePath: './build/serviceWorker.js',
templateFilePath: './service-worker.tmpl',
stripPrefix: 'build/',
handleFetch: false,
runtimeCaching: [{
urlPattern: /this\\.is\\.a\\.regex/,
handler: 'networkFirst'
}]
}
This is the build command in my package.json -
"build": "react-scripts build && sw-precache --config=sw-precache-config.js",
However, I am facing two issues after the build -
Even though I have mentioned in the config file that the service worker name should be 'serviceWorker.js', the build process is generating two service worker files -
serviceWorker.js and service-worker.js
In the generated serviceWorker.js (build by the build script), 'index.html' file is not being mentioned in the precache (so the service worker is not caching index.html as intended)-
var precacheConfig =
[["static/css/main.d35a7300.chunk.css","5cf96316c6919194ba6eb48522a076f0"],["static/js/1.6105a37c.chunk.js","ae3537cefdcf8ee5758c3af13aab4568"],["static/js/main.4857c4da.chunk.js","9d6cc8fb962129f3e8bc459c85d39297"],["static/js/runtime~main.229c360f.js","3b44b5daad3fcbefa8b355dfbc3d9630"]];
Please let me know if I can clarify anything else.
Replace index.html in staticFileGlobs with build/index.html
You are getting two service worker files because the name you are specifying: serviceWorker.js is different from the default name CRA goes with: service-worker.js
Change your swFilePath to service-worker.js and that should fix it.

How to inject API server URL when deploying react frontend?

Disclaimer: I am a React noob so perhaps what I am trying to do is not the React Way
I am writing a React front-end that will be deployed to be served statically by some cloud provider, e.g. S3 or Google Storage or whatever. This front-end interacts with several API servers that live somewhere in the cloud, maybe in the same provider, maybe not. Furthermore, while developing the UI or part of it, those servers' addresses might be local or testing instances.
How do I inject the API server(s) URLs into my react application in a flexible so that I can deploy in dev, staging or prod using different addresses?
SOLUTION: I finally ended up using a combination of solutions proposed:
use .env.production and .env.development files (exact names) to store the variable REACT_APP_API_URI = 'host'
this is automatically picked-up by create-react-app's build scaffolding and available in UI code as process.env.REACT_APP_API_URI
Note this somewhat goes against principles from 12 Factor Apps, e.g. storing env variables in files in version control but it does the job ATM.
You can try this:
// http.js
const getBaseUrl = () => {
let url;
switch(process.env.NODE_ENV) {
case 'production':
url = 'https://stackoverflow.com';
break;
case 'development':
default:
url = 'https://google.com';
}
return url;
}
export default axios.create({
baseURL: getBaseUrl(),
});
Using this package https://github.com/toddbluhm/env-cmd you could create an env file for your environment
for example create .env.staging and .env file with this code
// .env.staging file
API_URL=https://staging.url.com/api/
// .env file
API_URL=https://url.com/api/
How to fetch with API_URL from env variable:
fetch(process.env.API_URL)
Then you can just add some extra scripts in your package.json:
{
"scripts": {
"build:staging": "env-cmd .env.staging yarn build",
"build:prod": "env-cmd .env yarn build"
}
}
You can use .env file if the API's are constant for development or production environment. after build you can't change these parameters.
If you want to change the URL after build, add a js file lets say config.js
Include the conf.js in index.html
Add URL in conf.js like
var URL1 = 'https://www.google.com'
You can access the parameter like :
export const {URL1} = window;
You can do that making use of environment variables on the build step for example.
You can use something like .env that allows you to define environment variables and load them on your webpack file for example (assuming you use webpack). But you can really use it with any bundler.
.env file:
API=http://localhost:3000
On your webpack you could make use of the DefinePlugin
example taken from docs: add your API env
...
require('dotenv').config()
...
new webpack.DefinePlugin({
API_ENDPOINT: process.env.API,
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify('5fa3b9'),
BROWSER_SUPPORTS_HTML5: true,
TWO: '1+1',
'typeof window': JSON.stringify('object')
});
Anyway, this is just one way. I like this way because it makes my project ready for defining API keys and other useful stuff for different environments.
NOTE: You can even define different .env files for local, staging and production and load the respective one in the webpack depending on the build type.

Resources