1.5.1 • Published 6 years ago

cra-firebase v1.5.1

Weekly downloads
3
License
MIT
Repository
github
Last release
6 years ago

Build Status Coverage Status

cra-firebase

This is a helper library which helps to implement server-side rendering (SSR) of project started with help of create-react-app (CRA) in google's firebase (FBS)

The idea

The capabilities which provide both create-react-app and firebase speeds up development of a product and removes lots of headaches, but they could do even more together.

'Firebase cloud functions' allows to implement server-side rendering, but our code needs to be prepared for the server beforehand. Two major requirements are:

  • compatible with node v6.11.5
  • jsx should be transpiled into plain js

Also, we need to share some code between server and client. And it is good to write in one style (i.e. in es6/7).

So, in order to ease achievement of all these goals you can use this tool. Basically, this utility uses babel with necessary presets to transpile server and shared code into compatible with node 6.

It also can provide you with initial source code to see how it work.

How to use

New project

  1. Init project with help of create-react-app (CRA)
  2. Install firebase-tools as a npm devDependency or a global module - this is up to your personal taste: yarn add -D firebase-tools or npm i -D firebase-tools or npm i -G firebase-tools
  3. Init project with firebase yarn firebase init or $(npm bin)/firebase init or firebase init
  4. During initialization check functions and hosting options. Optionally, also check firestore - example code provides a graphql on top of firestore.
  5. As a hosting dir specify build
  6. Add cra-firebase (e.g. this utility) as a devDependency: yarn add -D cra-firebase or npm i -D cra-firebase
  7. Run yarn cra-firebase init -y or $(npm bin)/cra-firebase init -y

Existing projects

  1. Add cra-firebase (e.g. this utility) as a devDependency: yarn add -D cra-firebase or npm i -D cra-firebase
  2. Update your package.json scripts by either:
  • A) Add command into package.json scripts which will execute this line cra-firebase build
  • B) run yarn cra-firebase init to get step-by-step process of updating your project

Commands

  • init - initialize project.
  • build - builds ypu project.
  • start - watches your src files and transforms them into node 6 compatible code.
  • version - outputs current version of this utility.

Description of the commands

What happens when you run the init -y command:

  1. Modifies your project's package.json file and replaces CRA's build script with own. Don't worry: during the running of cra-firebase build, it also runs CRA build scripts.
  2. Deals with rewrite rules in your firebase.json:
    • If during firebase initialization process you chosed SPA option this script replaces default rewrite rule which point to index.html to point to a function app.
    • If no appropriate rule was found it adds one.
    • If a correct rule already present (i.e. points to app function) - just skips this step.
    • Also, if you already defined your own rule for '**' route - it will respect it and will skip this step.
  3. Appends to your .gitignore file list of dirs and files which are dynamically generated by cra-firebase. This step respects your firebase config. Plese note, that this step doesn't check if ignores are already present in your .gitignore.
  4. Inserts seed code into your project.

Or You can run init without -y flag - in this case you'll be asked about each step.

What happens when you run the build command

  1. Sets BABEL_ENV to production - a requirement of the react-app babel preset. It also saves a previous value and restores it in the end.
  2. Deletes CRA build directory, and files which cra-firebase generates.
  3. Runs CRA 'build' script.
  4. Transpiles all code that should go into firebase functions.
  5. Copies index.html content into markup.js.
  6. Deletes build/index.html. This is crucial to make SSR to work. Otherwise, firebase will ignore your rewrite rule.
  7. Copies dependencies from your root package.json into functions's
  8. runs npm i inside functions dir
  9. Sets BABEL_ENV to the original value.

Serving firebase cloud functions locally

run one of:

yarn build yarn firebase deploy yarn firebase serve --only functions,hosting

or

npm run build $(npm bin)/firebase deploy $(npm bin)/firebase serve --only functions,hosting

Please keep in mind that firebase hosting will serve static files from your build directory, so upon client hydration you'll finish with old version of your app.

In feature I hope to add a functionality to serve fresh version of static files. I suspect that this requires to run a webpack with CRA build config in parallel with local firebase emulation.

About directories and files structure:

CRA provides to us a slightly rigid structure of our code: all source code should be in src dir. And there is no way to tell CRA to handle other structure. That's why I suggest supporting this idea further in a manner of putting structure into a structure like this:

├── src/
│   ├── client/
│   │   └── clientOnly.jsx
│   │
│   ├── shared/
│   │   └── components/
│   │       └── app.jsx
│   │
│   ├── server/
│   │   └── serverOnly.js
│   │
│   ├── index.js
│   └── server.index.js
│

This utility respects a custom name of firebase's dir structure looks into your firebase.json for a name of your functions dir. Here a quote from FBS docs:

By default, the Firebase CLI looks in the functions/ folder for the source code. You can specify another folder by adding the following lines in firebase.json:

"functions": {
  "source": "another-folder"
}

So, all transpiled code from src will be written into proper functions dir. server.index.js will be transpiled and output into index.js under functions dir.

Another vital point to implement SSR is to provide to your server a code with initial markup. Since CRA generates bundles with dynamic names we need to know their names to serve correct HTML with SSR. So, as a result of a build process, you will have a markup.js file in your functions dir. This is a file which exports a single function as a default export. This function accepts single argument which is expected to be a result of a react-dom/server.renderToString call.

Basically, in your server.index.js you can write next:

import { https } from 'firebase-functions'
import { renderToString } from 'react-dom/server'
import React from 'react'
import markup from './markup'
import App from './shared/components/App'

