Load different css files based on environment with NextJS - javascript

I'm building a frontend app using NextJS v.13 and this will be a generic frontend codebase that will be used by multiple sites.
I want to have:
button.site1.css
button.site2.css
And when I build the codebase for site1 I want to somehow tell the app to use button.site1.css when building.
Basically I want to achieve the following:
.env.local
HOST_NAME=site1
About.js
import styles from `./Button.${process.env.HOST_NAME}.scss`; // This doesn't work. "Imports must be string literals"
const About = () => {
<div>
<h1 className={styles.h1}">About Page</h1>
</div>
)
}

Although #Felix Eklöf has recommended a very nice approach by programmatically renaming files. Here's something more simple, suitable, and convenient.
Just import both the styles in your component and depending on the variable from .env file use the one that is needed. Next.js automatically tree-shakes extra classes that are not used, so you don't have to worry about performance in production or making production have large CSS.
import styles1 from "your-styles1.module.scss";
import styles2 from "your-styles2.module.scss";
const styles = process.env.HOSTNAME === "host1" ? styles1 : styles2;
Much straightforward and easier to implement. Right?
Update
If you are looking for conditionally adding global styles. Use link inside next/head instead.
First, put your styles inside public directory. Then, withing your _app.jsx
// do not import global styles that are scopped to specific HOST,
export default function(...) {
const styleSheetLink = getStyleSheetLink(process.env.HOSTNAME) // some logic to detect appropreate stylesheet.
...
return (
<>
<Head>
<link href={styleSheetLink} .../>
</Head>
...
</>
)
...
}
Update
To enable CSS Optimization, you need to install critters and enable CSS Optimization.
// next.config.js
...
experimental: { optimizeCss: true }
...
}
And
yarn add critters

Here's one option, not sure if it will fit your problem entirely.
So, you have button.site1.css, button.site2.css etc.
Add the env variable HOST_NAME=site1 as you suggested.
Write a java/bash- script that copies either button.site1.css or button.site2.css based on HOST_NAME into a common name like just button.css.
In your react components you import button.css instead.
import styles from `./button.css`;
In package.json add that script in prebuild.
{
"name": "npm-scripts-example",
"version": "1.0.0",
"description": "npm scripts example",
"scripts": {
"prebuild": "node copycss.js",
"build": "next build",
}
}
You also have to copy any of the CSS files manually when you're editing so you don't get errors in the editors. Or just run the script you've written locally with env vars.
EDIT
I was unable to try it earlier, but here's and example of copy script. I tried it in a fresh next.js project and it worked.
./copy-css.js
const fs = require('fs')
const site = process.env.HOST_NAME
fs.copyFileSync(`./styles/button.${site}.css`, `./styles/button.css`)
I'm guessing you have more files than just button, then you could put all site-specific CSS files in a separate folder and search it in the copy-css.js script and run the copyFileSync on each file.

Related

Storybook - Cannot Mock a Nested React-Router-Dom Link without throwing endless NPM errors

