Failed to regiester service worker error in Next.js file - reactjs

I'm using workbox-webpack-plugin to generate a service worker for me and I'm using copy-webpack-plugin move generated service worker files to the same directory as main.js. My next js config file goes like this:-
module.exports = {
webpack: (config, {isServer, buildId, dev, ...rest}) => {
if (dev) {
const devSwSrc = join(__dirname, "register-sw.js");
config.plugins.push(new CopyWebpackPlugin([devSwSrc]));
config.plugins.push(new GenerateSW({ ...defaultGenerateOpts }));
// Register SW
const originalEntry = config.entry;
config.entry = async () => {
const entries = await originalEntry();
const swCompiledPath = join(__dirname, 'register-sw-compiled.js');
if (entries['main.js'] && !entries['main.js'].includes(swCompiledPath)) {
let content = await readFile(require.resolve('./register-sw.js'), 'utf8');
await writeFile(swCompiledPath, content, 'utf8');
entries['main.js'].unshift(swCompiledPath);
}
return entries;
};
}
I'm trying to copying my service worker to the same dir as main.js which is chunk/static so that when it's fetched it should not return any error. But instead, I'm getting this error.
TypeError: Failed to register a ServiceWorker for scope ('[http://localhost:3000/](http://localhost:3000/)') with the script ('[http://localhost:3000/service-worker.js](http://localhost:3000/service-worker.js)'): A bad HTTP response code (404) was received when fetching the script.
I know this error is because it's not getting served from the same dir as main.js and I need to make some changes in copy-webpack-plugin in order to achieve that. Also I'm trying to avoid custom server.js file to server routes like /service-worker
Any help would be really appreciated. Thanks in advance

Related

Loading contents of files into a NextJs app at runtime as env vars

I'm trying to load the contents of files in a specific folder as env vars accessible at process.env.SomethingHere at runtime not build time as those files don't exist at build time yet (if process.env is the right place to load those secrets, more about that in the end). I managed to write the code to read the files and create a key-value object/dictionary from them, but not sure where is the best spot in the app lifecycle to add this so it's available for both server-side middleware and client-side frontend
function readFiles(dir) {
var vars = {};
if(!fs.existsSync(dir))
{
console.log("failed to load files in directory: " + dir);
return vars;
}
// read directory
let fileNames = fs.readdirSync(dir);
if (!fileNames) {
console.log("failed to load files in directory: " + dir);
return vars;
}
fileNames.forEach((filename, index) => {
// get current file name
const name = path.parse(filename).name;
// get current file path
const filepath = path.resolve(dir, filename);
// get information about the file
let stat = fs.statSync(filepath);
if (!stat) {
console.log("failed to load file stats: " + name);
return;
}
// check if the current path is a file or a folder
const isFile = stat.isFile();
// exclude folders
if (isFile) {
let data = fs.readFileSync(filepath);
if (!data) {
console.log("failed to load file: " + name);
return;
}
let content = data.toString('utf8', 0, data.length);
// callback, do something with the file
vars[name] = content;
}
});
return vars;
}
Some background:
I'm using Azure keyvault for storing secrets and have a Kubernetes cluster with a frontend and a backend pod.
In the backend pod I was able to mount the secrets using CSI driver fetch the values at app startup and load that into the python os.environ. I'm trying to achieve the same for the frontend pod, the mounting is successful, but I haven't been able to place my code in the right spot to load the secrets into process.env if this is even the right place to add them.
ChatGPT solved the issue eventually, here is the answer:
In a Next.js application, the server-side entry point is the server.js file located in the root directory of the project. By default, this file exports the createServer function, which is used to create the Express server that handles server-side rendering of the application.
You can add your code to load the environment variables in this file by importing the readFiles function and using the Object.assign() method to merge the key-value object with the existing process.env object, like this:
env.js
function readFiles(dir) {
// existing code here
}
module.exports = readFiles;
server.js in the root directory of the project
const readFiles = require('./env');
const dir = './dir_name'
const envVars = readFiles(dir);
Object.assign(process.env, envVars);
const dev = process.env.NODE_ENV !== 'production'
const hostname = 'localhost'
const port = 3000
const {createServer} = require('next');
const app = next({ dev, hostname, port })
const handle = app.getRequestHandler();
app.prepare().then(() => {
createServer(handle).listen(port, (err) => {
if (err) throw err;
console.log(`> Ready on http://${hostname}:${port}`)
});
});

How to register two different service-workers in the same scope?

I have a service-worker.js file to make my reactjs app PWA. I now also want to add push notification using FCM, which requires me to have firebase-messaging-sw.js in the public folder. So now for both to work both are going to require to be in the same scope.
But as far as I have read from various answers on this site, we can't have two different service workers in the same scope, so how do we combine both service-worker.js and firebase-messaging-sw.js so both can function properly. One of the answers suggested that I rename the service-worker.js to firebase-messaging-sw.js which doesn't work. I did find one successful implementation on GitHub which I didn't understand much https://github.com/ERS-HCL/reactjs-pwa-firebase .
How can I have both the service-worker.js and firebase-messaging-sw.js work together?
firebase-messaging-sw.js
// Scripts for firebase and firebase messaging
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
// Initialize the Firebase app in the service worker by passing the generated config
const firebaseConfig = {
apiKey: "xxxx",
authDomain: "xxxx.firebaseapp.com",
projectId: "xxxx",
storageBucket: "xxxx",
messagingSenderId: "xxxx",
appId: "xxxx",
measurementId: "xxxx"
}
firebase.initializeApp(firebaseConfig);
// Retrieve firebase messaging
const messaging = firebase.messaging();
self.addEventListener("notificationclick", function (event) {
console.debug('SW notification click event', event)
const url = event.notification.data.link
event.waitUntil(
clients.matchAll({type: 'window'}).then( windowClients => {
// Check if there is already a window/tab open with the target URL
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
// If so, just focus it.
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
// If not, then open the target URL in a new window/tab.
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
})
messaging.onBackgroundMessage(async function(payload) {
console.log("Received background message ", payload)
const notificationTitle = payload.notification.title
const notificationOptions = {
body: payload.notification.body,
icon: './logo192.png',
badge: './notification-badgex24.png',
data: {
link: payload.data?.link
}
}
self.registration.showNotification(notificationTitle, notificationOptions)
})
service-worker.js
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
clientsClaim();
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }) => {
// If this isn't a navigation, skip.
if (request.mode !== 'navigate') {
return false;
} // If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) {
return false;
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
} // Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
Ok, after spending weeks, I figured it out. So for anyone else, using the service worker created using the default create react app, follow the steps below.
first create firebase-messaging-sw.js in public folder and put the same code as in the question, but this time also add importScripts("/service-worker.js").
// Scripts for firebase and firebase messaging
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js")
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js")
importScripts(`/service-worker.js`) // <- here notice this
.
.
.
The import will import the service-worker.js after the build step. The service worker you have in the src folder is only a template. You cannot use importScript in the service-worker.js file that's in the src folder as that will throw importScripts is not defined error.
Your build folder after your build step:
└── build/
├── static/
│ ├── .
│ ├── .
│ └── .
├── index.html
├── firebase-messaging-sw.js
├── service-worker.js
├── .
└── .
Now in index.html add
<script>
if ('serviceWorker' in navigator){
navigator.serviceWorker.register('/firebase-messaging-sw.js')
.then(reg => console.debug("Service worker registered sucessfully"))
}
</script>
And that's it. Both firebase-messaging and your PWA service worker will work
or you can also
You can create a new file called sw.js in your public folder and use imporScript() to import both firebase-messaging-sw.js and default service-worker.js
make sure to pass in service worker registration in getToken of the firebase
const registration = await navigator.serviceWorker.register('/sw.js')
const currentToken = await getToken(messaging, {
serviceWorkerRegistration: registration,
vapidKey: '<VAPID_KEY>'
})
now simply register your new service worker in index file as shown
I think one way of doing this is just having only firebase-messaging-sw.js file and using workbox to inject your service-worker.js into it
https://developer.chrome.com/docs/workbox/precaching-with-workbox/
forexample :
// build-sw.js
import {injectManifest} from 'workbox-build';
injectManifest({
swSrc: './src/sw.js',
swDest: './dist/firebase-messaging-sw.js',
globDirectory: './dist',
globPatterns: [
'**/*.js',
'**/*.css',
'**/*.svg'
]
});
and all firebase config must be in sw.js to be written in firebase-messaging-sw.js
and in your package.json simply run build-sw.js before react-script start
"scripts": {
"start": "node build-sw.js && react-scripts start",
}
or you can use react-app-rewired and workbox box plugin instead

Serve static javascript file from nextjs api

I need a route in website build with nextjs that sends javascript that can be used on different website to do some things.
I created new file in pages/api, let's call it sendTest.ts so it's location is pages/api/sendTest.ts. In the same folder I crated test.ts file that I want to send from sendTest.ts.
sendTest.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import fs from 'fs';
import path from 'path'
const filePath = path.join(__dirname, 'test.js');
const file = fs.readFileSync(filePath);
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.setHeader('Content-Type', 'text/javascript');
res.status(200).send(file)
}
test.ts
console.log('hello');
After build nextjs serves that file at website.com/api/sendTest but after bundling it ends up as
"use strict";
(() => {
var exports = {};
exports.id = 318;
exports.ids = [318];
exports.modules = {
/***/ 211:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
console.log("hello there");
/***/ })
};
;
// load runtime
var __webpack_require__ = require("../../webpack-api-runtime.js");
__webpack_require__.C(exports);
var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
var __webpack_exports__ = (__webpack_exec__(211));
module.exports = __webpack_exports__;
})();
which when used in different page as
<script src="website.com/api/sendTest"></script>
results in error
Uncaught ReferenceError: require is not defined
at sendTest:22:27
at sendTest:28:3
My question is how can I force nextjs to skip loading webpack into that file and just allow typescript to change content into javascript and serve file as is? Or is there better way to do what I want, which is sending javascript from specified nextjs route?
Got it.
I changed
const filePath = path.join(__dirname, 'test.js');
to
const filePath = path.resolve('.', 'script/test.js');
and put my script file into folder called script (name doesn't matter) in the main directory

Accessing file in public folder through capacitor

I'm using ionic-v5, capactior and react to build an app.
I have a file (data.json) stored inside of my public/ folder.
I simply want to be able to load that file in and store it as an object.
So far I have tried:
import { Filesystem, FilesystemEncoding } from '#capacitor/core'
let contents = await Filesystem.readFile({
path: "data.json",
encoding: FilesystemEncoding.UTF8,
})
import { HTTP } from '#ionic-native/http';
let response = await HTTP.get("file://data.json", {}, {});
ret = response.data;
return ret;
I have also looked at https://ionicframework.com/docs/native/file but the documentation is poor to say the least.
Along with this I have tried pre-pending /android_asset/public to all of the paths but no luck (I know it would only work on Android, I just wanted to get something).
If you're using Ionic React (v5) and you just want to access a file in /myapp/public, you don't need Capacitor.
You can use axios (or fetch).
Folder structure:
/myapp
/assets
/json
/myFile.json
Sample code:
// https://stackoverflow.com/a/52570060
export const fetchJsonFile = (fileName: string): Promise<any> => (
axios.get(`./json/${fileName}`)
.then((response) => response).catch((error) => {
Promise.reject(error);
})
);

create react app Configuration file after build app

I want a Config File (JSON) in root folder after build to config my app.
like Translation and API Urls and ...
Can I do this with create react app?
Create config.js or json file outside src directory and include it in index.html like
<script src="%PUBLIC_URL%/config.js" type="text/javascript"></script>
configure parameters in config.js
config.js
var BASE_URL = "http://YOUR-URL";
you can get paramenters like
const BASE_URL = window.BASE_URL;
You can store you JSON file in the public/ folder and it'll automatically provide this file when you host your Create React App.
Something like: /public/my-configuration-file.json
then when you restart your application:
localhost:3000/my-configuration-file.json
will provide you this json file.
You could create a custom hook that reads a "public" config file using fetch.
// This path is relative to root, e.g. http://localhost/config.json
const configFile = './config.json'
export function useConfig() {
const [config, setConfig] = useState(initialConfig);
useEffect(() => {
(async function fetchConfig() {
try {
const response = await (await fetch(configFile)).json();
setConfig(response);
} catch (e) {
console.log(e);
}
}());
}, []);
return config;
}
Then use it anywhere in you app
function App() {
const config = useConfig();
return (
<div>{config.foo}</div>
);
}
You'll always have an up to date non-cached version of it's data.
updating this topic with a brand new package that is available now that brings the joys of .Net Configuration to the JavaScript world: wj-config.
This package is pretty much an exact answer to what you need. Read this blog post for more information.
It is incredible to me how during over 6 years nobody filled in this gap in React (and JavaScript in general). Anyway, give wj-config a try. I think it will be a positive experience.

Resources