const app = https.onRequest((req, res) => {
  const apphtml = renderToString(<App />)
  res.status(200).send(markup(apphtml))
})

export { app }

As I understand from firebase documentation you need a named export in your functions/index.js even if you exporting only one function. This makes sense to me because you can do more than just SSR. By the way: you can use this tool to write cloud functions code using modern ES syntax.

⚠️ WARNING: you must destrucure firebase-functions import because there is no default export (read more here)

About markup.js

Because CRA setup of webpack genarates each time assets with dynamic names we need to know those names. And the only availiable approach is to read build/index.html and transform it into function which will accapt additional content and return whole markup as a string. Finally, we can send this string to a user.

markup.js exports a single function as a default export.

This function has 3 arguments: 1. content - required. expects a result of renderToString or similar function, 2. scriptsGlabals - optional. Expects a object, which transformed into additional in the bottom of a html. keys becomes names for global variables with corresponding values. example:

{
  'window.__APOLLO_STATE__': 'yourstate',
  otherKey: 'bla-bla-bla',
}

transforms into:

<script charset="UTF-8">
  window.__APOLLO_STATE__="yourstate";
  otherKey="bla-bla-bla";
</script>
  1. headNodes - optional. Expects a string of html nodes(tags) which will be injected into the markup. For example, you can inject styles or additional meta. Please note, that headNodes will be injected right before </head> closing tag. In another words: it will be the last in the head.

Usage example:

// in server.index.js
import { https } from 'firebase-functions'
import { renderToString } from 'react-dom/server'
import React from 'react'
import markup from './markup'
import App from './shared/components/App'

const app = https.onRequest((req, res) => {
  const apphtml = renderToString(<App />)
  const globalVars = {
    myGlobalVar: 'globalVarValue'
  }
  const injectIntoHead = '<meta charset="utf-8">'
  const finalMarkup = markup(apphtml, globalVars, injectIntoHead)
  res.status(200).send(finalMarkup)
})

export { app }

Handling more filetypes and/or syntaxes

Adding more babel presets and plugins

By default, cra-firebase uses next presets:

  • env - with targeting node: '6.11.1',
  • react-app - same as CRA
  • flow

You can specify more babel presets and plugins in order to deal with new file formats or for some additional features. You can do it via CLI commands, .babelrc file at the root of your project, or in package.json in babel field.

CLI commands are:

  • presets: --presets=preset-name,other-preset
  • plugins: --plugins=plugin-name,other-plugin,more-one-plugin,list-may-continue

At the current state, this utility doesn't handle declaration of presets/plugins with options, because it doesn't dedupe such declarations. But simple declarations (without options) works.

"presets": [
  ["env", {"targets": { "node": 8}}] // will not be deduped
  "react" // will be deduped
]

Please note that this utility is interested in plugins and presets sections configuration of babel, other parts are ignored. It does NOT look into any env option, only default one.

Adding more filetypes into filtering

you can specify in package.json or in .crafirebaserc.json or via CLI command option more filetypes which you want to add/exclude to a process of babel transformation.

In configuration files values must be arrays of strings.

An example of package.json:

"crafirebase": {
  "exclude": ["tmp.ts", "tmp.js"],
  "include": [".ts", ".filetypeofyourtaste"]
}

An example of .crafirebaserc.json:

"exclude": ["tmp.ts", "tmp.js"],
"include": [".ts", ".filetypeofyourtaste"]

The examples of CLI commands:

  • exclude: --exclude=.sh,.tmp
  • include: --include=.txt,.ts

Seed code

I've build some basic seed code which you can use as a starter for your project.

It has graphQL with help of apollo, styled-components and SSR.

It requires Firestore to be enabled. Don't worry - I've made seed code to be clever enough to detect if Firestore is enabled and it will notify you. And it will provide you with an a option to fill your Firestore with example data.

After Firestore was populated I recommend you to delete a file src/server/utils/fillDb.js and line 18 in src/server.index.js. It is expressApp.get('/api/seeddb', fillDb).

Known problems

Problem of serving firebase locally and development: right now I can't figure how to toss CRA dev tools into firebase serve functionality. So right now you have a localhost:3000 for front-only and localhost:5000 for back-end only.

Assets like SVG and css aren't fit well within server. Because node expects plain javascript.

You can add additional babel transformers and file designated to be transformed (instructions below). But bear in mind that during transformation import statements must be altered to address proper filenames. i.e. if in your src

import icon from 'icon.svg'

In your output file should be something like this

const icon = require('icon')

Alternative solutions:

for SVG: convert SVG into plain react components with any tool. For example SVGR.

And for styles use something more suitable into SSR.

Configuration

This utility allowed to be configured via .crafirebaserc.json or a crafirebase section in root's package.json

What could be configured:

config fielddefault valuedescription
indexserver.index.jsfilename of source of server's index file
include'.js', '.jsx', '.svg'file extensions which should be transformed with help of babel and moved into functions. Default values are NOT overwritten, which means they will be merged
exclude'spec.js', 'test.js'file extensions which should be excluded from copy/transform. Default values are NOT overwritten, which means they will be merged
1.5.1

6 years ago

1.5.0

6 years ago

1.4.0

6 years ago

1.3.1

6 years ago

1.3.0

6 years ago

1.2.0

6 years ago

1.1.3

6 years ago

1.1.2

6 years ago

1.1.1

6 years ago

1.1.0

6 years ago

1.0.0

6 years ago