Skip to content

Commit

Permalink
Add Storybook (Vite) framework package (#10064)
Browse files Browse the repository at this point in the history
Co-authored-by: Josh GM Walker <[email protected]>
Co-authored-by: Tobbe Lundberg <[email protected]>
  • Loading branch information
3 people authored Jun 4, 2024
1 parent 16b3e9c commit 285d657
Show file tree
Hide file tree
Showing 43 changed files with 1,730 additions and 367 deletions.
7 changes: 7 additions & 0 deletions .changesets/10064.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- Add Storybook (Vite) framework package (#10064) by @arimendelow

Adds:
- Storybook framework package for using Storybook with Vite.
- CLI package (command: `yarn rw sbv`) for running Storybook using Vite. CLI package additionally creates Mock Service Worker, and, on first run, the project-side Storybook config files.

Current Storybook (Webpack) users will need to manually migrate any additional Storybook config (mocks, etc.). The primary user-facing difference between the old and new Storybook integrations is that the config used by the old one lives in the `@redwoodjs/testing` package, and the config used by this new one lives in the user's `web/.storybook` directory.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ blog-test-project/*
*.code-workspace
.nova
packages/**/redwoodjs-*.tgz
packages/**/storybook-*.tgz
packages/create-redwood-app/create-redwood-app.tgz

# For esbuild.
Expand Down
8 changes: 8 additions & 0 deletions packages/cli-packages/storybook-vite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# CLI Packages - Storybook Vite

This CLI package is intended to be used with the [Storybook Framework package](../../storybook/README.md). We are still finalizing usage details.
For now, get started as follows:
- Run `yarn rw sbv` from your project. This will:
- Add the necessary config files, if they don't already exist: `web/.storybook/{main.ts + preview-body.html}`.
- Create the Mock Service Worker, which is needed for all Cell mocking.
- Run Storybook.
49 changes: 49 additions & 0 deletions packages/cli-packages/storybook-vite/build.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

import { build, defaultIgnorePatterns } from '@redwoodjs/framework-tools'

import fg from 'fast-glob'

await build()

/**
* We need template files, which esbuild won't copy over,
* so we do it manually here.
*/
async function copyAssets() {
const cliRootDirPath = path.dirname(fileURLToPath(import.meta.url))
const cliSrcDirPath = path.join(cliRootDirPath, 'src')
const cliDistDirPath = path.join(cliRootDirPath, 'dist')

let pathnames = await fg(
[
'**/*.template',
],
{
absolute: true,
cwd: cliSrcDirPath,
ignore: defaultIgnorePatterns,
}
)

// For Windows.
pathnames = pathnames.map(p => path.normalize(p))

for (const pathname of pathnames) {
const distPathname = pathname.replace(cliSrcDirPath, cliDistDirPath)

try {
await fs.cp(pathname, distPathname)
} catch (error) {
console.error(
`Couldn't copy ${pathname} to ${distPathname}. ` +
`(Replaced ${cliSrcDirPath} with ${cliDistDirPath} to get the dist pathname.)`
)
throw error
}
}
}

await copyAssets()
40 changes: 40 additions & 0 deletions packages/cli-packages/storybook-vite/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@redwoodjs/cli-storybook-vite",
"version": "7.0.0",
"repository": {
"type": "git",
"url": "https://github.com/redwoodjs/redwood.git",
"directory": "packages/cli-packages/storybook-vite"
},
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsx ./build.mts && yarn build:types",
"build:pack": "yarn pack -o redwoodjs-cli-storybook-vite.tgz",
"build:types": "tsc --build --verbose",
"build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"",
"prepublishOnly": "NODE_ENV=production yarn build"
},
"dependencies": {
"@redwoodjs/cli-helpers": "workspace:*",
"@redwoodjs/project-config": "workspace:*",
"@redwoodjs/telemetry": "workspace:*",
"chalk": "4.1.2",
"execa": "5.1.1",
"storybook": "7.6.17",
"storybook-framework-redwoodjs-vite": "workspace:*",
"terminal-link": "2.1.1",
"yargs": "17.7.2"
},
"devDependencies": {
"@redwoodjs/framework-tools": "workspace:*",
"@types/yargs": "17.0.32",
"tsx": "4.10.3",
"typescript": "5.4.5"
},
"gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1"
}
82 changes: 82 additions & 0 deletions packages/cli-packages/storybook-vite/src/commands/storybook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import terminalLink from 'terminal-link'
import type { Argv } from 'yargs'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

