Skip to content

Commit

Permalink
feat(vite-plugin): add initial MDX and frontmatter support
Browse files Browse the repository at this point in the history
  • Loading branch information
itsjavi committed Sep 9, 2023
1 parent 0e225bd commit ca0c54b
Show file tree
Hide file tree
Showing 11 changed files with 1,357 additions and 312 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ StoryLite provides an intuitive UI that's customizable to your unique needs.
tab, etc.
- **HMR (Hot Module Reload)** support when story files (or any module they use) change.
- **SSG (Static Site Generation)** support thanks to Vite.
- **Markdown and MDX** support out via Vite plugins.

## Installation

Expand All @@ -67,6 +68,27 @@ For the next steps, please check the
[example React](https://github.com/itsjavi/storylite/tree/main/packages/examples/react) directory to
learn how to integrate it in your project.

## Adding MDX support

Check the `docs` package to see how to add MDX support to your project:

- [vite.config.ts](https://github.com/itsjavi/storylite/tree/main/packages/docs/vite.config.ts)
- [stories/index.stories.mdx](https://github.com/itsjavi/storylite/tree/main/packages/docs/stories/index.stories.mdx)

With this setup you can:

- Import modules from JS and JSX files.
- Define the default story metadata in the MDX's frontmatter block. The body of the MDX will be used
as the default story component.
- You can import other MD/MDX files in your stories, they will be ready to be used as JSX
components.
- You can export JSX components to define new story variants.

You currently can't:

- You cannot import TS/TSX files in your MDXs. This is a limitation of Vite's MDX plugin.
- You currently cannot export Story objects, instead you can only export JSX components.

## Current Focus and Future

While StoryLite is geared towards React components at the moment, the potential exists for broader
Expand Down
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"format": "pnpm lint-fix && pnpm prettier-fix",
"hooks:pre-commit": "pnpm lint-staged",
"hooks:pre-push": "pnpm run quality-checks",
"postinstall": "pnpm lint-staged || echo 'lint-staged failed on postinstall'",
"postinstall": "pnpm lint-staged || echo 'postinstall failed'",
"lint": "turbo lint -- --max-warnings=0",
"lint-fix": "turbo lint-fix",
"prepare": "husky install",
Expand All @@ -34,8 +34,7 @@
"version:bump-minor": ".scripts/bump-version.sh minor && .scripts/tag-version.sh",
"version:bump-patch": "pnpm ws:version",
"version:publish": "pnpm run version:publish-gh-refs && pnpm run version:publish-npm",
"version:publish-gh-refs": "git push origin main --tags",
"version:publish-gh-releases": ".scripts/tag-release-gh.sh",
"version:publish-gh-refs": "git push && git push --tags",
"version:publish-npm": "pnpm -r --filter='@storylite/*' exec pnpm publish"
},
"lint-staged": {
Expand All @@ -56,9 +55,9 @@
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.4",
"@types/node": "^20.5.9",
"@types/node": "^20.6.0",
"changelogen": "^0.5.5",
"eslint": "^8.48.0",
"eslint": "^8.49.0",
"husky": "^8.0.3",
"jest": "^29.6.4",
"jest-environment-jsdom": "^29.6.4",
Expand Down
12 changes: 10 additions & 2 deletions packages/docs/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# storylite-docs
# StoryLite, a lightweight alternative to StoryBook.

Storylite documentation and website
**StoryLite** is a modern and lightweight toolkit for crafting and managing design systems and
components with ease. Inspired by the popular StoryBook UI and powered by Vite⚡️, StoryLite offers
a streamlined and familiar developer experience.

With StoryLite, you can swiftly create, test, and refine UI components in isolation, ensuring that
your application maintains a consistent look and feel.

Tailored for smaller projects that crave simplicity without the overhead of a full StoryBook setup,
StoryLite provides an intuitive UI that's customizable to your unique needs.
6 changes: 5 additions & 1 deletion packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@mdx-js/rollup": "^2.3.0",
"@storylite/storylite": "workspace:*",
"@storylite/vite-plugin": "workspace:*",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react-swc": "^3.3.2",
"remark-frontmatter": "^4.0.1",
"remark-mdx-frontmatter": "^3.0.0",
"typescript": "^5.2.2",
"vite": "^4.4.9"
"vite": "^4.4.9",
"vite-plugin-markdown": "^2.1.0"
}
}
14 changes: 14 additions & 0 deletions packages/docs/stories/index.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: Welcome
renderFrame: root
navigation:
icon: 🏠
order: 0
hidden: true # hidden in the submenu
---

import BaseReadme from '../README.md'

<img src="logo.svg" width={64} height={64} alt="StoryLite Logo" />

<BaseReadme />
33 changes: 0 additions & 33 deletions packages/docs/stories/index.stories.tsx

This file was deleted.

21 changes: 15 additions & 6 deletions packages/docs/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/// <reference types="vite/client" />
import { resolve } from 'path'

import mdx from '@mdx-js/rollup'
import storylite from '@storylite/vite-plugin'
import react from '@vitejs/plugin-react-swc'
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
import { defineConfig } from 'vite'

/**
* @see https://vitejs.dev/config
* @see https://vitejs.dev/guide/build.html#multi-page-app
*/

import storylitePlugin from '@storylite/vite-plugin'
import react from '@vitejs/plugin-react-swc'
import { defineConfig } from 'vite'

export default defineConfig({
build: {
rollupOptions: {
Expand All @@ -20,8 +23,14 @@ export default defineConfig({
},
},
plugins: [
storylitePlugin({
stories: 'stories/**/*.stories.tsx', // relative to process.cwd()
{
enforce: 'pre', // this ensures that .md/mdx files are processed before react & storylite plugins
...mdx({
remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
}),
},
storylite({
stories: 'stories/**/*.stories.{tsx,md,mdx}', // relative to process.cwd()
}),
react(),
],
Expand Down
2 changes: 1 addition & 1 deletion packages/storylite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
},
"devDependencies": {
"@r1stack/coding-style": "^0.4.5",
"@types/node": "^20.5.9",
"@types/node": "^20.6.0",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"publint": "^0.2.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
},
"devDependencies": {
"@r1stack/coding-style": "^0.4.5",
"@types/node": "^20.5.9",
"@types/node": "^20.6.0",
"publint": "^0.2.2",
"tsup": "^7.2.0",
"typescript": "^5.2.2"
Expand Down
68 changes: 54 additions & 14 deletions packages/vite-plugin/src/story-collector.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BaseStoryWithId, StoryFiles, StoryFilesMap } from './types'
import { BaseStory, BaseStoryWithId, StoryFiles, StoryFilesMap } from './types'

function filenameToId(filename: string) {
return (
'/' +
filename
.replace(/\.[jt]sx?$/, '')
.replace(/\.mdx?$/, '')
.replace(/\.(stories|story)$/, '')
.replace(/^\//g, '')
.split('/')
Expand All @@ -26,22 +27,50 @@ function camelToTitleCase(str: string) {
})
}

function resolveDefaultExport(modules: any): BaseStory {
const frontmatter: BaseStory = modules?.frontmatter ?? {}
const defaultExport = modules?.default ?? undefined

if (typeof defaultExport === 'function') {
return {
...frontmatter,
component: defaultExport,
navigation: {
hidden: true,
...frontmatter?.navigation,
},
}
}

if (typeof defaultExport === 'object') {
return {
...frontmatter,
...defaultExport,
navigation: {
hidden: true,
...frontmatter?.navigation,
...defaultExport?.navigation,
},
}
}

return frontmatter
}

function modulesToStories(
fileId: string,
modules: { [key: string]: any },
): { [key: string]: BaseStoryWithId } {
const defaultExport: BaseStoryWithId = {
const _resolvedDefaultStoryWithoutId = resolveDefaultExport(modules)
const _resolvedDefaultStory: BaseStoryWithId = {
id: fileId,
...modules.default,
navigation: {
hidden: true,
...modules.default?.navigation,
},
..._resolvedDefaultStoryWithoutId,
}

// Recreates the modules object, but as story objects, with the default export merged into each
return Object.fromEntries(
Object.entries(modules)
.filter(([exportName]) => exportName !== 'frontmatter')
.map(([exportName, exportedValue]: [string, any]): [string, BaseStoryWithId] => {
let story: Partial<BaseStoryWithId> = exportedValue

Expand All @@ -60,29 +89,38 @@ function modulesToStories(

const storyIdPrefix = fileId.replace(/^[\\\/]/g, '').replace(/[\\\/]/g, '-')
const storyId = `${storyIdPrefix}-${exportName}`.toLowerCase()
// console.log(storyId)

if (exportName === 'default') {
story = {
..._resolvedDefaultStory,
...story,
id: storyId,
}
}

// Title resolution: .name -> .title -> .component.displayName -> exportName
const storyTitle =
story.name ??
story.title ??
story.component?.displayName ??
// defaultExport.title ??
// _resolvedDefaultStory.title ??
exportName

// don't inherit navigation from default export
const inheritedNavigation = exportName === 'default' ? defaultExport.navigation : {}
const inheritedNavigation = exportName === 'default' ? _resolvedDefaultStory.navigation : {}

// named exports should inherit the default export's decorators as well and apply them
// first, then apply their own decorators.
const mergedDecorators =
exportName === 'default'
? defaultExport.decorators ?? []
: [...(defaultExport.decorators ?? []), ...(story.decorators ?? [])]
? _resolvedDefaultStory.decorators ?? []
: [...(_resolvedDefaultStory.decorators ?? []), ...(story.decorators ?? [])]

const fullStory: BaseStoryWithId = {
// we also merge default export's properties,
// which are shared across all stories unless overridden
...defaultExport,
..._resolvedDefaultStory,
id: storyId,
navigation: {
...inheritedNavigation,
Expand Down Expand Up @@ -127,8 +165,10 @@ export function createStoryFilesMap(storyFiles: StoryFiles): StoryFilesMap {
.sort(([aPath], [bPath]) => aPath.localeCompare(bPath))
// sort story files by the navigation.order property defined in the default export
.sort(([, modulesA], [, modulesB]) => {
const aOrder = modulesA?.default?.navigation?.order ?? Infinity
const bOrder = modulesB?.default?.navigation?.order ?? Infinity
const defaultA = resolveDefaultExport(modulesA)
const defaultB = resolveDefaultExport(modulesB)
const aOrder = defaultA.navigation?.order ?? Infinity
const bOrder = defaultB.navigation?.order ?? Infinity

return aOrder - bOrder
})
Expand Down
Loading

0 comments on commit ca0c54b

Please sign in to comment.