SSR on Firebase Hosting with GC Functions not working - firebase

Ok so this is my folder Structure
So here is the Functions Index File:
const functions = require('firebase-functions')
const express = require('express')
const { Nuxt } = require('nuxt')
const app = express()
const config = {
dev: false,
buildDir: 'nuxt',
build: {
publicPath: '/'
}
}
const nuxt = new Nuxt(config)
function handleRequest (req, res) {
res.set('Cache-Control', 'public, max-age=600, s-maxage=1200')
nuxt.renderRoute('/').then(result => {
res.send(result.html)
}).catch(e => {
res.send(e)
})
}
app.get('*', handleRequest)
exports.nuxtApp = functions.https.onRequest(app)
But all I get when visiting the Url is "{"code":"MODULE_NOT_FOUND"}
(after deploying)
All i did in the nuxt.config.js is just telling it to make the build directory into the nuxt folder in the functions folder
firebase.json
{
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"function": "nuxtApp"
}
]
}
}
When testing locally with Firebase Serve it works but it only renders the base url / and nothing else and also I have no Static Assets like my scss files or the app manifest.

After a few days of debugging I found a solution.
At first, you have to extend your error logging, so you can see the stack-trace:
console.error(e)
res.send(e)
My errors were:
error#1 firebase package was not installed in my functions folder, so I had to install it with npm install --save firebase in the functions directory. The overall firebase package is not required by Cloud Functions, however it's needed for my nuxt project for firestore usage
error#2 You could get an error like firebaseApp.firestore is not a function. It's due to a wrong import of firebase to you could function. I found the solution for this problem here
I change this import:
import firebase from 'firebase';
import 'firebase/firestore';
To this:
import firebase from '#firebase/app';
import '#firebase/firestore'
After solving these two errors, my NuxtJs app worked well with Firebase Cloud Functions.

Related

Using Firebase Functions with Nuxt 3

