Under the hood

Guillaume Chau

@Akryum

Vue.js Core Team

@Akryum@m.webtoo.ls

A story is a small number of components mounted in an isolated environment

Story:

Why stories?

Organize and document components for other developers

Showcase features and components

Develop components in isolation

Tests components

Visual regression

⚡ Dynamic source

🍱 Variant grids

📖 Markdown docs

🌔 Dark theme

📱 Responsive testing

🎹 Flexible controls

📷 Visual regression testing

🎨 Automatic design tokens

🔍 Fast fuzzy/full-text search

More to come...

Packed with features!

Writing stories

Writing stories
should look similar to writing component code

<!-- Cars.story.vue -->
<template>
  <Story title="Cars">
    <Variant title="default">
      🚗
    </Variant>
    <Variant title="Fast">
      🏎️
    </Variant>
    <Variant title="Slow">
      🚜
    </Variant>
  </Story>
</template>

How Vite powers Histoire

Vite Native

Reuse the same build pipeline

Less time and effort setting up

Fast boot and instant HMR

Virtual modules

histoire dev

Story FS watcher

.md FS watcher

Vite dev server

(HTTP server)

vite-node server

Generate docs-only stories

<!-- Cars.story.vue -->
<template>
  <Story title="Cars">
    <Variant title="default">
      🚗
    </Variant>
    <Variant title="Fast">
      🏎️
    </Variant>
    <Variant title="Slow">
      🚜
    </Variant>
  </Story>
</template>
  • Docs of Story 1 
  • Docs page 1
  • Docs page 2
  • Story 1
  • Story 2
  • Story 3
  • ...

Collect

Story FS watcher

.md FS watcher

Vite dev server

(HTTP server)

vite-node server

  • Story 1
  • Story 2
  • Story 3
  • ...
  • Docs of Story 1 
  • Docs page 1
  • Docs page 2

Vite dev server

(HTTP server)

Virtual modules

Browser

import {
  files, tree, onUpdate
} from 'virtual:$histoire-stories'
export let files = [{ ... }]
export let tree = { ... }
const handlers = []
export function onUpdate (cb) {
  handlers.push(cb)
}
if (import.meta.hot) {
  import.meta.hot.accept(newModule => {
    files = newModule.files
    tree = newModule.tree
    handlers.forEach(h => {
      h(newModule.files, newModule.tree)
      newModule.onUpdate(h)
    })
  })
}

HMR

async resolveId (id) {
  if (id === 'virtual:$histoire-stories') {
    return '\0virtual:$histoire-stories'
  }
}
async load (id) {
  if (id === '\0virtual:$histoire-stories') {
    return `export let files = [${JSON.stringify(files)}]
export let tree = ${JSON.stringify(tree)}
const handlers = []
export function onUpdate (cb) {
  handlers.push(cb)
}
if (import.meta.hot) {
  // Handle HMR...
}`
  }
}

Vite dev server

(HTTP server)

Virtual modules

Browser

HMR

stories data & tree

resolved config

theme CSS variables

setup code

support plugins

markdown files

full-text search indexes

Vite dev server

(HTTP server)

Browser

View: Story 1

stories data & tree

setup code

support plugin

markdown files

Mount: Story 1

Story1.story.vue

Render: Story 1: Content

Render: Story 1: Controls

support plugin

support plugin

Load

  • Props
  • Slots
  • Render functions

Browser

View: Story 1

stories data & tree

support plugin

Mount: Story 1

Story1Component.vue

Load

Render: Story 1: Content

Render: Story 1: Controls

support plugin

support plugin

Sync state

can be in an iframe

Browser

View: Story 1

stories data & tree

support plugin

Mount: Story 1

Story1Component.vue

Load

Render: Story 1: Content

support plugin

Auto-CodeGen: Story 1

support plugin

slot

<button
  color="primary"
  disabled
>
  Click me!
</button>

Sync state

Browser

Virtual modules

stories data & tree

resolved config

theme CSS variables

setup code

support plugins

markdown files

full-text search indexes

Static code

Vite build

HMR API

export const count = 1

if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    if (newModule) {
      // newModule is undefined when SyntaxError happened
      console.log('updated: count is now ', newModule.count)
    }
  })
}

Accept an update for current module

import { foo } from './foo.js'

foo()

if (import.meta.hot) {
  import.meta.hot.accept('./foo.js', (newFoo) => {
    // the callback receives the updated './foo.js' module
    newFoo?.foo()
  })
}

Accept an update for a dependency

if (import.meta.hot) {
  // When current module is replaced
  import.meta.hot.dispose((data) => {
    // cleanup side effect
  })
  
  // when the module is no longer used in the page
  import.meta.hot.prune((data) => {
    // cleanup side effect
  })
}

Cleanup module side-effects

if (import.meta.hot) {
  import.meta.hot.accept((module) => {
    // You may use the new module instance
    // to decide whether to invalidate.
    if (cannotHandleUpdate(module)) {
      import.meta.hot.invalidate()
    }
  })
}

Bail out of HMR and propagate the update in parent modules

Client-Dev server communication

Using HMR API

vitePlugins.push({
  name: 'histoire:example',
  
  configureServer (server) {
    server.ws.on('some-event', (payload) => {
      server.ws.send('some-other-event', somePayload)
    })
  },
})
if (import.meta.hot) {
  import.meta.hot.send('some-event', { message: 'Hello Amsterdam' })
  
  import.meta.hot.on('some-other-event', (somePayload) => {
    console.log(somePayload)
  })
}

Browser

Vite server

Thank you!

Histoire: A deep dive (Vue Amsterdam 2023)

By Guillaume Chau

Histoire: A deep dive (Vue Amsterdam 2023)

A look at the internals of Histoire, a tool to generate component stories, and how Vite makes it possible.

  • 2,517