Skip to content

Commit

Permalink
feat(cli): Lazy install storybook (#8454)
Browse files Browse the repository at this point in the history
* initial package skeleton implemented

* Remove package versioning for just now

* move dependencies around

* Update package installing.

Packages no longer require a version and will default to the same version as the redwood CLI if no version is specified.

* Remove bin handling from esbuild

* Update toml defaults

* Add aliases to the storybook command

* Add default `command-cache.json` to crwa templates

* Apply suggestions from code review
  • Loading branch information
Josh-Walker-GM authored Jun 1, 2023
1 parent 3d8c540 commit 8fb0f86
Show file tree
Hide file tree
Showing 21 changed files with 1,499 additions and 1,166 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"license": "MIT",
"workspaces": [
"packages/*",
"packages/auth-providers/*/*"
"packages/auth-providers/*/*",
"packages/cli-packages/*"
],
"scripts": {
"build": "lerna run build",
Expand Down
3 changes: 3 additions & 0 deletions packages/cli-packages/storybook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CLI Packages - Storybook

**WIP**: This package is the first example of extracting a command from `@redwoodjs/cli` into it's own CLI plugin package.
23 changes: 23 additions & 0 deletions packages/cli-packages/storybook/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import fs from 'node:fs'

import * as esbuild from 'esbuild'
import fg from 'fast-glob'

// Get source files
const sourceFiles = fg.sync(['./src/**/*.ts'])

// Build general source files
const result = await esbuild.build({
entryPoints: sourceFiles,
format: 'cjs',
platform: 'node',
target: ['node18'],
outdir: 'dist',
logLevel: 'info',

// For visualizing the bundle.
// See https://esbuild.github.io/api/#metafile and https://esbuild.github.io/analyze/.
metafile: true,
})

fs.writeFileSync('meta.json', JSON.stringify(result.metafile))
47 changes: 47 additions & 0 deletions packages/cli-packages/storybook/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@redwoodjs/cli-storybook",
"version": "5.0.0",
"repository": {
"type": "git",
"url": "https://github.com/redwoodjs/redwood.git",
"directory": "packages/cli-packages/storybook"
},
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "yarn node ./build.mjs && yarn build:types",
"build:types": "tsc --build --verbose",
"build:watch": "nodemon --watch src --ext \"js,ts,tsx\" --ignore dist --exec \"yarn build\"",
"prepublishOnly": "NODE_ENV=production yarn build"
},
"jest": {
"testPathIgnorePatterns": [
"/dist/"
]
},
"gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1",
"dependencies": {
"@redwoodjs/project-config": "5.0.0",
"@redwoodjs/telemetry": "5.0.0",
"@storybook/addon-a11y": "7.0.18",
"@storybook/addon-docs": "7.0.18",
"@storybook/addon-essentials": "7.0.18",
"@storybook/react-webpack5": "7.0.18",
"chalk": "4.1.2",
"execa": "5.1.1",
"lodash.memoize": "4.1.2",
"storybook": "7.0.18",
"terminal-link": "2.1.1",
"yargs": "17.7.2"
},
"devDependencies": {
"esbuild": "0.17.19",
"fast-glob": "3.2.12",
"jest": "29.5.0",
"typescript": "5.1.3"
}
}
Original file line number Diff line number Diff line change
@@ -1,47 +1,58 @@
import terminalLink from 'terminal-link'

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

export const command = 'storybook'
export const aliases = ['sb']

export const description =
'Launch Storybook: a tool for building UI components in isolation'
'Launch Storybook: a tool for building UI components and pages in isolation'

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