Environment
Operating System: macOS 10.15.7
Node Version:v16.14.2
Nuxt Version: 3.0.0-rc.2
Firebase: 9.7.0
firebase-admin: 10.2.0
firebase-functions: 3.21.0
firebase-functions-test: 0.3.3
In firebase.json the following config is set:
{
"functions": { "source": ".output/server" }
}
I have a file under the "server" directory containing the following function:
import * as functions from "firebase-functions";
export const helloWorld = functions.https.onRequest((request, response) => {
functions.logger.info("Hello logs!", {structuredData: true});
response.send("Hello from Firebase!");
});
When I run:
NITRO_PRESET=firebase npm run build
firebase emulators:start --only functions
then go to my firebase emulator log, it does not show the new helloWorld() function being initialized. Also, when going to "http://localhost:5001/$PROJECTNAME/us-central1/helloWorld", it returns "Function us-central1-helloWorld does not exist, valid functions are: us-central1-server" which suggests that my function has not been initialized.
Is there any way I can write firebase cloud functions in my Nuxt 3 app from files in my server directory?
I saw a similar discussion here that said it was possible to change the nuxt.config.ts functions object between deploying functions,storage,firestore and deploying server and hosting. I am trying to write firebase functions solely in the "server" directory without creating a "functions" directory and the root of my project. Is this possible?
I have also opened a discussion on GitHub here
Unfortunately, the procedure you are following has some points to highlight. As previously mentioned on this thread:
The Vue.js app is a front-end component (even if it is hosted in a cloud service like Firebase Hosting).
The Cloud Functions are serverless back-end components, hosted in the Firebase (Google Cloud) infrastructure and reacting to events.
To get these two components interacting with each other there are basically two possibilities:
For Callable Cloud Functions and HTTPS Cloud Functions, you will call them from your Vue.js app.
For background triggered Cloud Functions (e.g. triggered by a Firestore event like doc creation), the Vue.js front-end could generate the event (e.g. write to Firestore) and/or listen to the result of a Cloud Function execution (e.g. a Firestore doc is modified).
…
As explained in the documentation, to call the Callable Function from your Vue.js app, you need to do as follows (with the JS SDK v9):
Add Firebase to your Vue.js app. For example via a firebaseConfig.js file:
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getFunctions } from "firebase/functions";
const firebaseConfig = {
apiKey: "...",
// ....
};
const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);
const functions = getFunctions(firebaseApp);
export { db, functions };
Then, in your component, you do
<script>
import { functions } from '../firebaseConfig';
import { httpsCallable } from 'firebase/functions';
// ...
methods: {
async callFunction() {
const addMessage = httpsCallable(functions, 'addMessage');
const result = await addMessage({ text: messageText })
const data = result.data;
//...
});
}
</script>
I also tried to reproduce the issue, and I successfully deployed the functions on the emulator with the same approach from the question, following the documentation from GCP on how to add Firebase to your project, and using this Youtube tutorial as a guide, it has some important tips on how to add Firebase to a NuxtJS project.
I will leave a sample on how my firebase.json file ended up looking once all the set-up was finished:
{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"functions": {
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint"
],
"source": ".output/server"
},
"hosting": {
"site": "<your_project_id>",
"public": ".output/public",
"cleanUrls": true,
"rewrites": [{ "source": "**", "function": "server" }],
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
Additionally, I would like to suggest using the Firebase CLI, as it has more accessibility features, and check this Medium guide to take a deeper look at how to add Firebase to Nuxt.

NextJS Firebase onRequest Function Rewrites

I am working on a project using the Vercel Next.js firebase-with-hosting template found here.
The template uses a Firebase Function to allow for Firebase Hosting. I am trying to add additional Firebase onRequest functions so that I can send request to the server.
I've added the printTest function and set the rewrites in the firebase.json file but keep getting 404 errors. I've successfully implemented this before with Express and without NextJS. I think I have an issue with my rewrites.
I've also tested onCallable, and pubSub functions and these worked great. My guess is I have a rewrite issue. Any help with understanding why this does now work would be greatly appreciated.
const { join } = require('path'); // From NextJS Vercel Base Build
const { default: next } = require('next'); // From NextJS Vercel Base Build
const isDev = process.env.NODE_ENV !== 'production'; // From NextJS Vercel Base Build
const nextjsDistDir = join('src', require('./src/next.config.js').distDir); // From NextJS Vercel Base Build
const admin = require('firebase-admin'); // Firebase Admin SDK for NodeJS.
const functions = require('firebase-functions'); // For NextJS + Firebase Functions + Firebase Hosting.
const serviceAccount = require('./firebaesAdminServiceAccountKey.json'); // Service account key for Firebase Admin SDK.
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++ Firebase Admin Initialization ++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++ Firebase Firestore Initialization ++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
const dba = admin.firestore();
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++ Nextjs Configuration ++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
const nextjsServer = next({
dev: isDev,
conf: {
distDir: nextjsDistDir,
},
});
const nextjsHandle = nextjsServer.getRequestHandler();
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++ Cloud Functions ++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Nextjs Cloud Function to allow for Firebase Hosting.
exports.nextjsFunc = functions.https.onRequest((req, res) => {
return nextjsServer.prepare().then(() => nextjsHandle(req, res));
});
exports.printTest = functions.https.onRequest((req, res) => {
console.log('THIS WORKS!');
res.status(200).send();
});
Here is the firebase.json file with the rewrite.
{
"hosting": {
"public": "public",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"function": "nextjsFunc"
},
{
"source": "/printTest",
"function": "printTest"
}
]
},
"functions": {
"source": ".",
"predeploy": [
"npm --prefix \"$PROJECT_DIR\" install",
"npm --prefix \"$PROJECT_DIR\" run build"
],
"runtime": "nodejs10"
}
}
The URL pattern ** means that all of your requests are sent to nextjsFunc() and that includes /printTest. Since that is the first rewrite config rule, anything below is ignored.
From the doc, here's an important note:
Hosting applies the rewrite defined by the first rule with a URL pattern that matches the requested path. So, you need to deliberately order the rules within the rewrites attribute.
Fix the issue by changing the order and putting the least-strict rule at the end:
"rewrites": [
{
"source": "/printTest",
"function": "printTest"
},
{
"source": "**",
"function": "nextjsFunc"
}
]

Firebase redirects to deleted function