I am trying to implement StorybookJS into a SSR React app. Basic components work fine (button, headers etc). But anything that nests using dependencies like react-router-dom breaks.
Example:
We have a custom built <Link /> component that manages external links with a ternary. The external links flip to <a href= while internals use react-router-dom's <Link> imported as <ReactLink />. That code is like this:
// src/client/components/link/Link.js
import { Link as ReactLink } from "react-router-dom";
import { isLinkExternal } from "services/utils";
export const Link = ({ href, children = null, ...props }) => {
return isLinkExternal(href) ? (
<a href={href} {...props}>
{children}
</a>
) : (
<ReactLink to={href} {...props}>
{children}
</ReactLink>
);
};
The StorybookJS file for it looks like this:-
// link.stories.js
import React from "react";
import { Link } from "./Link"; // importing my component
export default {
title: "My Components/Link",
component: Link, // assigning my component
};
export const MyStoryBookLink = () => <Link href="/foo">I am a link</Link>;
Now, when i run Storybook it throws a load of errors, here are the recurring/main ones:-
ERROR in ./node_modules/redis-parser/lib/hiredis.js
Module not found: Error: Can't resolve 'hiredis' in '/Users/me/Documents/my-proj/node_modules/redis-parser/lib'
...
...
# ./.storybook/generated-stories-entry.js
I haven't touched anything redis / hiredis related and there is no such file as generated-stories-entry.js. The app works perfectly in Dev and Production so this is exclusively a Storybook env issue.
Next error down:
ERROR in ./node_modules/cache-manager-ioredis/node_modules/ioredis/lib/connectors/connector.js
Module not found: Error: Can't resolve 'net' in '/Users/me/Documents/myProject/node_modules/cache-manager-ioredis/node_modules/ioredis/lib/connectors'
Again, Though we are using cache-manager-ioredis, no idea why this is suddenly missing a module if it works fine on the app itself and all i'm trying to do is render a .
Next one:
Module not found: Error: Can't resolve 'tls' in cache-manager-ioredis
Same thing again^^
Then i get a load of these:
/Users/me/Documents/myProj/__mocks__/hiredis doesn't exist
.mjs
/Users/me/Documents/myProj/__mocks__/hiredis.mjs doesn't exist
.js
/Users/me/Documents/myProj/__mocks__/hiredis.js doesn't exist
.jsx
/Users/me/Documents/myProj/__mocks__/hiredis.jsx doesn't exist
.ts
/Users/me/Documents/myProj/__mocks__/hiredis.ts doesn't exist
.tsx
/Users/me/Documents/myProj/__mocks__/hiredis.tsx doesn't exist
.json
/Users/me/Documents/myProj/__mocks__/hiredis.json doesn't exist
.cjs
/Users/me/Documents/myProj/__mocks__/hiredis.cjs doesn't exist
Suggests it's looking for mocks to cover these sub sub sub dependencies, wherever they're needed.
I get the same for net and tls.
Finally, I get some:
Field 'browser' doesn't contain a valid alias configuration
I'm thinking somewhere in the depths of using react-router-dom/Link it is trying to find these, and they would only be there if webpack dev server / hot reloading made them accessible, OR if they were transpiled to be accessible from the production bundle.
But how do I mock these out? And is there an easy way to do it rather than manually mocking every sub dependency?
I have tried:
adding __mocks__/react-router-dom.js with an export const Link = ({props}) => <div>{children}</div> but it doesnt seem to kick in.
adding alias logic to .storybook/main.js:
webpackFinal: (config) => {
config.resolve.alias['react-router-dom'] = require.resolve('../__mocks__/react-router-dom.js');
return config;
},
Again, nothing seems to change.
using the storybook-react-router pkg but this seems quite old now, it configs to an old config.js file rather than main.js and uses the older storiesOf syntax. Also couldn't get to do anything.
manually installed tls, hiredis etc as --save-dev dependencies. But this seems hack. Why are these modules missing?
I cannot believe Storybook is this hard to use, more likely I'm overlooking something. I just want to mock something as common and basic as a from RRD.
What am I doing wrong? What am I missing?
I think I found the reason. It is because of node.js packages. To make it work, there are 2 solutions.
avoid importing node.js packages (usually related to SSR) for storybook related code. I use NX to structure my code, so I can easily move those part to its own library and only reference it from the top. (No storybook for the top App either in this solution)
skip those packages in the config.
something like
config.resolve.fallback = { http: false, net: false, tls: false, fs: false, dns: false, path: false };

Use env variable in helper file (Gatsby)

