(AdonisJS + Storybook) - React = Success?
I'll detail what I found out about how to make Storybook work with AdonisJS WITHOUT adding an extra UI layer on the frontend.
server
adapter. Without this, nothing here would be possible. This allows us to write anything we want on the server, and then for Storybook to dynamically fetch said anything for use in stories.{builder,manager}-webpack5
packages. When using Adonis' Encore-based Webpack tooling, it uses Webpack 5. Storybook ships with Webpack 4. You can guess that those different versions don't like each other, so we can tell Storybook to use Webpack 5 instead.Running npx storybook init
in an Adonis application will confuse Storybook so much that it'll let you select a type. We want to select server
from the dropdown, that installs a bunch of the basic pieces for us. Make sure to enable Webpack 5 support after this:
npm i -D @storybook/manager-webpack5 @storybook/builder-webpack5
npm un -D @storybook/manager-webpack4 @storybook/builder-webpack4
And edit .storybook/init.js
:
module.exports = {
stories: ['../stories/**/*.stories.mdx', '../stories/**/*.stories.@(json)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
- framework: '@storybook/server'
+ framework: '@storybook/server',
+ core: {
+ builder: 'webpack5',
+ },
}
Now, the big question is how to structure our HTTP endpoints that Storybook will query. An early pattern I've gone for here is to have a component
route that will relay its parameters to render any component, with any props (as long as they can be serialized into a query string, which is admittedly kind of an annoying limitation).
For this, you'll also want to set the server URL in Storybook's config. That's .storybook/preview.js
:
export const parameters = {
server: {
- url: 'https://whatever-url-they-have-here.netlify.app',
+ url: 'http://localhost:3333/dev/component',
},
}
Add a new route:
// We want to make sure this route is ONLY exposed
// in development!
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
Route.get("/dev/component/*", async ({ view, params, request }) => {
// Get query string params first...
const qs = request.qs();
// And render the proxy component view.
return view.render("component", {
// Map to whichever name was provided after `/dev/component/`
name: params["*"],
props: qs,
});
});
}
We can then define an Edge view that proxies to a component (in this case resources/views/component.edge
):
@!component(`components/${name}`, props || {})
This will effectively only render whichever component is given as the name
parameter, with props, should there be any.
Now we can define components (resources/views/components/button.edge
):
<button>{{ label || "Test" }}</button>
Try loading https://localhost:3333/dev/component/button in your browser, you'll see a Test
button. You can also now add query parameters, for example the button label: https://localhost:3333/dev/component/button?label=My+cool+label
.
Before we try to configure Storybook further, we'll need to enable Adonis' CORS support. We need this because the default browser policy is very restrictive, and we want the storybook page to be able to make HTTP requests to the Adonis page. Modify config/cors.ts
and set enabled: false
to enabled: true
.
It's time to add some stories. Change stories/button.stories.json
to:
{
"title": "Buttons",
"parameters": {
"options": {
"component": "button"
}
},
"stories": [
{
"name": "No label",
"parameters": {
"server": {
"id": "button"
}
}
},
{
"name": "With label",
"parameters": {
"server": {
"id": "button",
"params": {
"label": "This is a label"
}
}
}
},
{
"name": "With custom label",
"parameters": {
"server": {
"id": "button"
}
},
"args": {
"label": "Customize me!"
}
}
]
}
There's three stories being defined here. I'll go through them:
/dev/component/button
./dev/component/button?label=This+is+a+label
. Notice how the params
object will be serialized into query string parameters.args
instead of server.params
, we can create controls. The contents of args
will also get serialized into the query string.You may have noticed that you need to run Storybook and Adonis at the same time to make this work, otherwise Storybook will hit you with a big red error message about being unable to reach the remote.
I like to use a package called npm-run-all
for this. Install it, and edit your package.json
scripts to something like this:
{
"scripts": {
[...]
- "storybook": "start-storybook -p 6006",
+ "storybook": "run-p dev run-storybook",
+ "run-storybook": "start-storybook -p 6006",
[...]
}
}
run-p
is a script that will run two or more other npm scripts in parallel.
We may want to customize how Storybook fetches from our Adonis endpoint. Thankfully, the @storybook/server
adapter provides a hook for this. Say we wanted to use a POST request with the props in the request's body instead:
// in .storybook/preview.js
const fetchStoryHtml = async (url, path, params, context) => {
const fetchUrl = new URL(`${url}/${path}`);
const response = await fetch(fetchURL, {
method: "POST",
body: params,
});
return response.text();
};
export const parameters = {
server: {
url: `http://localhost:3333/dev/component`,
fetchStoryHtml,
},
};