Intro
Hello to the 30 people out there who still useEmber 👋🐿
Just kidding - I know the number is higher than 30, but in a world dominated by React, Angular, and Vue, it seems like we who develop with Ember (either by choice [really?!?] or by career happenstance), are pretty alone - especially in terms of useful and helpful material.
That's part of the reason why when faced with the task of addingFastboot (Ember's version of server-side rendering) to a project riddled with jQuery, along with the demand that all the jQuery remain functional, it took me quite a heavy amount of blood, sweat, and tears to get things working.
As such, I'll share here a nice little trick I learned along the way in case any other poor soul finds themself in the dark shadows that is Ember Fastboot development.
What's the issue?
When adding Fastboot to an Ember project that makes heavy use of addons that in turn utilize third-party client-side JS libraries (typically jQuery), you will quickly find out that your project will have a hard time rendering on the server if you don't make some drastic changes. This is simply due to the project being unable to build and render client-side JS within the server (ie node) environment.
This leaves us with a few options. First, we can simply gut all the incompatible client-side JS logic and / or use node-compatible equivalents. A common example of this is usingember-fetch
instead ofjQuery.ajax
. Second, we can hope that the maintainer(s) of the addon in question has taken notice of the Fastboot issue and made their library Fastboot compatible.
Unfortunately, there are inherent problems with both of these options. First, often a node-compatible equivalent simply doesn't exist. Second, often the maintainer of a library's idea of making their library Fastboot compatible looks something like this:
if(process.env.EMBER_CLI_FASTBOOT){return;}
...which, aside from being broken (this test always fails, asEMBER_CLI_FASTBOOT
does not exist inprocess.env
as far as I can tell), essentially only does one thing - which is to simply not import the library into the application. This means that when the app finally makes it to the browser, the library will not be there 😑
We want the best of both worlds. We want the offending addon to be loaded into Fastboot but its client-side code not evaluated until it reaches the browser.
What's the solution?
The most streamlined and bulletproof solution I've found so far is acting as if you yourself are the maintainer of the library. In essence, you must become one with the maintainer and realign the inner zen of the library - also known as making some changes to the library'sindex.js
😁
As noted in theFastboot Addon Author Guide, if your addon includes third-party code that is incompatible with node / Fastboot, you can add a guard to yourindex.js
that ensures it is only included in the browser build. This is achieved by creating separate build tree specifically for the browser.
Unfortunately, the Fastboot guide falls short in its given example of actually implementing such a guard. So we will give a more thorough and real-world example here.
Being Slick(er)
Let's say we want to use the addonember-cli-slick
, which is essentially an Ember port of theSlick Slider plugin. The addon'sindex.js
looks like this:
'use strict';constpath=require('path');module.exports={name:require('./package').name,blueprintsPath:function(){returnpath.join(__dirname,'blueprints');},included:function(app){this._super.included(app);app.import('node_modules/slick-carousel/slick/slick.js');app.import('node_modules/slick-carousel/slick/slick.css');app.import('node_modules/slick-carousel/slick/slick-theme.css');app.import('node_modules/slick-carousel/slick/fonts/slick.ttf',{destDir:'assets/fonts'});app.import('node_modules/slick-carousel/slick/fonts/slick.svg',{destDir:'assets/fonts'});app.import('node_modules/slick-carousel/slick/fonts/slick.eot',{destDir:'assets/fonts'});app.import('node_modules/slick-carousel/slick/fonts/slick.woff',{destDir:'assets/fonts'});app.import('node_modules/slick-carousel/slick/ajax-loader.gif',{destDir:'assets'});}};
If you look closely, you will see that the first import being made isslick.js
. This is awful for Fastboot and will cause it to blow up server-side. So how do we make slick a little more slicker with its imports?
The first step is getting rid of theblueprintsPath
and creating a separate import tree for our offending code, which we will term asvendor code. Let's write out the function and import our necessary objects:
module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');},included:function(app){[...]
Now, let's use theFunnel
object to specify the code we want to separate:
module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})},included:function(app){[...]
Next, we define theguard that is mentioned in the Fastboot documentation, which essentially states to only include our code if theFastBoot
object isundefined
, which is guaranteed to betrue
when we are in the browser:
module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})browserVendorLib=map(browserVendorLib,(content)=>`if (typeof FastBoot === 'undefined') {${content} }`);},included:function(app){[...]
Then, to wrap up the separation, we return a merge of both thedefaultTree
and our browser / vendor tree:
module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})browserVendorLib=map(browserVendorLib,(content)=>`if (typeof FastBoot === 'undefined') {${content} }`);returnnewmergeTrees([defaultTree,browserVendorLib]);},included:function(app){[...]
But wait!! This also has the potential to fail - as it is actually possible fordefaulTree
to beundefined
! So, we must guard against this by only including it if it exists:
module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})browserVendorLib=map(browserVendorLib,(content)=>`if (typeof FastBoot === 'undefined') {${content} }`);letnodes=[browserVendorLib];if(defaultTree){nodes.unshift(defaultTree);}returnnewmergeTrees(nodes);},included:function(app){[...]
The next step is correcting the app import statement inincluded
. We want to change the import statement to point at our newvendor/slick/
directory. In our case this looks like:
[...]included:function(app){this._super.included(app);app.import("node_modules/slick-carousel/slick/slick.css");app.import("node_modules/slick-carousel/slick/slick-theme.css");app.import("node_modules/slick-carousel/slick/fonts/slick.ttf",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.svg",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.eot",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.woff",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/ajax-loader.gif",{destDir:"assets"});app.import("vendor/slick/slick.js");}};
And finally, the obligatory code snippet of everything put together:
'use strict';module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})browserVendorLib=map(browserVendorLib,(content)=>`if (typeof FastBoot === 'undefined') {${content} }`);letnodes=[browserVendorLib];if(defaultTree){nodes.unshift(defaultTree);}returnnewmergeTrees(nodes);},included:function(app){this._super.included(app);app.import("node_modules/slick-carousel/slick/slick.css");app.import("node_modules/slick-carousel/slick/slick-theme.css");app.import("node_modules/slick-carousel/slick/fonts/slick.ttf",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.svg",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.eot",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.woff",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/ajax-loader.gif",{destDir:"assets"});app.import("vendor/slick/slick.js");}};
And that's it! We now can successfully includeember-slick
into our server-side rendered Ember project, deferring its evaluation until it reaches the browser and in turn avoiding any fatal errors during the process - which is quite a feat for anyone that's dealt with Ember Fastboot and fancy browser JS addons 🥳
Conclusion
While it's quite a cold, dark world out there for Ember developers nowadays, there are still some glints of light and hope here and there. One such glint is the realization that including client-side JS heavy addons into a Fastboot project is indeed possible and can be achieved by editing the addon'sindex.js
.
I hope this helps the 29 others out there that may be facing similar issues 😉
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse