cra-firebase v1.5.1
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
- Init project with help of create-react-app (CRA)
- Install firebase-tools as a npm devDependency or a global module - this is up to your personal taste:
yarn add -D firebase-tools
ornpm i -D firebase-tools
ornpm i -G firebase-tools
- Init project with firebase
yarn firebase init
or$(npm bin)/firebase init
orfirebase init
- During initialization check
functions
andhosting
options. Optionally, also checkfirestore
- example code provides a graphql on top of firestore. - As a hosting dir specify
build
- Add cra-firebase (e.g. this utility) as a devDependency:
yarn add -D cra-firebase
ornpm i -D cra-firebase
- Run
yarn cra-firebase init -y
or$(npm bin)/cra-firebase init -y
Existing projects
- Add cra-firebase (e.g. this utility) as a devDependency:
yarn add -D cra-firebase
ornpm i -D cra-firebase
- Update your
package.json
scripts
by either:
- A) Add command into
package.json
scripts
which will execute this linecra-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:
- Modifies your project's
package.json
file and replaces CRA'sbuild
script with own. Don't worry: during the running of cra-firebase build, it also runs CRA build scripts. - 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 functionapp
. - 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.
- If during firebase initialization process you chosed SPA option this script replaces default rewrite rule which point to
- 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
. - 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
- 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. - Deletes CRA
build
directory, and files which cra-firebase generates. - Runs CRA 'build' script.
- Transpiles all code that should go into firebase functions.
- Copies
index.html
content intomarkup.js
. - Deletes
build/index.html
. This is crucial to make SSR to work. Otherwise, firebase will ignore your rewrite rule. - Copies dependencies from your root package.json into functions's
- runs
npm i
inside functions dir - 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>
- 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 field | default value | description |
---|---|---|
index | server.index.js | filename 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 |