Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/giant-paws-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"website": patch
---

Document MDX and mock date.
122 changes: 122 additions & 0 deletions packages/website/blog/2023-09-19-ladle-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
slug: ladle-v3
title: Ladle v3
author: Vojtech Miksu
author_title: Developer Platform, Uber
author_url: https://twitter.com/vmiksu
author_image_url: https://miksu.cz/images/profile.jpg
tags: [ladle, storybook, react, testing, components]
---

Ladle is a tool designed for building and testing React components through [stories](https://github.com/ComponentDriven/csf). It serves as a seamless alternative to Storybook and is built using Vite and SWC. Our main goal is to ensure it's as swift and user-friendly as conceivable.

Introduced 18 months ago, Ladle is now utilized in 335 different Uber projects with a total of 15,896 stories. The community response has been positive as well:

- 🎯 60,000 weekly downloads.
- 🖊️ 40 contributors.
- 💬 300 folks joined our [Discord](https://discord.gg/H6FSHjyW7e) community.

Having taken our break from Storybook, Webpack, and Babel, today I'm eager to share insights from our journey and introduce the upcoming major release of Ladle. This version emphasizes the open-source community, addresses existing issues, enhances and outlines standard configurations with libraries like Next, Emotion, and styled-components, and establishes future priorities.

## What's new in v3?

- [SWC](https://swc.rs/) (Speedy Web Compiler) is now the default compiler, replacing Babel for transforming React and Typescript in Ladle projects. This shift leads to 2x faster production builds, swift initial startup, and incremenetal builds. We've exclusively adopted it for our internal projects. Furthermore, we've crafted our own set of [plugins](https://github.com/tajo/swc-plugin-fusion) to replace certain Babel plugins. We're confident in its scalability; however, if preferred, you can [revert](/docs/babel) to the Babel-based @vite/plugin-react.
- Official support for Node v18 and v20, while Node v16 is now deprecated.
- Added [MDX stories](/docs/mdx) and markdown compatibility.
- A streamlined method to [mock dates](/docs/mock-date) within stories.
- CSS-in-JS libraries function without any extra tweaks. Documented configurations are available for [Emotion](/docs/css#emotion), [Styled-components](/docs/css#styled-components), [BaseWeb + Styletron](/docs/css#baseweb-and-styletron), [Tailwind](/docs/css#tailwind).
- An outlined setup for [Next.js](/docs/nextjs).
- Exported types like `StoryDefault` and `Meta` are now interfaces, allowing for extensions.

Recent additions include:

- Global [hotkeys](/docs/hotkeys) for accelerated navigation.
- Prebundled dependencies to prevent later reloads.
- Global `args` and `argTypes`.
- A specialized control for adjusting [backgrounds](/docs/background).
- Enhanced Typescript types via `satisfies`.
- [HTTP/2 support](/docs/http2).
- The [`storyOrder`](/docs/config#storyorder) feature to customize story sequences.
- Additional features and bug resolutions.

## Lessons Learned

The primary catalyst behind Ladle was performance enhancement. Incremental builds transitioned from several seconds or even minutes to mere milliseconds. Startup durations reduced dramatically. And bellow a chart showcases our progression to a 100% Ladle adoption by 08/2023 and its impact on CI build times.

![Production build times](/img/build-times.png)

Technically, we've been doing three different migrations at the same time:

- From Storybook to Ladle.
- From Webpack to Vite.
- From Babel to SWC.

### Storybook to Ladle

This phase was relatively smooth. Ladle was architectured to be compatible with the majority of Storybook's core APIs and features. As we integrated individual setups, we incorporated missing elements into Ladle. The transition was seamless for about 99% of our stories, requiring minimal changes.

However, Ladle is more strict in some cases. For instance, story titles must be string literals to support automated code-splitting.

### Webpack to Vite

Transitioning from Webpack to Vite posed significant challenges, primarily because they operate differently. While Webpack automatically adjusts any JS code for browser compatibility, Vite interacts minimally with the code, leveraging native browser functionalities, primarily ES modules.

#### Importing Types

It was common for developers to overlook the subtle distinctions between:

```ts
import type { Foo } from "baz";
```

vs

```ts
import { Foo } from "baz";
```

The issue is that Vite will pass the second import into the browser without any changes and and browser will complain about non-existing export `Foo` since types get stripped from the code before being sent to the browser.

#### Commonjs Modules

Vite isn't designed for CommonJS modules. We transitioned our codebase to ES modules, replacing occasional `require` calls with alternatives like `await import` or leveraging SWC's [dead code elimination](https://swc.rs/docs/configuration/compilation#jsctransformoptimizersimplify).

#### import window

```js
import window from "global/window";
```

will result in

> ReferenceError: Cannot access 'window' before initialization

The solution is to rename `window` to something else.

### Startup time

It turns out that Vite can be quite slow if you need to import thousands of modules. To be fair, it's often a self-inflicted issue. There is a popular pattern where developers create these `feature/index.js` god modules that re-export every single module from the `feature` folder. Then, they import from `feature/index.js` instead of importing individual modules.

This is not a big issue with Webpack since it always bundles code together and it can eliminate code that is not used. Vite, on the other hand, needs to import all these modules individually and each import results in a separate HTTP request. This can be make browser quite unhappy when it reaches thousands of HTTP requests.

It's hard to refactor large codebases like that. We partially alleviate this issue by adapting [HTTP/2](/docs/http2) so browsers can download multiple modules in a single HTTP request. SWC also makes individual module transformations much faster. The Vite team is aware of [this issue](https://github.com/vitejs/vite/issues/1309) and they made a lot of changes to make Vite faster over time. This [persistent cache plugin](https://github.com/vitejs/vite/pull/14333) should also help.

### Babel to SWC

This was the easiest part. SWC is mostly a drop-in replacement for Babel. We had some custom babel plugins we had to migrate into Rust and [SWC plugins](https://github.com/tajo/swc-plugin-fusion). I had no previous experience with Rust and the [SWC plugin API](https://swc.rs/docs/plugin/ecmascript/getting-started) is still marked as experimental and not that well documented. It also seems less flexible than Babel's plugin API that provides more information about the AST and allows you to modify it more easily (I'm still not sure how to properly track scopes with SWC). However, ChatGPT was extremely helpful and I was able to migrate all our custom Babel plugins into SWC in a few days. There are also a few existing [SWC plugins](https://github.com/swc-project/plugins) for some popular libraries.

There were some pretty rare (incorrect) syntaxes that Babel happily parses and SWC fails on. Those are pretty easy to find and fix. We also ran into a syntax being compiled differently by Babel (not adhering to the spec) and SWC (adhering to the spec) that caused a subtle runtime error related to [class fields](https://swc.rs/docs/migrating-from-tsc#usedefineforclassfields). This one took a few days to debug but it was a good lesson to not rely on Babel's non-standard behavior.

Overall, the migration was pretty straightforward. It took a while since we didn't really force it and did it mostly in a spare time. Also, the amount of setups and stories we had to migrate was abnormally large.

## What's Next

The secondary reason why Ladle was created was to make our test automation easier. That's why Ladle has statically analyzable [meta](/docs/meta) parameter and first-class [programatic API](/docs/programmatic). We might open-source/release more of it, there are some testing concepts described [here](/docs/visual-snapshots).

We want to make easier to generate permutations of stories for automated testing. This could mean utilizing typescript types and making some changes to controls API to make it statically analyzable. Ladle also doesn't support component story format 3.0. All these things are somewhat related and we are looking into how to best combine them to improve our internal testing framework.

On the other hand, we have no plans to add support for other frameworks than React since we are React only shop. If you are looking for Vue/Svelte Vite based alternatives, you should try [https://histoire.dev/](https://histoire.dev/). It is also not our goal to support all the features of Storybook or trying to duplicate its addon ecosystem. Storybook is a great tool and we are not trying to replace it, it's also trying to address some performance issues by introducing [vite builder](https://storybook.js.org/docs/react/builders/vite#page-top).

## Last Words

I extend my gratitude to all the OSS maintainers who've contributed to Ladle's existence. A special thanks to Vite for their continuous support (including Ladle into their [ecosystem test suite](https://github.com/vitejs/vite-ecosystem-ci)). Additionally, SWC's emergence as a solid alternative to Babel, courtesy of its recent plugin API, has been instrumental.
20 changes: 20 additions & 0 deletions packages/website/docs/babel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
id: babel
title: Babel
---

Ladle uses [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) by default. This plugin uses [SWC](https://swc.rs/) to transform all React code and is many times faster than Babel.

If you want to use [Babel](https://babeljs.io/) instead of SWC (in case you need to apply some custom babel plugins), you can do it by installing and adding [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react) into `./vite.config.js`:

```js title="vite.config.js"
import react from "@vitejs/plugin-react";

export default {
plugins: [react()],
};
```

Ladle automatically disables the default SWC plugin.

SWC is up to 20x faster than Babel and can supercharge your development experience. The only downside is that you can't utilize a rich ecosystem of Babel plugins if your project uses a special syntax. However, there are also a few SWC [plugins](https://github.com/swc-project/plugins) and there is a [plugin API](https://swc.rs/docs/plugin/ecmascript/getting-started) so you can write your own.
101 changes: 101 additions & 0 deletions packages/website/docs/css.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,104 @@ export const MyStory: Story = () => {
If the project contains valid PostCSS config (any format supported by [postcss-load-config](https://github.com/postcss/postcss-load-config), e.g. `postcss.config.js`), it will be automatically applied to all imported CSS.

Ladle just defaults to [Vite's CSS handling](https://vitejs.dev/guide/features.html#css).

## Tailwind

There is a working [example](https://github.com/tajo/ladle/blob/main/e2e/css/src/hello.stories.tsx#L11). You need to install `tailwindcss` and create

```js title="postcss.config.js"
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
```

and

```js title="tailwind.config.js"
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {},
variants: {},
plugins: [],
};
```

## Emotion

Add these dependencies

```sh
pnpm add @emotion/react @swc/plugin-emotion @vitejs/plugin-react-swc
```

And create

```js title="vite.config.js"
import react from "@vitejs/plugin-react-swc";

export default {
plugins: [
react({
jsxImportSource: "@emotion/react",
plugins: [["@swc/plugin-emotion", {}]],
}),
],
};
```

## Styled-components

Styled-components work out of the box but it's highly recommended to install the optional plugin.

```sh
pnpm add styled-components @swc/plugin-styled-components @vitejs/plugin-react-swc
```

And create

```js title="vite.config.js"
import react from "@vitejs/plugin-react-swc";

export default {
plugins: [
react({
plugins: [["@swc/plugin-styled-components", {}]],
}),
],
};
```

## BaseWeb and Styletron

```sh
pnpm add baseui styletron-react styletron-engine-monolithic
```

and create a global provider:

```js title=".ladle/components.tsx"
import { Provider as StyletronProvider } from "styletron-react";
import { Client as Styletron } from "styletron-engine-monolithic";
import { LightTheme, DarkTheme, BaseProvider } from "baseui";
import type { GlobalProvider } from "@ladle/react";

const engine = new Styletron();

export const Provider: GlobalProvider = ({ children, globalState }) => (
<StyletronProvider value={engine}>
<BaseProvider
theme={{
...(globalState.theme === "dark" ? DarkTheme : LightTheme),
direction: globalState.rtl ? "rtl" : "ltr",
}}
>
{children}
</BaseProvider>
</StyletronProvider>
);
```

This will setup [BaseWeb](https://baseweb.design) component library, [Styletron](https://styletron.org/) and integrates Dark/Light themes with Ladle.
94 changes: 94 additions & 0 deletions packages/website/docs/mdx.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
id: mdx
title: MDX
---

Ladle supports MDX and markdown by default. You can use MDX to write your stories. This might be useful for documentation.

## Basic MDX Story

```md title="basic.stories.mdx"
# Header

> Some quote: Suspendisse at tempor velit. **Fusce** a fermentum arcu,
> vitae semper mi. Nunc placerat, mauris ac volutpat tempus, arcu eros
> accumsan nisi, in congue risus turpis in ligula.

Some [example](https://example.com) link.

1. One
2. Two
3. Three
```

This story will be displayed in the side nagivation as `Basic` > `Readme`.

## Custom Title

The title in side navigation can be customized:

```md title="basic.stories.mdx"
import { Meta } from "@ladle/react";

<Meta title="Documentation/Welcome" />

# Customized

This is a paragraph.
```

This story is displayed as `Documentation` > `Welcome`. If you set the `title` to `Welcome`, the story would be displayed as `Basic` > `Welcome`. You can use arbitrary number of levels.

## Multiple Stories

You can also create multiple stories within one `*.stories.mdx` file:

```md title="multiple.stories.mdx"
import { Story } from "@ladle/react";

This part will be rendered as a separate `Readme` story.

<Story name="First">
<input />
<button>simple</button>
</Story>

<Story name="Second">
<input />
<button>second</button>
</Story>
```

## Meta Parameters

You can also set `meta` parameters:

```md title="args.stories.mdx"
import { Story, Meta } from "@ladle/react";

<Meta meta={{ iframed: true }} />

This part will be rendered as a separate `Readme` story.

<Story name="First">
<input />
<button>simple</button>
</Story>

<Story name="Second" meta={{ width: 400 }}>
<input />
<button>second</button>
</Story>
```

## Importing Markdown

You can import markdown into your stories:

```md title="markdown.stories.mdx"
import Readme from "./README.md";

# Header

<Readme />
```
21 changes: 21 additions & 0 deletions packages/website/docs/mock-date.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
id: mock-date
title: Mock Date
---

When you test your stories, it might be useful to mock `new Date()` so the displayed components always render same values. You can use `meta.mockDate` parameter to set a specific date and time:

```js title="date-picker.stories.tsx"
import type { Story } from "@ladle/react";

export const DatePicker: Story = () => {
const date = new Date();
return (
<h1>{date.toLocaleDateString("en-US")}</h1>
);
};

DatePicker.meta = {
mockDate: "1995-12-17T03:24:00",
};
```
22 changes: 0 additions & 22 deletions packages/website/docs/swc.md

This file was deleted.

Loading