diff --git a/.storybook/main.ts b/.storybook/main.ts index bdd3d86..527433d 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -5,8 +5,9 @@ const config: StorybookConfig = { // Seems redundant, but this forces Storybook to // default to the jump start index page "../src/stories/*.mdx", - "../src/stories/**/*.mdx" + "../src/stories/**/*.mdx", ], + staticDirs: ["../src/stories/assets"], addons: [ "@storybook/addon-onboarding", "@storybook/addon-links", diff --git a/README.md b/README.md index 6d9db34..c5c9121 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,18 @@ If you want to use jump-start, you probably want to navigate to [jump-start-template](https://github.com/kevinschaul/jump-start-template) instead! +## Installation + This repo contains the `jump-start` command, used to convert a jump-start repo -into a Storybook website. To view the website locally, navigate to your +into a Storybook website. + +``` +npm install +pipx install shot-scraper +shot-scraper install +``` + +To view the website locally, navigate to your jump-start repo (or [mine, for example](https://github.com/kevinschaul/jump-start)), run `npm install` and then `npm run dev`. diff --git a/bin/buildStorybook.ts b/bin/buildStorybook.ts index 306a545..f77c039 100644 --- a/bin/buildStorybook.ts +++ b/bin/buildStorybook.ts @@ -1,12 +1,41 @@ import { join } from "node:path"; +import { spawn } from "node:child_process"; import { copyJumpStartTools, spawnWithIO, symlinkStarters } from "./util"; import updateStories from "../src/util/updateStories"; +import updateScreenshots from "../src/util/updateScreenshots"; const root = join(import.meta.dirname, "../"); type BuildStorybookOpts = { startersDir: string; + updateImages: string; }; +async function startStorybookUpdateScreenshots( + toolsRoot: string, + startersDir: string, + storiesDir: string, +) { + return new Promise((resolve, reject) => { + // Start the storybook server + console.log("Starting dev server to update images"); + const child = spawn("storybook", ["dev", "--ci", "-p", "6006"], { + cwd: toolsRoot, + }); + + // When the dev server logs that storybook has started, call + // updateScreenshots + child.stdout?.on("data", (data) => { + if (/Storybook .* started/.test(data.toString())) { + console.log("Updating images"); + updateScreenshots(startersDir, storiesDir); + child.kill(); + console.log("Images updated"); + resolve(true); + } + }); + }); +} + const buildStorybook = async (opts: BuildStorybookOpts) => { console.log(`Using startersDir: ${opts.startersDir}`); @@ -14,14 +43,24 @@ const buildStorybook = async (opts: BuildStorybookOpts) => { symlinkStarters(toolsRoot, opts.startersDir); // Rewrite stories now - console.log('Updating stories') + console.log("Updating stories"); const storiesDir = join(toolsRoot, "./src/stories"); updateStories(opts.startersDir, storiesDir); - console.log('Update stories complete.') + console.log("Update stories complete."); + + if (opts.updateImages) { + await startStorybookUpdateScreenshots( + toolsRoot, + opts.startersDir, + storiesDir, + ); + } // Build the site - const outDir = join(opts.startersDir, "dist") + const outDir = join(opts.startersDir, "dist"); console.log(`Building site to ${outDir}`); - spawnWithIO("storybook", ["build", "--output-dir", outDir], { cwd: toolsRoot }); + spawnWithIO("storybook", ["build", "--output-dir", outDir], { + cwd: toolsRoot, + }); }; export default buildStorybook; diff --git a/bin/cli.ts b/bin/cli.ts index 8db0ce5..350329b 100755 --- a/bin/cli.ts +++ b/bin/cli.ts @@ -22,7 +22,9 @@ Arguments passed following "--" are passed along to storybook, e.g.: "Directory where starters are. Defaults to cwd.", process.cwd(), ) - .option("--no-watch", "Don't watch for file changes", process.cwd()) + .option("--no-watch", "Don't watch for file changes") + .option("--update-images", "Update the preview images", false) + .option("--no-update-images", "Don't update the preview images", true) .action(storybook); program @@ -33,6 +35,8 @@ program "Directory where starters are. Defaults to cwd.", process.cwd(), ) + .option("--update-images", "Update the preview images", true) + .option("--no-update-images", "Don't update the preview images", false) .action(buildStorybook); program @@ -45,14 +49,4 @@ program ) .action(updateReadme); -// TODO -// program -// .command("update-screenshots") -// .option( -// "--starters-dir ", -// "Directory where starters are. Defaults to cwd.", -// process.cwd(), -// ) -// .action(updateScreenshots); - program.parse(); diff --git a/bin/storybook.ts b/bin/storybook.ts index bdea358..6d67443 100644 --- a/bin/storybook.ts +++ b/bin/storybook.ts @@ -3,11 +3,13 @@ import { watch } from "chokidar"; import { copyJumpStartTools, spawnWithIO, symlinkStarters } from "./util"; import updateStories from "../src/util/updateStories"; import { Command } from "commander"; +import updateScreenshots from "../src/util/updateScreenshots"; const root = join(import.meta.dirname, "../"); type StorybookOpts = { startersDir: string; noWatch: boolean; + updateImages: boolean; }; const storybook = async (opts: StorybookOpts, command: Command) => { @@ -17,10 +19,10 @@ const storybook = async (opts: StorybookOpts, command: Command) => { symlinkStarters(toolsRoot, opts.startersDir); // Rewrite stories now and any time a change is made to the starters - console.log('Updating stories') + console.log("Updating stories"); const storiesDir = join(toolsRoot, "./src/stories"); updateStories(opts.startersDir, storiesDir); - console.log('Update stories complete.') + console.log("Update stories complete."); if (!opts.noWatch) { const watcher = watch(opts.startersDir, { @@ -35,6 +37,20 @@ const storybook = async (opts: StorybookOpts, command: Command) => { // Start the storybook server, including any additional commands passed // through - spawnWithIO("storybook", ["dev", "--ci", "-p", "6006", ...command.args], { cwd: toolsRoot }); + const child = spawnWithIO( + "storybook", + ["dev", "--ci", "-p", "6006", ...command.args], + { cwd: toolsRoot }, + ); + + if (opts.updateImages) { + // When the dev server logs that storybook has started, call + // updateScreenshots + child.stdout?.on("data", (data) => { + if (/Storybook .* started/.test(data.toString())) { + updateScreenshots(opts.startersDir, storiesDir); + } + }); + } }; export default storybook; diff --git a/bin/util.ts b/bin/util.ts index 8f28040..e8327d8 100644 --- a/bin/util.ts +++ b/bin/util.ts @@ -1,11 +1,15 @@ import { join } from "node:path"; -import { spawn } from "node:child_process"; +import { SpawnOptions, spawn } from "node:child_process"; import { cpSync, rmSync, symlinkSync, unlinkSync } from "node:fs"; +export function getToolsRoot(startersDir: string) { + return join(startersDir, "./.build/jump-start-tools"); +} + export function copyJumpStartTools(root: string, startersDir: string) { // Copy jump-start-tools out of node_modules to avoid compilation errors with // storybook - const toolsRoot = join(startersDir, "./.build/jump-start-tools"); + const toolsRoot = getToolsRoot(startersDir); console.log(`Copying ${root} to ${toolsRoot}`); rmSync(toolsRoot, { recursive: true, force: true }); cpSync(root, toolsRoot, { recursive: true }); @@ -22,19 +26,21 @@ export function symlinkStarters(root: string, startersDir: string) { symlinkSync(startersDir, symlinkPath); } -export function spawnWithIO(command: string, args: string[], options) { +export function spawnWithIO(command: string, args: string[], options: SpawnOptions) { console.log("Starting server"); const child = spawn(command, args, options); - child.stdout.on("data", (data) => { + child.stdout?.on("data", (data) => { process.stdout.write(data); }); - child.stderr.on("data", (data) => { + child.stderr?.on("data", (data) => { process.stderr.write(data); }); child.on("close", (code) => { console.log(`child process exited with code ${code}`); }); + + return child } diff --git a/package.json b/package.json index ba6e01d..1d48bb4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "jump-start": "bin/cli.ts" }, "scripts": { - "dev": "./bin/cli.ts storybook --starters-dir ../jump-start", + "dev": "./bin/cli.ts storybook --starters-dir ../jump-start --update-images", "build": "./bin/cli.ts build-storybook --starters-dir ../jump-start", "test": "vitest" }, diff --git a/src/util/updateScreenshots.ts b/src/util/updateScreenshots.ts index 1b13241..04368a9 100644 --- a/src/util/updateScreenshots.ts +++ b/src/util/updateScreenshots.ts @@ -6,45 +6,39 @@ import "dotenv/config"; import { parseStarters } from "./parseStarters"; import { execSync } from "child_process"; -if (path.basename(import.meta.url) === "updateScreenshots.ts") { - let startersPath = process.env["JUMP_START_STARTERS"]; - - if (!startersPath) { - console.log(`No env variable found: JUMP_START_STARTER`); - console.log(`Using JUMP_START_STARTER=./ by default`); - startersPath = "./"; - } - - const groups = parseStarters(startersPath); +export default function updateScreenshots( + startersDir: string, + storiesDir: string, +) { + const groups = parseStarters(startersDir); for (const group in groups) { for (const starter of groups[group]) { if (starter.preview) { - const outDir = path.join( - startersPath, - "jump-start-gallery", - "public", - "screenshots", - starter.group, - ); + const outDir = path.join(storiesDir, "assets", starter.group); const outFile = path.join(outDir, `${starter.title}.png`); try { mkdirSync(outDir); } catch (e) {} - console.log(`Taking screenshot of ${starter.group}/${starter.title}`); + // Note: This assumes that the storybook dev server is running, and specifically at port 6006 + const url = `http://localhost:6006/iframe.html?viewMode=docs&id=${starter.group.toLowerCase()}-${starter.title.toLowerCase()}--docs`; + + console.log(`Taking screenshot of ${starter.group}/${starter.title} at ${url}`); execSync( ` shot-scraper \ - http://localhost:3000/${starter.group}/${starter.title} \ + "${url}" \ --selector '.starter-preview iframe' \ --wait-for 'document.querySelector(".starter-preview[data-has-rendered=true]")' \ --javascript 'document.head.appendChild(document.createElement("style")).innerHTML = "\ - .sp-preview-actions { display: none; } \ + .starter-preview iframe { flex-grow: 0; } \ + .sp-preview-actions { display: none !important; } \ ";' \ + --wait 1000 \ --width 400 \ --output ${outFile} `, - { timeout: 30000 }, + { timeout: 10000 }, ); } } diff --git a/src/util/updateStories.ts b/src/util/updateStories.ts index 474a834..757b5bc 100644 --- a/src/util/updateStories.ts +++ b/src/util/updateStories.ts @@ -12,18 +12,24 @@ export default function updateStories(startersDir: string, storiesDir: string) { try { fs.mkdirSync(storiesDir, { recursive: true }); + fs.mkdirSync(path.join(storiesDir, "assets"), { recursive: true }); } catch (e) { null; } - const readme = fs.readFileSync( - path.join(startersDir, "README.md"), - "utf-8", - ); + const startersWithPreviews = Object.values(groups) + .flat() + .filter((d) => d.preview); + + const startersWithPreviewsMd = startersWithPreviews.map((starter) => { + return `![Preview of ${starter.group}/${starter.title}](${starter.group}/${starter.title}.png)`; + }); + + const readme = fs.readFileSync(path.join(startersDir, "README.md"), "utf-8"); const readmeWithoutStarters = rewriteReadmeSection( readme, "## Starters", - "View available starters on the left", + `View available starters on the left\n\n${startersWithPreviewsMd}`, ); // Write out an overview story