Loading chunks from dynamic imports though CDN URL like other assets - javascript

I have a CDN pointing to my base domain, there is 1:1 mapping basically. I'm trying to build my bundle on the server, and I want to load it using CDN URL. What I want to have after npm run build is:
public/
css/
app.css
js/
index.js
1.js.gz
1.js
2.js.gz
2.js
And then my CDN will reflect that, so I want these resources to be loaded like this:
https://mycdn.com/public/js/index.js
https://mycdn.com/public/css/app.css
My current webpack.mix.js config:
mix
.sass('resources/sass/app.css', 'public/css')
.js('resources/js/index.js', 'public/js')
It generates all files in the proper location, which is good. Then I include them in my index.blade.php:
<script src="{{ elixirCDN('/js/index.js') }}"></script>
elixirCDN is my custom function:
function elixirCDN($file)
{
$cdn = '';
if(config('system.cdn_url'))
{
$cdn = config('system.cdn_url');
}
return $cdn . elixir($file);
}
It basically prepends filename with CDN url, so it all works fine.
The problem begins when I use dynamic imports, like this:
const Home = () => import("./Home")
The ideal situation would be that it also loads with the CDN:
https://mycdn.com/public/js/1.js
but instead, it doesn't, it loads with a relative path and my base domain:
https://mybasedomain.com/public/js/1.js
Obviously, because it's dynamic. How can I make my chunks to be loaded from the CDN as well?
I've tried to set publicPath as my CDN url, but it doesn't have any effect. I've also tried setPublicPath() but it's the same.

Hi this is not the definitive answer but a step in the right direction. I got into this issue because my conditional import broke after updating all the webpack and babel packages.
Before the dynamic import you need to set the temporary public path for webpack to the location of the chunck which webpack needs to import dynamically
__webpack_public_path__ = 'https://cdn.dev.acsi.eu/eurocampings/assets/dev/dist/';
Than the import works
import(/* webpackChunkName: "state" */ './modules/BookingState/State')
.then(({createState}) => {
createState();
this.renderEurocampingsWebsite(window);
});
For me the dynamic import works, however it seems that the conditional import before, all the package updates, was working in without async behavior...
Just found out that Webpack has more magical comments, which can make the code behave procedural, at the cost of having both modules imported for the conditional import. This might also be helpful
import(
/* webpackChunkName: "state" */
/* webpackMode: "eager" */
/* webpackPreload: true */
'./modules/BookingState/State'
)
.then...

I had a similar problem, where my main script was loaded into another page, and when loading chunks it tried loading them from the root url instead of the cdn.
Using the answer above, I came to the following solution:
in root imported file
export function setWebpackToLoadChunksFromScriptUrl() {
const scriptPath: string = (document.currentScript as any || {}).src;
const loadPath = scriptPath ? scriptPath.substring(0, scriptPath.lastIndexOf('/')) : '';
if (loadPath) {
__webpack_public_path__ = loadPath;
}
}
setWebpackToLoadChunksFromScriptUrl();
export async function loadApplication(elementId: string) {
const { loadApplicationOnHtmlElement } = (await import('./loaded-app'));
loadApplicationOnHtmlElement((elementId));
}

Related

How to add custom scripts bundle in NextJS

