Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 898
Eckhardt Avatar asked May 26 '19 17:05

Eckhardt


People also ask

How do I fix attempted import error firebase app does not contain a default export imported as Firebase <UNK>?

To Solve Attempted import error: 'firebase/app' does not contain a default export (imported as 'firebase') Error If You are using Version 9 then don't forget that things changed a bit for importing firebase Now there is a “compatibility” option so can use the /compat folder in your imports.


Video Answer


3 Answers

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>
like image 87
Eckhardt Avatar answered Oct 18 '22 05:10

Eckhardt


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,
  },
};

like image 39
Tiago Nobrega Avatar answered Oct 18 '22 05:10

Tiago Nobrega


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);
    }
  }
like image 3
zakaria chahboun Avatar answered Oct 18 '22 03:10

zakaria chahboun