I have a Gatsby project that looks a bit like this:
File structure
- src
- pages
- Homepage.js
- helpers
- customFetch.js
Homepage.js
import React, { useEffect } from 'react'
import customFetch from '../helpers/customFetch'
export default function Homepage() {
useEffect(()=>{
setInterval(() => {
customFetch('/my-endpoint').then((result)=> {
// Do something...
})
}, 5000);
}, [])
return (
<div>
Homepage content here
</div>
)
}
customFetch.js
export default function customFetch(path) {
const apiURL = 'https://api.mysite.com'
// Do something fancy here...
return fetch(`${apiURL}${path}`)
}
What I want to do is to bring apiURL from a .env.* file instead of hard coding it. How could I do it the Gatsby way? Thanks!
PS.: I know that doing this inside a page is trivial (https://www.gatsbyjs.org/docs/environment-variables/) but that's a bit different than what I want to do.
First of all, you need to set up your environment in your build and develop command like this (in your package.json):
"build": "GATSBY_ACTIVE_ENV=yourEnvironment gatsby build",
"develop": "GATSBY_ACTIVE_ENV=yourEnvironment gatsby develop",
The snippet above will get the configuration from .env.yourEnvironment file. Then, you need to create your environment file and define your desired variables, such as (.env.yourEnvironment):
API_URL=https://api.mysite.com
Then, in your gatsby-config.js (outside module.exports) you need to require that environment file:
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
})
According to Gatsby documentation, this is because:
Project environment variables that you defined in the .env.* files
will NOT be immediately available in your Node.js scripts. To use
those variables, use NPM package dotenv to examine the active .env.*
file and attach those values. dotenv is already a dependency of
Gatsby, so you can require it in your gatsby-config.js or
gatsby-node.js like this:
The last step is to get your variable in your component using:
const apiURL = process.env.API_URL
In your case:
export default function customFetch(path) {
const apiURL = process.env.API_URL
// Do something fancy here...
return fetch(`${apiURL}${path}`)
}
That allows you to separate the logic and token and environment variables to implement a multisite (multidomain) project for example.

Use sass variables into js files create-react-app

I have some color defined variables into _colors.scss file.
$color-succcess: #706caa;
$color-error: #dc3545;
I would like to also use them into some styled react components react-table into my js file.
I used the following article https://til.hashrocket.com/posts/sxbrscjuqu-share-scss-variables-with-javascript as reference and many others like it but I cannot get it to work.
I export the colors from my scss file:
:export {
colorSuccess: $color-succcess;
colorError: $color-error;
}
and I import them into my js file:
import colors from "../../styles/_colors.scss";
but they are probably not loaded right.
How can I configure the create-react-app generated code to achieve the same thing as the guy in the article does with webpack.
I have faced a similar issue, and it took me a bunch of tries to get the desired result. If you already have node-sass installed, try the following.
First of all, try importing the main scss file without the underscore, i.e. instead of ../../styles/_colors.scss, try ../../styles/index.scss or whatewer your main file is.
Secondly, keep track of the variable names. This code DID NOT work:
:export {
$textBlack: $text-black;
}
while this one works perfectly
:export {
textBlack: $text-black;
}
For some reason it does not like the dollar sign in the variable name, though it is a valid JS variable name. Your case should work fine
Try to reproduce these steps:
1. Enable sass in CRA by installing node-sass.
npm i --save-dev node-sass
2. Create a sass file example.scss.
$hello: 'world';
:export {
my-var: $hello;
}
3. Import the sass into your react component.
import React from 'react';
import Example from './example.scss';
export default () => Example.hello;
If you are using CRA version 4, there is a known issue where this feature will not work unless you define your scss file as <name>.modules.css.
See https://github.com/facebook/create-react-app/issues/10047#issuecomment-724227353 for details.
install the sass in the project if you didn't install that.
yarn add --dev node-sass
create a file and use this rule to name it => filename.module.scss ( for example variables.module.scss )
$color-succcess: #706caa;
$color-error: #dc3545;
:export {
colorSuccess: $color-succcess;
colorError: $color-error;
otherColor: #786539;
}
and import it in the component like below:
import colors from "../../styles/_colors.module.scss";
const TheComponent = () => {
console.log(colors.colorSuccess)
return <h1>The Component</h1>
}
export default TheComponent
// output in console : #706caa
In order to load SCSS files in CRA you need to add node-sass as your dependency to package.json. I've tested it out and after adding it to clean CRA project and importing colors object (using :export, like in the code you provided) works as expected.

