How to build two different builds from with react - node.js

I am implementing an admin panel and don't want to expose the panel's front-end code to clients. I figured the best approach would be to configure npm run build to create two builds - one client build and one admin build. A and then the back-end would control which build gets returned based on authentication.
Possible duplicate with an answer, but doesn't actually explain how you would do that if you are not already familiar with how the build process works inside out. Also, webpack Entry Points do look like something that would be applied here, but as someone who is not very familiar with webpack the limited non beginner-friendly documentation kinda goes over my head.
Some information on my setup:
I have and ReactJS / NodeJS SPA. Front-end and back-end are configured in monorepo principle where both share node_modules, package.json, .env, and so on. For that, I used react-app-rewired to change the path for npm run build and npm run start commands without the need to mess with webpack.
Here is my file structure:
back-end/
...
front-end/
public/
src/
admin/ <- Would prefer the admin panel front-end to be here if possible
...
build/
...
build_admin/ <- This is what I want
...
node_modules/
...
.env
.gitignore
config-overrides.js
package.json
...
"scripts" from package.json:
"scripts": {
"start": "node ./back-end/server.js",
"build": "react-app-rewired build",
"front-end": "set HTTPS=true&&set SSL_CRT_FILE=...&&set SSL_KEY_FILE=...&&react-app-rewired start",
"back-end": "nodemon ./back-end/server.js",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
So if my approach is practical - how do I set up npm run build to make two builds from select* src/ files?
*By select I mean for the client build ignore the admin/ source files and for admin build just build with admin/ files.
Some additional points to get ahead of alternative solutions:
I want to make the admin panel in React as a SPA so Node View Engine is not an option.
I don't want to waste resources by spinning up a whole separate app just to run a basic admin panel and not to mention the headache of dealing with sharing data between two separate applications.
Reason, why I am avoiding showing the admin panel front-end code in the first place, is not that there will be hard-coded sensitive data, but because you can infer quite a lot of information based on UI(input fields, description, button names, graphs, etc).

As of now, not an answer for your original question, how to make two different builds.
A way to make it harder for people to look at the admin page source and easier for you to deploy, is to use the code splitting capabilities of Webpack.
React provides a simple way to split your app in bundles, with the React.lazy method. However I think you need React 16.6.0 and above
For webpack code splitting you'll need version 4 and above
I used react 18.2.0, webpack 5.75.0
I bootstraped the app with npx create-react-app to go fast.
I think react-app-rewired keeps default cra configuration and you should be good. Maybe you will need to update some webpack config, will see
If you are using some kind of routing, you can inspire yourself from this code. I used react-dom-router 6.8 version
The App.js file
import { BrowserRouter, Routes, Route, Link,Navigate } from 'react-router-dom';
import { ProtectedRoute } from './ProtectedRoute';
import * as React from 'react'
// The React.lazy() will normally handle everything for you
const Admin = React.lazy(() => import('./AdminPage'))
function App() {
return (
<BrowserRouter>
<h1>Lazy Router</h1>
<nav>
<Link to="/">Home</Link>
<Link to="/admin">Admin page</Link>
</nav>
<Routes>
<Route path='/' element={<HomePage/>} />
<Route path='/admin' element={
<ProtectedRoute
element={<Admin/>}
fallback={<Navigate to="/" replace/>}
/>
}
/>
</Routes>
</BrowserRouter>
);
}
const HomePage = () => {
return (
<div>
Home page
</div>
)
}
export default App;
So you can pass the Component you want to protect, the AdminPanel Page into the ProtectedRoute component. Its goals is to call your API to check if it has permission to acces the AdminPanel.
I defined two props, an element, any component you want, here the AdminPanel component wrapped with React Lazy.
And a fallback, A Redirect to the HomePage if authorization failed.
The ProtectedRoute.js
import { useEffect, useState } from 'react';
import * as React from 'react'
const ProtectedRoute = (props) => {
/* You could write your own hook that will performed the authorization call to your API.
The hook could returns load and authorized states
But for simplicity
*/
// Auhtorization state which reflects your api auth
const [authorized, setAuthorized] = useState(false)
// Load state, just to wait until isAuth function returns
const [load, setLoad] = useState(false)
// An inelegant function to mimic authorization call to the server
const isAuth = () => {
setTimeout(() => {
setAuthorized(true)
setLoad(true)
}, 2000)
}
// Call your API when the ProtectedRoute component is mounted
useEffect(() => {
isAuth()
},[])
// Loader until the authorization complete and eventually your AdminPanel is loaded
return (
<React.Suspense fallback={<>...loading</>}>
{load ?
authorized ?
props.element
: props.fallback
: <>...loading</>}
</React.Suspense>
)
}
export { ProtectedRoute }
And the AdminPage.js
const AdminPage = () => {
return (
<div>
Admin Page
</div>
)
}
export default AdminPage
You will see that the chunk for the adminPanel will only be downloaded when you request it, i.e. when authorization is successful.
Note, if the bundle fails to load with React.lazy, your react application will completely crash. You have to protect it with a <MyErrorBoundary></MyErrorBoundary> component.
This is explained in this react documentation
Is this method 100% bullet proof, can some people hack a bit react state ? I won't be surprise if it is the case. But it still makes it harder for people to access the source code of your AdminPanel.
In addition, if your react bundles are distributed by a server you control, when the client requests the bundle for the admin page, you can check its rights.
By default, bundles are named with barbarian names, and will be hard to filter, but I think you can give them the name of the module in question, according to the webpack documentation, in the output:{filename:etc..} section. This would be much easier to filter and control, when a client request the adminpanel.bundle.js file.
Will edit my response if you need some precisions and after playing a bit with webpack, but for now I am lazy

You should definitely use webpack entry points. Config is not really difficult and I presume it can be settled with react-app-rewired (see Extended Configuration Options chapter).
To avoid any overburden, the idea is to use a path as entry point name and the [name] joker for output name. In your case, you could try to start with this basic config that may be adapted to correctly resolve real entry paths :
entry: {
'./build/index': './front-end/src/index.js',
'./build_admin/index': './front-end/src/admin/index.js'
},
output: {
path: './',
filename: '[name].js'
}
It will create a bundle at ./build/index.js from first entry point and a bundle at ./build_admin/index.js from second entry point. You can also provide more details to handle common dependencies and so on.
the ./ may not resolve to your project root folder but you can tweak this as needed, with a starting ../ for instance.
If needed, you may have to require path to correctly resolve paths.
See this thread for more examples : How to set multiple file entry and output in project with webpack?

Related

Next deploy via Vercel: found pages without a React Component as default export in

When I deploy my Nextjs app I get a buid err. Vercel doesn`t see nested pages. How can I override it, maybe it is posible in vercel.json?
Building logs
Project structure
I tried this, but it`s breaks everything
module.exports = {
pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js']
}
Could you try moving your components outside pages folder as Next.js has a file-system based router built on the concept of pages.
Hope you should be using export default instead of export inside index.tsx for exporting your component.

Detect whether an npm package can run on browser, node or both

I'm building a NextJs application which uses a set of abstractions to enable code from both react (Component logic etc.) and node (API logic) to utilize same types and classes. The motive here is to make the development process seem seamless across client side and server side.
For example. a call on User.create method of the User class will behave differently based on the runtime environment (ie. browser or node) - on the browser, it will call an api with a POST request and on server it will persist data to a database.
So far this pattern worked just fine, and I like how it all turned out in terms of the code structure. However, for this to work, I have to import modules responsible for working on the server and browser to a single module (which in this case is the User class) this leads to a critical error when trying to resolve dependencies in each module.
For example I'm using firebase-admin in the server to persist data to the Firebase Firestore. But it uses fs which cannot be resolved when the same code runs on the browser.
As a work around I set the resolve alias of firebase-admin to false inside the webpack configuration (given it runs on browser) see code below.
/** next.config.js **/
webpack: (config, { isServer }) => {
if (!isServer) {
// set alias of node specific modules to false
// eg: service dependencies
config.resolve.alias = {
...config.resolve.alias,
'firebase-admin': false,
}
} else {
// set alias of browser only modules to false.
config.resolve.alias = {
...config.resolve.alias,
}
}
While this does the trick, it won't be much long until the process gets really tedious to include all such dependencies within resolve aliases.
So, my approach to this is to write a script that runs prior to npm run dev (or manually) that will read all dependencies in package.json and SOMEHOW identify packages that will not run on a specific runtime environment and add them to the webpack config. In order to this, there should be a way to identify this nature of each dependency which I don't think is something that comes right out of the box from npm or the package itself.
Any suggestion on how this can be achieved is really appreciated. Thanks`

Gatsby Failed Build - error "window" is not available during server side rendering

I have been trying to build my gatsby (react) site recently using an external package.
The link to this package is "https://github.com/transitive-bullshit/react-particle-animation".
As I only have the option to change the props from the components detail, I cannot read/write the package file where it all gets together in the end as it is not included in the public folder of 'gatsby-build'.
What I have tried:
Editing the package file locally, which worked only on my machine but when I push it to netlify, which just receives the public folder and the corresponding package.json files and not the 'node-modules folder', I cannot make netlify read the file that I myself changed, as it requests it directly from the github page.
As a solution I found from a comment to this question, we can use the "Patch-Package" to save our fixes to the node module and then use it wherever we want.
This actually worked for me!
To explain how I fixed it: (As most of it is already written in the "Patch Package DOCS), so mentioning the main points:
I first made changes to my local package files that were giving the error.(For me they were in my node_modules folder)
Then I used the Patch Package Documentation to guide my self through the rest.
It worked after I pushed my changes to github such that now, Patch Package always gives me my edited version of the node_module.
When dealing with third-party modules that use window in Gatsby you need to add a null loader to its own webpack configuration to avoid the transpilation during the SSR (Server-Side Rendering). This is because gatsby develop occurs in the browser (where there is a window) while gatsby build occurs in the Node server where obviously there isn't a window or other global objects (because they are not even defined yet).
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /react-particle-animation/,
use: loaders.null(),
},
],
},
})
}
}
Keep in mind that the test value is a regular expression that will match a folder under node_modules so, ensure that the /react-particle-animation/ is the right name.
Using a patch-package may work but keep in mind that you are adding an extra package, another bundled file that could potentially affect the performance of the site. The proposed snippet is a built-in solution that is fired when you build your application.

How does one securely authenticate a React client via OAuth if everything is client-side?

I'm trying to use OAuth with a React (frontend) and Meteor (server) project. The service that I'm trying to OAuth to is not one of the popular widely supported ones (i.e. Google, Facebook), so I've been having some trouble figuring out how to go about this.
Meteor has support for a secure server-sided 'settings.json' file that stores your app's api keys and secrets, which I would presumably use to authenticate the client. I just don't understand how.
I found this package https://www.npmjs.com/package/react-oauth-flow and the 'send OAuth request' component looks like this:
<OauthSender
authorizeUrl="https://www.dropbox.com/oauth2/authorize"
clientId={process.env.CLIENT_ID}
redirectUri="https://www.yourapp.com/auth/dropbox"
state={{ from: '/settings' }}
render={({ url }) => <a href={url}>Connect to Dropbox</a>}
/>
Now, I don't understand how {process.env.CLIENT_ID} would be able to be stored/fetched securely since the entire app is available to the client? So I couldn't use something like Meteor.settings.CLIENT_ID, because the app's client ID is not available to the react application.
The same for the OauthReceiver component:
<OauthReceiver
tokenUrl="https://api.dropbox.com/oauth2/token"
clientId={process.env.CLIENT_ID}
clientSecret={process.env.CLIENT_SECRET}
redirectUri="https://www.yourapp.com/auth/dropbox"
onAuthSuccess={this.handleSuccess}
onAuthError={this.handleError}
render={({ processing, state, error }) => (
<div>
{processing && <p>Authorizing now...</p>}
{error && (
<p className="error">An error occured: {error.message}</p>
)}
</div>
)}
/>
How is {process.env.CLIENT_SECRET} fetched? Again, cannot use Meteor.settings.CLIENT_SECRET, since it's only available to the server and the component is rendered client-side.
I feel this is a conceptual understanding issue on my part and if anyone could explain it to me, I would be very grateful.
process.env.CLIENT_SECRET refers to an environment variable passed into your application from the command line at runtime. If you are using Create React App, the documentation on how to implement this is here.
If you are not using CRA, then you will pass the environment variables into your webpack build command, either from your package.json or from the command line. The syntax will look like this:
{ // the rest of your package.json scripts: { "dev": "webpack --env.API_URL=http://localhost:8000 --config webpack.config.dev.js", "build": "webpack --env.API_URL=https://www.myapi.com --config webpack.config.build.js" } }
Using this syntax, you can pass in your client secret/etc as environment variables to your React application. However, these will then be made available to the client and is not as secure as proper authentication code flow for OAuth2.0.
If you already have a back-end (which you do), look to implement this flow for enhanced security.

Browserifying react with addons to a standalone component, usable by plugins

I am experementing a bit with react and browserify and have these wishes:
I want to bundle all code written by me into a single file
I want to bundle all 3rd party dependencies (react, react-router, lodash etc) into separate files, one for each lib, to maximize caching possibilities
I have managed to do the things described above but I ran into this specific situation:
In some places of my code I want to use react with addons and as such require it like this: var React = require('react/addons). I don't do this in all parts of my code and it is not done in 3rd party dependencies such as react-router. This seems to create a conflict. Either the browserified bundle will only be available through var React = require('react/addons) which breaks 3rd party dependencies, or I will have to bundle react both with or without addons which menas that react is bundled and downloaded twice.
I tried to use aliasify and make react an alias for react/addons but I couldn't make it work. Should this be possible?
Another acceptable solution would be to bundle just the addons in a separate bundle and through that make both react and react/addons available through calls to require. Is any of this possible?
Addition
As a comment to the first comment by BrandonTilley, this is not just applicable to React and addons. Lodash also comes with a number of different distributions and I would like to be able to choose the version to use in my webapp in this case as well.
Notice that what you want to achieve is documented here: Browserify partitionning
I'm packaging my app in 2 parts: appLibs.js and app.js.
I've done this for caching too but I choose to put all the code that does not change often in a single bundle instead of splitting it like you want, but you can use the same trick.
Here's the code that might interest you:
var libs = [
"react",
"react/addons", // See why: https://github.com/substack/node-browserify/issues/1161
... other libs
];
gulp.task('browserify-libs', function () {
var b = browserify(...);
libs.forEach(function(lib) {
b.require(lib);
});
return b.bundle().......
});
gulp.task('browserify',['browserify-libs'],function () {
var b = browserify(...);
libs.forEach(function(lib) {
b.external(lib);
});
return b.bundle().......
});
This way, React is only bundled once in appLibs.js and can be required inside app.js using both react and react/addons
If you really want to bundle your libs in separate files, bundle then with b.require("myLib"), but in this case be sure to check that the libraries do not have direct dependencies. If a lib to bundle has a dependency in React, this means that lib will be packaged in the bundle, potentially leading to multiple bundles having React inside them (and making weird failures at runtime). The library should rather use peerDependencies so that b.require does not try to package these dependencies
Sounds like the perfect use case for factor-bundle.
From the browserify handbook:
factor-bundle splits browserify output into multiple bundle targets based on entry-point. For each entry-point, an entry-specific output file is built. Files that are needed by two or more of the entry files get factored out into a common bundle.
Thanks for all suggestions but the solution I have chosen is a "shim" if that is the correct term. Looks like this:
Browserify react/addons into it's own file
Create my own file (called shim) only containing this: module.exports = require('react/addons');
Browserify my shim and use the expose option, exposing it as react
Now, either if react or react/addons is required I get react/addons

Resources