import type { StorybookYargsOptions } from '../types'

export const command = 'storybook-vite'
export const aliases = ['sbv']
export const description =
'Launch Storybook: a tool for building UI components and pages in isolation (now, with Vite)!'

export const defaultOptions: StorybookYargsOptions = {
open: true,
build: false,
ci: false,
port: 7910,
buildDirectory: 'public/storybook',
smokeTest: false,
}

export function builder(
yargs: Argv<StorybookYargsOptions>,
): Argv<StorybookYargsOptions> {
return yargs
.option('build', {
describe: 'Build Storybook',
type: 'boolean',
default: defaultOptions.build,
})
.option('build-directory', {
describe: 'Directory in web/ to store static files',
type: 'string',
default: defaultOptions.buildDirectory,
})
.option('ci', {
describe: 'Start server in CI mode, with no interactive prompts',
type: 'boolean',
default: defaultOptions.ci,
})
.option('open', {
describe: 'Open storybook in your browser on start',
type: 'boolean',
default: defaultOptions.open,
})
.option('port', {
describe: 'Which port to run storybook on',
type: 'number',
default: defaultOptions.port,
})
.option('smoke-test', {
describe:
"CI mode plus smoke-test (skip prompts; don't open browser; exit after successful start)",
type: 'boolean',
default: defaultOptions.smokeTest,
})

.epilogue(
`Also see the ${terminalLink(
'Redwood CLI Reference',
'https://redwoodjs.com/docs/cli-commands#storybook',
)}`,
)
}

export async function handler(options: StorybookYargsOptions): Promise<void> {
// NOTE: We should provide some visual output before the import to increase
// the perceived performance of the command as there will be delay while we
// load the handler.
recordTelemetryAttributes({
command: 'storybook-vite',
build: options.build,
ci: options.ci,
open: options.open,
smokeTest: options.smokeTest,
})
// @ts-expect-error - Custom workaround for storybook telemetry
process.emit('shutdown-telemetry')

const { handler: storybookHandler } = await import('./storybookHandler.js')
await storybookHandler(options)
}
147 changes: 147 additions & 0 deletions packages/cli-packages/storybook-vite/src/commands/storybookHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import fs from 'node:fs'
import path from 'node:path'

import type { ExecaError } from 'execa'
import execa from 'execa'

import { getPaths } from '@redwoodjs/project-config'
import { errorTelemetry } from '@redwoodjs/telemetry'

import c from '../lib/colors'
import type { StorybookYargsOptions } from '../types'

const readFile = (target: fs.PathOrFileDescriptor) =>
fs.readFileSync(target, { encoding: 'utf8' })

const writeFile = (target: string, contents: any) => {
const { base } = getPaths()
if (fs.existsSync(target)) {
throw new Error(`${target} already exists.`)
}

const filename = path.basename(target)
const targetDir = target.replace(filename, '')
fs.mkdirSync(targetDir, { recursive: true })
fs.writeFileSync(target, contents)
console.log(`Successfully wrote file \`./${path.relative(base, target)}\``)
}

