Skip to content

Commit

Permalink
feat: add preview images to storybook (#13)
Browse files Browse the repository at this point in the history
Requires shot-scraper to be installed
Adds --update-images and --no-update-images to storybook and
build-storybook subcommands

kevinschaul/jump-start-template#16
  • Loading branch information
kevinschaul authored Jun 24, 2024
1 parent 97ec67d commit c4c0880
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 52 deletions.
3 changes: 2 additions & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
47 changes: 43 additions & 4 deletions bin/buildStorybook.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,66 @@
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}`);

const toolsRoot = copyJumpStartTools(root, opts.startersDir);
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;
16 changes: 5 additions & 11 deletions bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -45,14 +49,4 @@ program
)
.action(updateReadme);

// TODO
// program
// .command("update-screenshots")
// .option(
// "--starters-dir <dir>",
// "Directory where starters are. Defaults to cwd.",
// process.cwd(),
// )
// .action(updateScreenshots);

program.parse();
22 changes: 19 additions & 3 deletions bin/storybook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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, {
Expand All @@ -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;
16 changes: 11 additions & 5 deletions bin/util.ts
Original file line number Diff line number Diff line change
@@ -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 });
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
36 changes: 15 additions & 21 deletions src/util/updateScreenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
);
}
}
Expand Down
16 changes: 11 additions & 5 deletions src/util/updateStories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit c4c0880

Please sign in to comment.