I am new to Firebase and is working on a project with nuxtjs, Firestore, Firebase functions, and Firebase hosting. I deployed a function that does server side rendering named 'nuxtssr' and it worked after deployment. But then I noticed that the default region of the function is in the US. I wanted to deploy the function to Europe West so I deleted the 'nuxtssr' function and deployed a new function 'nuxtssrEurope' with region set to Europe West. But after that, when I try to access my site through browser, it redirects to this page asking to verify myself.
https://accounts.google.com/ServiceLogin/webreauth?service=ah&passive=true&continue=https%3A%2F%2Fappengine.google.com%2F_ah%2Fconflogin%3Fcontinue%3Dhttps%3A%2F%2Fus-central1-example.cloudfunctions.net%2Fnuxtssr%2F&flowName=GlifWebSignIn&flowEntry=ServiceLogin
As you can see, the redirection is to the function 'nuxtssr' which I deleted which used to reside in US Central. When I verify myself, I get redirected and get this message from https://us-central1-example.cloudfunctions.net/nuxtssr/
Error: Forbidden
Your client does not have permission to get URL /nuxtssr/ from this server.
I looked around but could not find an answer. And yes I checked for any typos. This is my function:
const functions = require('firebase-functions')
const { Nuxt } = require('nuxt')
const express = require('express')
const app = express()
const config = {
dev: false
}
const nuxt = new Nuxt(config)
let isReady = false
const readyPromise = nuxt
.ready()
.then(() => {
isReady = true
})
.catch(() => {
process.exit(1)
})
async function handleRequest(req, res) {
if (!isReady) {
await readyPromise
}
res.set('Cache-Control', 'public, max-age=600, s-maxage=1200')
await nuxt.render(req, res)
}
app.get('*', handleRequest)
app.use(handleRequest)
exports.nuxtssrEurope = functions.region('europe-west1').https.onRequest(app)
My firebase.json
{
"hosting": {
"public": "public",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"function": "nuxtssrEurope"
}
]
}
}
Apparently. Cloud Functions only supports serving dynamic content for Firebase hosting on us-central1 only, so you can't use other servers for Cloud Functions. After I changed back to us-central1 for my Cloud Function, it works now.
If you are using HTTP functions to serve dynamic content for Firebase Hosting, you must use us-central1.
https://firebase.google.com/docs/hosting/functions

How to import Firebase only on client in Sapper?