I have some legacy custom javascripts that I need to bundle and put them in _document.js as a link. The filename should include a hash.
What would be the best way to accomplish this?
I tried webpack configs regarding entry/output but they break NextJs build.
The problem is that we use things like window, document, etc that do crash in server side.
Ideally what is needed is to inject this into a tag, as compiled / babelified javascript code.
What I tried is
Webpack HTML Plugin plus other plugins like InlineChunk or
InlineSource plugins. They didn't work because they generate code in
an index.html that is not used by NextJS.
Using Raw Loader to get the file content. Doesn't work because it is
not babelified.
Adding a custom entry to the Webpack config, like scripts:
'path/to/my-entry.js'. Didn't work because it adds a hash name to the
file and I have no way of knowing it.
Adding a custom entry into the NextJs polyfills. I thought it made
sense, but the polyfill tag has a nomodule which prevents its code to
run on new browsers.
Another options is to add the javascript code as a string, and then using __dangerouslySetInnerHtml but the problem is that I lose linter and babel abilities there.
I tried adding it as a page, but crashes for local development and even on build
webpack.config.js
module.exports = (nextConfig = {}) =>
Object.assign({}, nextConfig, {
webpack(config, options) {
const nextJsEntries = config.entry;
config.entry = async () => {
const entries = await nextJsEntries();
entries['pages/rscripts'] = 'test/test.js';
return entries;
};
...
Then in _document.js
<script src={`${publicRuntimeConfig.ASSET_PREFIX}/_next/${this.props.buildManifest.pages['/rscripts'][2]}`} />
You can just import js file like import 'path/to/js_file' in your _app.js/app.tsx file
import "../styles/globals.css"
import "../js/test"
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
This one works fine for me
I wanted to add another answer here as I came across this and I believe some things have changed in Next JS. Next now has this script component that you can use to load external scripts or dangerously set a script.
The Next.js Script component, next/script, is an extension of the HTML
element. It enables developers to set the loading priority of
third-party scripts anywhere in their application, outside next/head,
saving developer time while improving loading performance.
The cool thing is you can put them into whatever pages you want, maybe you have a script you want on a homepage, but not other pages, and Next will extract them and place them on the page based on the strategy you select. There are a few gotchas, can't load in the head, beforeInteractive is a little finicky, so I would read the link above and the actual API reference before making any choices.
import { useEffect } from 'react';
import Script from 'next/script';
function thirdPartyScript() {
useEffect(() => {
// just for fun. This actually fires
// before the onLoad callback
}, []);
return (
<Script
id="test-script"
strategy="afterInteractive"
src="/public/pages/scripts/test.js"
onLoad={() => {
console.log('Onload fires as you would expect');
}}
/>
);
}

Include JSON files into React build

I know this question maybe exist in stack overflow but I didn't get any good answers, and I hope in 2020 there is better solution.
In my react app I have a config JSON file, it contains information like the title, languages to the website etc..
and this file is located in 'src' directory
{
"headers":{
"title":"chat ",
"keys":"chat,asd ,
"description":" website"
},
"languages":{
"ru":"russian",
"ar":"arabic",
"en":"English"
},
"defaultLanguage":"ru",
"colors":{
"mainColor":"red",
"primary":"green",
"chatBackGround":"white"
}
}
I want to make my website easy to edit after publishing it, but after I build my app, I can't find that settings.json file there in build directory.
I find out that files in public directory actually get included to build folder, I tried to put my settings.JSON in public,
but react won't let me import anything outside of src directory
I found other solutions like this one but didn't work
https://github.com/facebook/create-react-app/issues/5378
Also I tried to create in index.html a global var like (window.JSON_DATA={}), and attach a JS object to it and import it to App.js, but still didn't work.
How can I make a settings JSON file, and have the ability to edit it after publishing the app?
Add your settings.json to the public folder. React will copy the file to the root of build. Then load it with fetch where you need it to be used. For example if you need to load setting.json to the App.js then do the next:
function App() {
const [state, setState] = useState({settings: null});
useEffect(()=>{
fetch('settings.json').then(response => {
response.json().then(settings => {
// instead of setting state you can use it any other way
setState({settings: settings});
})
})
})
}
If you use class-components then do the same in componentDidMount:
class CustomComponent extends React.Component {
constructor(props) {
super(props);
this.state = {settings: null};
}
componentDidMount() {
fetch('settings.json').then(response => {
response.json().then(settings => {
this.setState({settings: settings});
})
})
}
}
Then you can use it in render (or any other places of your component):
function App() {
...
return (
{this.state.settings && this.state.settings.value}
)
}
The easiest way would be to require() the file on the server during server side rendering of the html page and then inline the json in the html payload in a global var like you mentioned window.JSON_DATA={}. Then in your js code you can just reference that global var instead of trying to use import.
Of course this approach would require you to restart your server every time you make a change to the json file, so that it get's picked up. If that is not an option then you'll need to make an api call on the server instead of using require().
You may want to look at using npm react-scripts (https://www.npmjs.com/package/react-scripts) to produce your react application and build. This will package will create a template that you can put your existing code into and then give you a pre-configure build option that you can modify if you would like. The pre-configured build option will package your .json files as well. Check out their getting started section (https://create-react-app.dev/docs/getting-started/)
If you don't want to go that route, and are just looking for quick fix, then I would suggest change your json files to a JS file, export the JS object and import it in the files you need it since you seem to be able to do that.
//src/sampledata.js
module.exports = {
sample: 'data'
}
//src/example.jsx (this can also be .js)
const sampledata = require('./sampledata');
console.log(sampledata.sample); // 'data'
you can use 'Fetch Data from a JSON File'
according to link
https://www.pluralsight.com/guides/fetch-data-from-a-json-file-in-a-react-app
example

How to import a module from the static using dynamic import of es6?

I'm trying to add dynamic import into my code to have a better performance on the client-side. So I have a webpack config where is bundling js files. On SFCC the bundled files are in the static folder where the path to that files is something like this: /en/v1569517927607/js/app.js)
I have a function where I'm using dynamic import of es6 to call a module when the user clicks on a button. The problem is that when we call for that module, the browser doesn't find it because the path is wrong.
/en/lazyLoad.js net::ERR_ABORTED 404 (Not Found)
This is normal because the file is on /en/v1569517927607/js/lazyLoad.js.
There is a way to get it from the right path? Here is my code.
window.onload = () => {
const lazyAlertBtn = document.querySelector("#lazyLoad");
lazyAlertBtn.addEventListener("click", () => {
import(/* webpackChunkName: "lazyLoad" */ '../modules/lazyLoad').then(module => {
module.lazyLoad();
});
});
};
I had the same problem and solved it using the Merchant Tools > SEO > Dynamic Mapping module in Business Manager.
There you can use a rule like the following to redirect the request to the static folder:
**/*.bundle.js i s,,,,,/js/{0}.bundle.js
All my chunk files are named with the <module>.bundle pattern.
Here you can find more info :
https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/search_engine_optimization/b2c_dynamic_mappings.html
Hope this helps.
I believe you'll likely need to do some path.resolve() magic in either your import statement or your webpack.config.js file as is shown in the accepted answer to this question: Set correct path to lazy-load component using Webpack - ES6
We did it in a different way. That required two steps
From within the template file add a script tag that creates a global variable for the static path. Something like
// inside .isml template
<script>
// help webpack know about the path of js scripts -> used for lazy loading
window.__staticPath__ = "${URLUtils.httpsStatic('/')}";
</script>
Then you need to instruct webpack to know where to find chunks by changing __webpack_public_path__ at runtime
// somewhere in your main .js file
// eslint-disable-next-line
__webpack_public_path__ = window.__staticPath__ + 'js/';
Optional step:
You might also want to remove code version from your __staticPath__ using replace (at least we had to do that)
__webpack_public_path__ = window.__staticPath__.replace('{YOUR_CODE_VERSION_GOES_HERE}', '') + 'js/';

Conditionally import assets in create-react-app

Is it possible to conditionally import assets when creating a React app using create-react-app? I'm aware of the require syntax - example:
import React from "react";
const path = process.env.REACT_APP_TYPE === "app_1" ? "app_1" : "app_2";
const imagePath = require(`./assets/${path}/main.png`);
export default function Test() {
return (
<img src={imagePath} alt="" />
);
}
This however bundles all my assets no matter what.
It will load the proper image, but it will still bundle all the files together in the final build.
When I look in the dev tools for the final build, I can see all the assets there even though I only wanted to load the assets for app_1.
Am I forced to touch the webpack config, if so, what should I change? or is there another way?
In the days when React didn't exist we didn't put assets into our JS files. We let the CSS to decide, what assets to load for what selectors. Then you could simply switch a corresponding class on or off for a corresponding element (or even the whole page) and viola it changes color, background, or even a form. Pure magic!
Ah. What times these were!
All above is true and I do not understand why would anyone do or recommend doing it differently. However if you still want to do it (for any reason) - you can! Latest create-react-app comes with out-of-the-box support for lazy loading of arbitrary components via dynamic importing and code splitting. All you need to do is use parenthesized version of the import() statement instead of the regular one. import() takes in a request string as usual and returns a Promise. That's it. Source code of the dynamicaly requested component won't be bundled in, but instead stored in separate chunks to be loaded on demand.
Before:
import OtherComponent from './OtherComponent';
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
After:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
Notice how function MyComponent part is identical.
For those wondering if it is tied to CRA or React, it's not. It's a generic concept that can be used in vanilla JavaScript.
You will need to use webpack (or other bundler.) The code is not being run when it's bundled, so the compiler has no way of knowing which branch of logic to follow (app_1 or app_2). Therefore you have to get into the bundler's logic in order to achieve your goal.
However, this isn't as scary as it seems since webpack has built in capability to do this (no 3rd parties required...)
I would look into using webpack.providePlugin
(https://webpack.js.org/plugins/provide-plugin)
or its sibling DefinePlugin
(https://webpack.js.org/plugins/define-plugin)
(I'm afraid these examples are off the top of my head, so it's very unlikely they'll work on first pass.)
Examples:
Both will require a provider module...
// in path/provider.js
module.exports = {
live: '/path/to/live/image',
dev: '/path/to/dev/image'
}
Provide Plugin Example
// in webpack
new webpack.ProvidePlugin({
imagePath: [
'path/provider', // the file defined above
process.env.ENVIRONMENT // either 'dev' or 'live'
]
}),
// in code
export default function Test() {
return (
<img src={imagePath} alt="" />
);
}
Define Plugin example:
// in webpack
new webpack.DefinePlugin({
'process.env.ENVIRONMENT': JSON.stringify(process.env.ENVIRONMENT)
});
// in code
var providers = require('path/provider'); // same path provider as above
export default function Test() {
return (
<img src={providers[process.env.ENVIRONMENT]} alt="" />
);
}
In both cases the bundler is forced to collapse your variable to an actual literal value at compile time - before bundling has taken place. Since you have now collapsed the logical path down to a single option, it is now free to only bundle the relevant assets.
You can't do this with default CRA settings.
Because if your dynamic require or dynamic import path is not static, webpack won't be able to determine which assets to include in the final build folder, therefore, it will grab everything from your ./src folder, and put them all to your build folder.
There is a way to do it with default CRA settings
You can add to .env something like
REACT_APP_SKIN=1
Put your skin assets in public/css1, public/css2 etc. And include them in public/index.html using code like
<link href="/css%REACT_APP_SKIN%/theme.css" rel="stylesheet">

Access static resource in server-side code in NextJs?

Im using the static render feature of NextJS to generate a static version of my site thus I want to ensure that on the very first render of the page all the data it needs to render correctly is supplied.
I have a number of blog posts which I have stored as .md files in /static and want to access them in a page such as:
import * as fs from "fs";
...
export default class extends React.Component<IProps, any> {
static async getInitialProps (props: IServerProps) {
const post = (await getDb()).posts.find(p => p.id == props.query.id);
const markdown = fs.readFileSync(`/static/posts/${post.markdownFileName}`);
return { post, markdown }
}
...
But if try to run the above I get the following error:
This dependency was not found: * fs
So im not sure how I should go about accessing these static resources while on the server..
Unfortunately Next.js doesn't allow the use of webpack loaders to handle different file types on the server (Although they are used to build the client-side bundles), but it is possible to use a Babel plugin. One such plugin for Markdown content can be found here: https://www.npmjs.com/package/babel-plugin-markdown
After configuring it in .babelrc:
{
"plugins": ["markdown"]
}
it's possible to use markdown.require() to pull in .md content:
const html = markdown.require('./foo.md')
More options are described at the link!

Categories

Resources