Deploying Storybook on Rails
At work, we’ve been using Storybook to help build out our design system.
We then ran into the issue of how to make it accessible to the wider team. We wanted it to be easily accessible to everyone, but not public.
Heroku is what we use for hosting, so the easiest thing seemed to be build it into our Rails app. I went down the path of looking at creating a separate engine to serve the Storybook files, before realising that Rack::Static had everything I need.
Turns out if you go hunting around in rack, theres a super handy Rack::Builder and we can combine this with Rack::Static to serve the static files storybook can generate.
# app/lib/storybook.rb
class Storybook
def self.available?
static_build_enabled? && storybook_static_present?
end
# In development, we want to run storybook so we get automatic recompiling
# on file changes
def self.static_build_enabled?
!Rails.env.development? && ENV["DISABLE_STORYBOOK_PRECOMPILE"].blank?
end
# Make sure that we static content is available
def self.storybook_static_present?(file: File)
return @storybook_static_present if defined?(@storybook_static_present)
storybook_directory = Rails.root.join("storybook-static/storybook")
@storybook_static_present ||= file.directory?(storybook_directory)
end
def self.app
Rack::Builder.app do
use Rack::Static,
index: "index.html",
cascade: false,
# Only serve files in the /storybook path, relative to the root
urls: ["/storybook"],
root: Rails.root.join("storybook-static"),
# Need to disable the Rails CSP, as otherwise all the scripts
# on the storybook index.html will not run
header_rules: [
[:all, { "Content-Security-Policy" => "" }]
]
# This run will never be called, as the Rack::Static middleware is
# serving everything, but it needs a 'run' to keep Rack::Builder happy
run(proc { |_env| ["404", { "Content-Type" => "text/html" }, [""]] })
end
end
end
We use devise for authentication, so the if we add this to our routes file, storybook won’t be accessible unless the request is authenticated as an admin
role.
As an alternative to using devise auth, you could mount storybook with rack basic auth if you wanted to keep it private.
# config/routes.rb
authenticate :admin do # devise authentication
# These files might not be compiled, so only allow this route if the
# compiled storybook files are present.
# This directory needs to match the one defined in the 'build-storybook'
# script in package.json
# Use `yarn storybook` to run storybook in development
constraints -> { Storybook.available? } do
mount Storybook.app, at: "/storybook-static"
end
end
}, at: "/storybook"
Initially I tried using heroku-buildpack-storybook to build the storybook, but there were some issues. Rails on Heroku automatically runs assets:precompile, which in turn runs assets:cleanup, which if you are using webpacker, results in the node_modules
directory being removed. This makes it a bit of a pain to run build-storybook.
The solution I came up with was to add an action to the assets:precompile
step so that the storybook files are compiled at the same time.
# config/initializers/storybook.rb
# If you are on a version of rack <= 3.0.8, then you need to
# map mjs files to application/javascript as Storybook uses mjs files.
# The next version of rack should have this built in when this is shipped:
# https://github.com/rack/rack/commit/1bd0f1597d8f4a90d47115f3e156a8ce7870c9c8
Rack::Mime::MIME_TYPES[".mjs"] = "application/javascript"
Rails.application.reloader.to_prepare do
# Compile storybook when precompiling assets so we don't have
# to remember to do it each time.
if Rake::Task.task_defined?("assets:precompile") && Storybook.static_build_enabled?
Rake::Task["assets:precompile"].enhance do
Rake::Task["storybook:build"].invoke
end
end
end
You will also need the rake task invoked above, which looks like this:
# lib/tasks/storybook.rake
namespace :storybook do
desc "Run the 'yarn build-storybook' command"
task build: :environment do |_t|
Dir.chdir(Rails.root) do
puts `yarn build-storybook`
end
end
end
# See config/initializers/storybook.rb for the hook of storybook
# build into the assets:precompile step
Then we need a script in yarn to run the build.
// package.json
"scripts": {
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook -o storybook-static/storybook"
},
And et voilĂ , you have storybook accessible at yourdomain.com/storybook-static