I'm importing Firebase into my Sapper application, I do not want the imports to be evaluated on the server. How do I make sure imports are only on the client-side?
I am using Sapper to run sapper export which generates the static files. I have tried:
Creating the firebase instance in it's own file and exported the firebase.auth() and firebase.firestore() modules.
Trying to adjust the rollup.config.js to resolve the dependencies differently, as suggested from the error message below. This brings more headaches.
Creating the Firebase instance in client.js. Unsuccessful.
Creating the instance in stores.js. Unsuccessful.
Declaring the variable and assigning it in onMount(). This causes me to have to work in different block scopes. And feels a bit hacky.
The initialization of the app, works fine:
import firebase from 'firebase/app'
const config = {...}
firebase.initializeApp(config);
I have also discovered that if I change the import to just import firebase from 'firebase' I do not get this server error:
#firebase/app:
Warning: This is a browser-targeted Firebase bundle but it appears it is being run in a Node environment. If running in a Node environment, make sure you are using the bundle specified by the "main" field in package.json.
If you are using Webpack, you can specify "main" as the first item in
"resolve.mainFields": https://webpack.js.org/configuration/resolve/#resolvemainfields
If using Rollup, use the rollup-plugin-node-resolve plugin and set "module" to false and "main" to true: https://github.com/rollup/rollup-plugin-node-resolve
I expected to just export these firebase functionalities from a file and import them into my components like:
<script>
import { auth } from "../firebase";
</script>
But as soon as that import is include, the dev server crashes. I don't want to use it on the server, since I'm just generating the static files.
Does anyone have some ideas on how to achieve importing only on client side?
So I have spent too much time on this. There isn't really a more elegant solution than onMOunt.
However, I did realize that sapper really should be used for it's SSR capabilities. And I wrote an article about how to get set up on Firebase with Sapper SSR and Cloud Functions:
https://dev.to/eckhardtd/how-to-host-a-sapper-js-ssr-app-on-firebase-hmb
Another solution to original question is to put the Firebase CDN's in the global scope via the src/template.html file.
<body>
<!-- The application will be rendered inside this element,
because `app/client.js` references it -->
<div id='sapper'>%sapper.html%</div>
<!-- Sapper creates a <script> tag containing `app/client.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
<!-- Insert these scripts at the bottom of the HTML, but before you use any Firebase services -->
<!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/6.0.4/firebase-app.js"></script>
<!-- Add Firebase products that you want to use -->
<script src="https://www.gstatic.com/firebasejs/6.0.4/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.0.4/firebase-firestore.js"></script>
</body>
</html>
and in the component:
<script>
import { onMount } from 'svelte';
let database, authentication;
onMount(() => {
database = firebase.firestore();
authentication = firebase.auth();
});
const authHandler = () => {
if (process.browser) {
authentication
.createUserWithEmailAndPassword()
.catch(e => console.error(e));
}
}
</script>
<button on:click={authHandler}>Sign up</button>
I was able to import firebase using ES6. If you are using rollup you need to consfigure namedExports in commonjs plugin:
//--- rollup.config.js ---
...
commonjs({
namedExports: {
// left-hand side can be an absolute path, a path
// relative to the current directory, or the name
// of a module in node_modules
'node_modules/idb/build/idb.js': ['openDb'],
'node_modules/firebase/dist/index.cjs.js': ['initializeApp', 'firestore'],
},
}),
The you can use it like this:
//--- db.js ---
import * as firebase from 'firebase';
import 'firebase/database';
import { firebaseConfig } from '../config'; //<-- Firebase initialization config json
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
export { firebase };
// Initialize db
export const db = firebase.firestore();
and maybe use it in a service like such:
// --- userService.js ----
import { db } from './common';
const usersCol = db.collection('users');
export default {
async login(username, password) {
const userDoc = await usersCol.doc(username).get();
const user = userDoc.data();
if (user && user.password === password) {
return user;
}
return null;
},
};
EDITED
Full rollup config
/* eslint-disable global-require */
import resolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';
import commonjs from 'rollup-plugin-commonjs';
import svelte from 'rollup-plugin-svelte';
import babel from 'rollup-plugin-babel';
import { terser } from 'rollup-plugin-terser';
import config from 'sapper/config/rollup';
import { sass } from 'svelte-preprocess-sass';
import pkg from './package.json';
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const legacy = !!process.env.SAPPER_LEGACY_BUILD;
// eslint-disable-next-line no-shadow
const onwarn = (warning, onwarn) =>
(warning.code === 'CIRCULAR_DEPENDENCY' && warning.message.includes('/#sapper/')) || onwarn(warning);
export default {
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode),
}),
svelte({
dev,
hydratable: true,
emitCss: true,
preprocess: {
style: sass(),
},
}),
resolve({
browser: true,
}),
commonjs({
namedExports: {
// left-hand side can be an absolute path, a path
// relative to the current directory, or the name
// of a module in node_modules
'node_modules/idb/build/idb.js': ['openDb'],
'node_modules/firebase/dist/index.cjs.js': ['initializeApp', 'firestore'],
},
}),
legacy &&
babel({
extensions: ['.js', '.mjs', '.html', '.svelte'],
runtimeHelpers: true,
exclude: ['node_modules/#babel/**'],
presets: [
[
'#babel/preset-env',
{
targets: '> 0.25%, not dead',
},
],
],
plugins: [
'#babel/plugin-syntax-dynamic-import',
[
'#babel/plugin-transform-runtime',
{
useESModules: true,
},
],
],
}),
!dev &&
terser({
module: true,
}),
],
onwarn,
},
server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
'process.browser': false,
'process.env.NODE_ENV': JSON.stringify(mode),
}),
svelte({
generate: 'ssr',
dev,
}),
resolve(),
commonjs(),
],
external: Object.keys(pkg.dependencies).concat(require('module').builtinModules || Object.keys(process.binding('natives'))),
onwarn,
},
serviceworker: {
input: config.serviceworker.input(),
output: config.serviceworker.output(),
plugins: [
resolve(),
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode),
}),
commonjs(),
!dev && terser(),
],
onwarn,
},
};
The clean way is to use the Dynamic Import as the documentation said: Making a component SSR compatible
The way to get around this is to use a dynamic import for your component, from within the onMount function (which is only called on the client), so that your import code is never called on the server.
So here for example we want to import the core of firebase and the authentication package too.
<script>
let firebase;
onMount(async () => {
const module = await import("firebase/app");
await import("firebase/auth");
firebase = module.default;
firebase.initializeApp(firebaseConfig);
});
<script>
And now you can use firebase object as you can, for example we want to login with email and password:
let email;
let password;
async function login() {
try {
let result = await firebase.auth().signInWithEmailAndPassword(
email,
password
);
console.log(result.user);
} catch (error) {
console.log(error.code, error.message);
}
}
In order to use Firebase with Sapper, you have to import firebase not firebase/app. You do want firebase to be able to load correctly with SSR on the backend, not just the frontend. If you have some metatags, for example, that would be stored in the database, you want them to load on the backend (UNTESTED).
You could just use firebase, but then you get the annoying console warning. Remember also firebase loads ALL firebase dependencies while firebase/app does not, that is why you don't want to use it on the frontend. There is probably a way with admin-firebase, but we want to have less dependencies.
Do not use rxfire at all. You don't need it. It causes errors with Sapper. Just plain Firebase.
firebase.ts
import firebase from 'firebase/app';
import "firebase/auth";
import "firebase/firestore";
import * as config from "./config.json";
const fb = (process as any).browser ? firebase : require('firebase');
fb.initializeApp(config);
export const auth = fb.auth();
export const googleProvider = new fb.auth.GoogleAuthProvider();
export const db = fb.firestore();
Firebase functions require an extra step and you must enable dynamic imports. (UNTESTED)
export const functions = (process as any).browser ? async () => {
await import("firebase/functions");
return fb.functions()
} : fb.functions();
While this compiles, I have not tried to run httpsCallable or confirmed it will load from the database on the backend for seo ssr from the db. Let me know if it works.
I suspect all of this will work with the new SvelteKit now that Sapper is dead.

Firebase Cloud Functions + Hosting - 404 Not Found

I'm building my web app on top of Firebase cloud functions, and hosting. My client was built with create-react-app, and I have a small express server running in a cloud function that serves the app build.
Everything works fine in a normal browser window however, I am having an issue where if I go to any url that is not '/' in incognito mode or a mobile device I get a 404 and "Not Found"
ie: hitting "https://144blocks.com/week" directly will load fine in a normal browser window but throws the following error in incognito and mobile.
My firebase function for serving the site is:
functions/index.js
const functions = require('firebase-functions');
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.get('/*', (req, res) => {
const fullPath = path.normalize(__dirname + '/../react-ui/build/index.html');
res.set('Cache-Control', 'public, max-age=300, s-maxage=600');
res.sendFile(fullPath);
});
exports.app = functions.https.onRequest(app);
In my client my firebase.json file:
firebase.json
{
"hosting": {
"public": "react-ui/build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"function": "app"
}
]
}
}
I'm not sure why I can't hit any route directly aside from '/' in an incognito window or a mobile device. Ideally, I want all my routes to flow through the one app.get(/* on my express server and have my client handle the routing.
There is currently a problem with Firebase Hosting, which appears to be affecting a number of sites, including two of my own. You can stay up-to-date with their status updates via this link.
I am still not 100% sure as to why this is working however I fixed the issue by:
1. In my functions/index.js file, updating the fullPath because I believe that the __dirname + '/../react-ui/build/index.html' wasn't resolving.
My name functions/index.js file:
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
const app = express();
app.get('/*', (req, res) => {
const fullPath = path.normalize(__dirname + '/build/index.html');
res.set('Cache-Control', 'public, max-age=300, s-maxage=600');
res.sendFile(fullPath);
});
exports.app = functions.https.onRequest(app);
On my build process, I'm moving the build directory into the functions folder to get deployed to Firebase with the functions so my project directory when I'm getting ready to deploy looks like where the build directory in functions and react-ui is identical:
If anyone has more insight into why this is exactly working I would appreciate additional information.
Thanks!
Try running firebase login. I had this exact issue and it was because my login session had expired.
Alternatively, try firebase login --no-localhost if you're having trouble with the standard login.
If you're using ci/cd, use firebase login:ci --no-localhost, save your ci token to your environment under FIREBASE_TOKEN and deploy with firebase deploy --only functions --token $FIREBASE_TOKEN.

Resources