How to Embed StencilJS components inside Storybook React App?

I'm trying to integrate Stencil and Storybook inside the same project. I've been following this set up guide and this one however one of the steps is to publish the library of components to NPM and that's not what I want.
I have this repo which I've configured with components library (src folder) and with the reviewer of those components with Storybook, which resides in the storybook folder.
The problem is that when I compile the components using Stencil and copy the dist folder inside the Storybook app and import the component nothing renders. Tweaking the configuration using custom head tags I was able to import it correctly however no styles where applied.
When I open the network panel there is some error when importing the component:
And thus the component is present in the DOM but with visibility set to hidden, which I think it does when there is an error.
This is the component au-button:
import { Component } from '#stencil/core';
#Component({
tag: 'au-button',
styleUrl: 'button.css',
shadow: true
})
export class Button {
render() {
return (
<button class="test">Hello</button>
);
}
}
Here is the story my component:
import React from 'react';
import { storiesOf } from '#storybook/react';
import '../components/components.js'
storiesOf('Button', module)
.add('with text', () => <au-button></au-button>)
These are the scripts inside the Storybook app:
"scripts": {
"storybook": "start-storybook -p 9009",
"build-storybook": "build-storybook",
"copy": "cp -R ./../dist/* components"
},
And the workflow is as follows:
Launch storybook
Make changes in the component
Execute build command
Execute copy command
Also, I would like to automate the developer experience, but after I solve this problem first.
Any ideas of what I could be doing wrong?
Sample for this could be found in the repo
https://github.com/shanmugapriyaEK/stencil-storybook. It autogenerates stories with knobs and notes. Also it has custom theme in it. Hope it helps.
I'm using #storybook/polymer and it's working for me really well.
following your example:
import { Component } from '#stencil/core';
#Component({
tag: 'au-button',
styleUrl: 'button.css',
shadow: true
})
export class Button {
render() {
return (
<button class="test">Hello</button>
);
}
}
the story would be:
import { storiesOf } from '#storybook/polymer';
storiesOf('Button', module)
.add('with text', () => <au-button></au-button>)
the scripts in the package.json:
"scripts": {
"storybook": "start-storybook -p 9001 -c .storybook -s www"
},
the storybook config file:
import { configure, addDecorator } from '#storybook/polymer';
const req = require.context('../src', true, /\.stories\.js$/);
function loadStories() {
req.keys().forEach((filename) => req(filename))
}
configure(loadStories, module);
and storybook preview-head.html you have to add to the body the following:
<body>
<div id="root"></div>
<div id="error-message"></div>
<div id="error-stack"></div>
</body>
I've been following this set up guide and this one however one of the steps is to publish the library of components to NPM and that's not what I want.
My reading of those guides is that they're stating “publish to NPM” as a way to have your files at a known URL, that will work most easily for deployment.
Without doing that, you'll need to figure out a different deployment strategy. How will you get the build products – the dist directory and static files – published so that your HTML will be able to reference it at a known URL? By choosing to diverge from the guidelines, that's the problem you have to address manually instead.
Not an insurmountable problem, but there is no general solution for all. You've chosen (for your own reasons) to reject the solution offered by the how-to guides, which means you accept the mantle of “I know what I want” instead :-)

Import or require a bundled js file created by react application