export async function handler({
build,
buildDirectory,
ci,
open,
port,
smokeTest,
}: StorybookYargsOptions) {
// We add a stub file to type generation because users don't have Storybook
// installed when they first start a project. We need to remove the file once
// they install Storybook so that the real types come through.
fs.rmSync(
path.join(getPaths().generated.types.includes, 'web-storybook.d.ts'),
{ force: true },
)

// Check for conflicting options
if (build && smokeTest) {
throw new Error('Can not provide both "--build" and "--smoke-test"')
}

if (build && open) {
console.warn(
c.warning(
'Warning: --open option has no effect when running Storybook build',
),
)
}

const cwd = getPaths().web.base
const staticAssetsFolder = path.join(cwd, 'public')
const execaOptions: Partial<execa.Options> = {
stdio: 'inherit',
shell: true,
cwd,
}

// Create the `MockServiceWorker.js` file. See https://v1.mswjs.io/docs/cli/init
await execa.command(
`yarn msw init "${staticAssetsFolder}" --no-save`,
execaOptions,
)

const redwoodProjectPaths = getPaths()
const storybookConfigPath = path.dirname(
`${redwoodProjectPaths.web.storybook}/main.ts`,
)

const storybookMainFilePath = path.join(storybookConfigPath, 'main.ts')
const storybookPreviewBodyFilePath = path.join(
storybookConfigPath,
'preview-body.html',
)

// Check if the config files exists yet. If they don't, create 'em!
if (!fs.existsSync(storybookMainFilePath)) {
console.log("Storybook's main.ts not found. Creating it now...")
const mainConfigTemplatePath = path.join(
__dirname,
'templates/main.ts.template',
)
const mainConfigContent = readFile(mainConfigTemplatePath)
writeFile(storybookMainFilePath, mainConfigContent)
console.log('main.ts created!')
}

if (!fs.existsSync(storybookPreviewBodyFilePath)) {
console.log("Storybook's preview-body.html not found. Creating it now...")
const previewBodyTemplatePath = path.join(
__dirname,
'templates/preview-body.html.template',
)
const previewBodyConfigContent = readFile(previewBodyTemplatePath)
writeFile(storybookPreviewBodyFilePath, previewBodyConfigContent)
console.log('preview-body.html created!')
}

let command = ''
const flags = [`--config-dir "${storybookConfigPath}"`]

if (build) {
command = `yarn storybook build ${[
...flags,
`--output-dir "${buildDirectory}"`,
]
.filter(Boolean)
.join(' ')}`
} else if (smokeTest) {
command = `yarn storybook dev ${[
...flags,
`--port ${port}`,
`--smoke-test`,
`--ci`,
`--no-version-updates`,
]
.filter(Boolean)
.join(' ')}`
} else {
command = `yarn storybook dev ${[
...flags,
`--port ${port}`,
`--no-version-updates`,
ci && '--ci',
!open && `--no-open`,
]
.filter(Boolean)
.join(' ')}`
}

try {
await execa.command(command, execaOptions)
} catch (e) {
if ((e as ExecaError).signal !== 'SIGINT') {
console.log(c.error((e as Error).message))
errorTelemetry(process.argv, (e as Error).message)
}
process.exit((e as ExecaError).exitCode ?? 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { StorybookConfig } from 'storybook-framework-redwoodjs-vite'

import { getPaths, importStatementPath } from '@redwoodjs/project-config'

const redwoodProjectPaths = getPaths()

const config: StorybookConfig = {
framework: 'storybook-framework-redwoodjs-vite',

stories: [
`${importStatementPath(
redwoodProjectPaths.web.src
)}/**/*.stories.@(js|jsx|ts|tsx|mdx)`,
],

addons: ['@storybook/addon-essentials'],
}

export default config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="redwood-app"></div>
17 changes: 17 additions & 0 deletions packages/cli-packages/storybook-vite/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
command,
aliases,
description,
builder,
handler,
} from './commands/storybook'

export const commands = [
{
command,
aliases,
description,
builder,
handler,
},
]
Loading

0 comments on commit 285d657

Please sign in to comment.