export function builder(yargs) {
// TODO: Provide a type for the `yargs` argument
export const builder = (yargs: any) => {
yargs
.option('build', {
describe: 'Build Storybook',
type: 'boolean',
default: false,
default: defaultOptions.build,
})
.option('build-directory', {
describe: 'Directory in web/ to store static files',
type: 'string',
default: 'public/storybook',
default: defaultOptions.buildDirectory,
})
.option('ci', {
describe: 'Start server in CI mode, with no interactive prompts',
type: 'boolean',
default: false,
default: defaultOptions.ci,
})
.option('open', {
describe: 'Open storybook in your browser on start',
type: 'boolean',
default: true,
default: defaultOptions.open,
})
.option('port', {
describe: 'Which port to run storybook on',
type: 'integer',
default: 7910,
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: false,
default: defaultOptions.smokeTest,
})
.check((argv) => {
// TODO: Provide a type for the `argv` argument
.check((argv: any) => {
if (argv.build && argv.smokeTest) {
throw new Error('Can not provide both "--build" and "--smoke-test"')
}
Expand All @@ -64,7 +75,10 @@ export function builder(yargs) {
)
}

export async function handler(options) {
const { handler } = await import('./storybookHandler')
await handler(options)
export const handler = async (options: StorybookYargsOptions) => {
// 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.
const { handler: storybookHandler } = await import('./storybookHandler.js')
await storybookHandler(options)
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
import path from 'path'
import path from 'node:path'

import execa from 'execa'

import { getPaths } from '@redwoodjs/project-config'
// Allow import of untyped package
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { errorTelemetry } from '@redwoodjs/telemetry'

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

const redwoodProjectPaths = getPaths()

export const handler = ({
export const handler = async ({
build,
buildDirectory,
ci,
open,
port,
smokeTest,
}) => {
const cwd = redwoodProjectPaths.web.base
}: StorybookYargsOptions) => {
const cwd = getPaths().web.base
const staticAssetsFolder = path.join(cwd, 'public')
const execaOptions = {
const execaOptions: Partial<execa.Options> = {
stdio: 'inherit',
shell: true,
cwd,
}

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

const storybookConfigPath = path.dirname(
require.resolve('@redwoodjs/testing/config/storybook/main.js')
)

/** @type {string?} */
let command
let command = ''
const flags = [`--config-dir "${storybookConfigPath}"`]

if (build) {
Expand Down Expand Up @@ -66,10 +70,10 @@ export const handler = ({
}

try {
execa.command(command, execaOptions)
await execa.command(command, execaOptions)
} catch (e) {
console.log(c.error(e.message))
errorTelemetry(process.argv, e.message)
console.log(c.error((e as Error).message))
errorTelemetry(process.argv, (e as Error).message)
process.exit(1)
}
}
17 changes: 17 additions & 0 deletions packages/cli-packages/storybook/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,
},
]
21 changes: 21 additions & 0 deletions packages/cli-packages/storybook/src/lib/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import chalk from 'chalk'

/**
* To keep a consistent color/style palette between cli packages, such as
* \@redwood/cli and \@redwood/create-redwood-app, please keep them compatible
* with one and another. We'll might split up and refactor these into a
* separate package when there is a strong motivation behind it.
*
* Current files:
*
* - packages/cli/src/lib/colors.js (this file)
* - packages/create-redwood-app/src/create-redwood-app.js
*/
export default {
error: chalk.bold.red,
warning: chalk.keyword('orange'),
green: chalk.green,
info: chalk.grey,
bold: chalk.bold,
underline: chalk.underline,
}
19 changes: 19 additions & 0 deletions packages/cli-packages/storybook/src/lib/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { memoize } from 'lodash'

import { getPaths as getRedwoodPaths } from '@redwoodjs/project-config'

import c from './colors'

/**
* This wraps the core version of getPaths into something that catches the exception
* and displays a helpful error message.
*/
export const _getPaths = () => {
try {
return getRedwoodPaths()
} catch (e) {
console.error(c.error((e as Error).message))
process.exit(1)
}
}
export const getPaths = memoize(_getPaths)
8 changes: 8 additions & 0 deletions packages/cli-packages/storybook/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface StorybookYargsOptions {
open: boolean
build: boolean
ci: boolean
port: number
buildDirectory: string
smokeTest: boolean
}
10 changes: 10 additions & 0 deletions packages/cli-packages/storybook/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.compilerOption.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
"outDir": "dist"
},
"include": ["src"],
}
2 changes: 0 additions & 2 deletions packages/cli/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import * as prismaCommand from './commands/prisma'
import * as recordCommand from './commands/record'
import * as serveCommand from './commands/serve'
import * as setupCommand from './commands/setup'
import * as storybookCommand from './commands/storybook'
import * as testCommand from './commands/test'
import * as tstojsCommand from './commands/ts-to-js'
import * as typeCheckCommand from './commands/type-check'
Expand Down Expand Up @@ -142,7 +141,6 @@ async function runYargs() {
.command(recordCommand)
.command(serveCommand)
.command(setupCommand)
.command(storybookCommand)
.command(testCommand)
.command(tstojsCommand)
.command(typeCheckCommand)
Expand Down
21 changes: 13 additions & 8 deletions packages/cli/src/lib/packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,26 @@ import { getPaths } from './index'

/**
* Installs a module into a user's project. If the module is already installed,
* this function does nothing.
* this function does nothing. If no version is specified, the version will be assumed
* to be the same as that of \@redwoodjs/cli.
*
* @param {string} name The name of the module to install
* @param {string} version The version of the module to install
* @param {string} version The version of the module to install, otherwise the same as that of \@redwoodjs/cli
* @param {boolean} isDevDependency Whether to install as a devDependency or not
* @returns Whether the module was installed or not
*/
export async function installModule(name, version, isDevDependency = true) {
export async function installModule(name, version = undefined) {
if (isModuleInstalled(name)) {
return false
}
await execa.command(
`yarn add ${isDevDependency ? '-D' : ''} ${name}@${version}`,
{
if (version === undefined) {
return await installRedwoodModule(name)
} else {
await execa.command(`yarn add -D ${name}@${version}`, {
stdio: 'inherit',
cwd: getPaths().base,
}
)
})
}
return true
}

Expand All @@ -34,6 +36,7 @@ export async function installModule(name, version, isDevDependency = true) {
* If no remote version can not be found which matches the local cli version then the latest canary version will be used.
*
* @param {string} module A redwoodjs module, e.g. \@redwoodjs/web
* @returns {boolean} Whether the module was installed or not
*/
export async function installRedwoodModule(module) {
const packageJsonPath = require.resolve('@redwoodjs/cli/package.json')
Expand Down Expand Up @@ -63,7 +66,9 @@ export async function installRedwoodModule(module) {
stdio: 'inherit',
cwd: getPaths().base,
})
return true
}
return false
}

/**
Expand Down
Loading

0 comments on commit 8fb0f86

Please sign in to comment.