Let's say I have a normal react application using redux and some ajax calls.
If I want to pass it to someone I will give them the bundled js file I created with webpack and ask them to include it in their HTML + render a div with an id of "myApp" for example:
<div id="myApp"></div>
Ok, what if their website is also created with react, and they want to include my bundled js file inside one of their components, and of course render the relevant div?
I tried to use import or require to simulate this:
require('./path/to/myBundle.js');
import './path/to/myBundle.js';
Example:
//...
import './path/to/myBundle.js'; // the file that will render myApp to the relevant div
// ....
export function SomeApp(args){
return(
<div>
<div id="myApp"></div>
<SomeComponent />
</div>
);
};`
This does not work as I get some errors about:
Uncaught Error: Minified React error #37; visit
http://facebook.github.io/react/docs/error-decoder.html?invariant=37
for the full message or use the non-minified dev environment for full
errors and additional helpful warnings.
And when I visit this site I see:
_registerComponent(...): Target container is not a DOM element.
However, if they'll use this file (myBundle.js) outside their components (top level index.html for example) it will work just fine of course.
EDIT:
I forgot to mention that I think I know what the problem is, the application doesn't have the HTML ready with this div yet. but I don't know a good and native way to wait for it to exist.
EDIT #2 following #Frxstrem 's answer:
I'm trying to follow this answer but I think I'm doing it wrong.
I have 2 copies of corry house slingshot demo app as app1 and app2.
changed the 'output' on webpack.config.prod.js of app1 to:
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
filename: 'app1Bundle.js',
library: "App1",
libraryTarget: "umd"
},
I'm trying to render app1 inside the homepage component of app2.
so i copied the "published" files from app1 to the root of app2 and called the folder app1, then added an import call:
import {app1} from '../../app1/app1Bundle';
and a matching tag inside the return function:
const HomePage = () => {
return (
<div>
<app1 />
<h1>App 2</h1>
</div>
);
};
I get the same error as I posted above.
I also tried different combinations:
import app1 from '../../app1/app1Bundle'; // without curly braces
or even just getting the script as a normal js script
import '../../app1/app1Bundle';
or
require('../../app1/app1Bundle');
and then tried to render a normal div tag with an id of "app1"
const HomePage = () => {
return (
<div>
<div id="app1"></div>
<h1>App 2</h1>
</div>
);
};
nothing seems to work as I still get the same error.
I think the problem is the timing of the script load and the rendering of the elements. I think the div does not exist yet when the bundled script is searching for it.
By default, Webpack will expose the entry module as a variable, which is useful when you include scripts with a <script> tag. (Because of this, if you require it you would likely just get {}.) However, if you want to load your bundle from other modules, you'll need to tell Webpack to expose it as an exported module instead.
The easiest way to do this is to set
{
...
"libraryTarget": "umd"
}
in your Webpack configuration. With that, Webpack knows that it should expose your entry module as a module that can be required in Webpack, but can also be loaded with a <script> tag as necessary.
Webpack libraryTarget documentation
The main problem i faced was to include the bundled js file of app1 after the DOM contains the target div it needs.
What i ended up doing was, creating a component in app2 project that will require() the bundled js file on componentDidMount() and will render and return the target div with a relevant id.
The reason i created a component is purely for re-usability purpose, instead of requiring this script with componentDidMount() on every component that needs it.
So, this is the component:
import React from 'react';
class AppOne extends React.Component {
componentDidMount() {
require('../app1/app1Bundle.js');
}
render() {
return (
<div id="app1"></div>
);
}
}
export default AppOne;
And this is how i use it in other component:
import React from 'react';
import AppOne from './AppOne';
const HomePage = () => {
return (
<div>
<h1>App 2 - wrapper for app1</h1>
<hr />
<AppOne />
<hr />
<h1>This is App2 as well </h1>
</div>
);
};
export default HomePage;
It's working fine. my only concern is that i may face some conflicts with react because i'm using 2 react apps though for ow i don't see any errors.
I guess that's an issue for a different question.
EDIT:
If someone will use this approach you should note that this will work only for the first load. because after the component will re-render itself the bundled script will not run again.

Categories