Skip to content

Feat: Add Storybook support#687

Closed
yannbf wants to merge 3 commits intovikejs:mainfrom
yannbf:feat/add-storybook-integration
Closed

Feat: Add Storybook support#687
yannbf wants to merge 3 commits intovikejs:mainfrom
yannbf:feat/add-storybook-integration

Conversation

@yannbf
Copy link

@yannbf yannbf commented Mar 9, 2026

This PR introduces support for Storybook via a new --storybook flag.

As Storybook is the first integration that brings in a CLI execution (the other integrations being boilerplates), I wasn't sure what was the recommended way to add this as integration, so I created the concept of "integrations" which will contain a flag, label, a run function (which would then trigger some validations and the actual CLI) and a nextSteps function to calculate what to show in the next steps section.

It looks like the following:
image

Note: This is a conversation starter, we can discuss and change the code to whatever works best for Bati. I'd be happy if we could provide Storybook as an option for folks to use with Vike!

Summary by CodeRabbit

  • New Features

    • Added Storybook initialization support via a new --storybook CLI option for React, Vue, and Solid projects.
    • Introduced a pluggable integration system for future CLI-only enhancements.
  • Tests

    • Added test coverage for Storybook configuration files and package scripts validation.
  • Documentation

    • Updated documentation to clarify CLI option organization between feature flags and integrations.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a modular CLI integration system that extends CLI functionality beyond built-in feature flags. It defines new types (Integration, IntegrationContext), adds an integrations registry with management utilities, implements Storybook as the first integration, and integrates the system into the main CLI flow with support for integration-specific next steps.

Changes

Cohort / File(s) Summary
Type System & Core Infrastructure
packages/cli/types.ts, packages/cli/integrations.ts
Added Integration and IntegrationContext types; introduced BatiArgDef type alias. Created new integrations registry module with getIntegrationArgDefs(), getEnabledIntegrations(), and runEnabledIntegrations() utilities for managing CLI integrations.
CLI Main Module
packages/cli/index.ts
Integrated the new integration system into CLI argument parsing and boilerplate generation. Added isFeatureFlag() helper, imported integration utilities, incorporated integration args into composed CLI arguments, and extended printOK() to display integration-specific next steps and labels.
Storybook Integration
packages/cli/storybook.ts
Implemented first CLI integration module with framework detection (getUiFrameworkFlag(), isStorybookFrameworkSupported()), Storybook initialization (initStorybook()), and exported storybookIntegration descriptor with run logic, label, and next steps guidance.
Tests & Documentation
packages/tests/src/load-test-files.ts, packages/tests/tests/FRAMEWORK+storybook.spec.ts, packages/features/README.md, AGENTS.md
Extended test matrix validation to include "storybook" key; added new test spec checking for Storybook config files and npm scripts; clarified in documentation that CLI options are split between feature flags and integrations; updated AGENTS.md with new public APIs.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: Add Storybook support' directly and clearly summarizes the main change—introducing Storybook support via a new CLI integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/index.ts (1)

621-631: ⚠️ Potential issue | 🟠 Major

Run integrations before creating the initial git commit.

Lines 621-623 create the repo snapshot before Lines 625-631 apply Storybook. A default bati --storybook will therefore leave the freshly generated repo dirty, because .storybook/* and the package.json updates happen after the initial commit.

💡 Suggested change
-      if (!args["skip-git"]) {
-        gitInit(args.project);
-      }
-
       const enabledIntegrations = getEnabledIntegrations(args as unknown as Record<string, unknown>);
       const appliedIntegrations = await runEnabledIntegrations(enabledIntegrations, {
         project: args.project,
         flags,
         allFeatures: features,
         packageManagerExec: pm.exec,
       });
+
+      if (!args["skip-git"]) {
+        gitInit(args.project);
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/index.ts` around lines 621 - 631, The initial git snapshot is
created via gitInit before integrations run, which causes generated files (e.g.,
.storybook and package.json changes applied by
getEnabledIntegrations/runEnabledIntegrations) to appear as uncommitted; move
the integration execution to occur before calling gitInit so the repo includes
integration changes in the initial commit, preserving the skip-git behavior by
only calling gitInit(args.project) after awaiting runEnabledIntegrations when
args["skip-git"] is falsy.
🧹 Nitpick comments (1)
packages/tests/src/load-test-files.ts (1)

38-38: Don’t special-case Storybook in the matrix allowlist.

packages/cli/integrations.ts is now the source of truth for CLI-only flags. Hardcoding "storybook" here means the next integration will need a second manual allowlist update, and the test matrix can drift from the real CLI surface. Prefer sourcing integration keys from the registry or from a shared helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tests/src/load-test-files.ts` at line 38, Remove the hardcoded
"storybook" special-case in the validKeys Set in load-test-files.ts and instead
derive CLI-only integration keys from the canonical source
(packages/cli/integrations.ts or the shared helper/registry) so the test matrix
stays in sync; replace the current new Set<unknown>([...flags, "storybook"])
usage by merging flags with the exported integration keys (import the relevant
symbol from integrations.ts) and construct the Set from that combined list
(referencing the validKeys and flags identifiers to locate where to change it).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/storybook.ts`:
- Around line 20-37: Add an explicit interactive flag to the integration context
and thread it into initStorybook so Storybook is non-interactive in CI/test
runs: extend IntegrationContext with an interactive boolean derived from
BATI_IS_CI || BATI_TEST in index.ts, ensure integration.run(...) receives that
context, and update the callsite that invokes initStorybook (the call to
initStorybook(...) around the integration.run path) to pass the interactive
value instead of relying on the default; this ensures initStorybook(interactive)
will run with interactive=false for unattended CI/test environments.

In `@packages/tests/tests/FRAMEWORK`+storybook.spec.ts:
- Around line 6-39: Replace the use of child_process.execSync("pwd", ...) with
the cross-platform Node API process.cwd() in both tests: use process.cwd() for
storybookDir in the "storybook config files" test (where storybookDir is set)
and for cwd in the "storybook scripts" test (where packageJsonPath is built);
update references to storybookDir and cwd accordingly so the tests no longer
shell out and work on Windows.

---

Outside diff comments:
In `@packages/cli/index.ts`:
- Around line 621-631: The initial git snapshot is created via gitInit before
integrations run, which causes generated files (e.g., .storybook and
package.json changes applied by getEnabledIntegrations/runEnabledIntegrations)
to appear as uncommitted; move the integration execution to occur before calling
gitInit so the repo includes integration changes in the initial commit,
preserving the skip-git behavior by only calling gitInit(args.project) after
awaiting runEnabledIntegrations when args["skip-git"] is falsy.

---

Nitpick comments:
In `@packages/tests/src/load-test-files.ts`:
- Line 38: Remove the hardcoded "storybook" special-case in the validKeys Set in
load-test-files.ts and instead derive CLI-only integration keys from the
canonical source (packages/cli/integrations.ts or the shared helper/registry) so
the test matrix stays in sync; replace the current new Set<unknown>([...flags,
"storybook"]) usage by merging flags with the exported integration keys (import
the relevant symbol from integrations.ts) and construct the Set from that
combined list (referencing the validKeys and flags identifiers to locate where
to change it).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: abc5d2e6-4c1a-4aea-ba87-cc877b2d68ae

📥 Commits

Reviewing files that changed from the base of the PR and between 08bfa12 and 75ea32a.

📒 Files selected for processing (8)
  • AGENTS.md
  • packages/cli/index.ts
  • packages/cli/integrations.ts
  • packages/cli/storybook.ts
  • packages/cli/types.ts
  • packages/features/README.md
  • packages/tests/src/load-test-files.ts
  • packages/tests/tests/FRAMEWORK+storybook.spec.ts

Comment on lines +20 to +37
export async function initStorybook(
cwd: string,
packageManagerExec: string,
interactive: boolean = true,
): Promise<boolean> {
let shouldUseDefaultConfig = !interactive;

// Prompt user if they want to initialize Storybook
if (interactive) {
shouldUseDefaultConfig = await confirm({
message: "Use default Storybook configuration?",
default: true,
});
}

// Run Storybook init with interactive questionnaire
const command = `${packageManagerExec} storybook@latest init --no-dev${shouldUseDefaultConfig ? " --yes" : ""}`;
execSync(command, { cwd, stdio: "inherit" });
Copy link

@coderabbitai coderabbitai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== initStorybook call sites ==\n'
rg -n 'initStorybook\(' packages/cli

printf '\n== prompt / CI / TTY plumbing in packages/cli ==\n'
rg -n 'confirm\(|interactive|BATI_TEST|process\.env\.CI|isTTY' packages/cli

Repository: vikejs/bati

Length of output: 782


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== IntegrationContext definition ==\n'
rg -B3 -A10 'interface IntegrationContext|type IntegrationContext' packages/cli

printf '\n== storybookIntegration.run() definition ==\n'
rg -B5 -A15 'storybookIntegration.*=|const storybookIntegration' packages/cli

printf '\n== How integrations are called in index.ts ==\n'
rg -B3 -A3 'storybookIntegration\.run\(|integration\.run\(' packages/cli/index.ts

printf '\n== Other integration implementations for interactive handling ==\n'
fd '\.(ts|tsx)$' packages/cli/integrations --exec rg -l 'interactive|BATI_TEST|confirm' {} \;

Repository: vikejs/bati

Length of output: 2090


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Integration.run() call site in index.ts (context around line 577) ==\n'
sed -n '540,620p' packages/cli/index.ts

printf '\n== Check how IntegrationContext is constructed ==\n'
rg -B10 -A5 'as IntegrationContext|IntegrationContext.*=' packages/cli/index.ts | head -60

printf '\n== Another integration implementation (e.g., tailwind) ==\n'
ls -la packages/cli/integrations/ 2>/dev/null || ls -la packages/cli/*.ts | grep -E '(tailwind|prettier|vitest)' || true

Repository: vikejs/bati

Length of output: 2526


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Direct search for integration.run invocation ==\n'
rg -n -B5 -A5 'integration\.run\(' packages/cli/index.ts

printf '\n== Search for all Integration usage in index.ts ==\n'
rg -n 'integrations|Integration' packages/cli/index.ts

printf '\n== Full storybook.ts file to see the full picture ==\n'
wc -l packages/cli/storybook.ts

Repository: vikejs/bati

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Full storybook.ts content ==\n'
cat -n packages/cli/storybook.ts

printf '\n== Search for storybookIntegration usage across codebase ==\n'
rg -n 'storybookIntegration' --type ts packages/cli

Repository: vikejs/bati

Length of output: 3008


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== integrations.ts full content ==\n'
cat -n packages/cli/integrations.ts

printf '\n== Search for integrations array usage ==\n'
rg -n 'integrations\[|for.*integrations|integrations\..*run\|integrations\.filter' packages/cli

printf '\n== Search for where integration features are added to boilerplates ==\n'
rg -n 'storybookIntegration\|integrations' packages/cli/index.ts | head -20

Repository: vikejs/bati

Length of output: 1488


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Search for runEnabledIntegrations invocation ==\n'
rg -n -B10 -A10 'runEnabledIntegrations' packages/cli/index.ts

printf '\n== Check types.ts Integration interface fully ==\n'
rg -B5 -A15 'export interface Integration' packages/cli/types.ts

Repository: vikejs/bati

Length of output: 2789


Make --storybook explicitly non-interactive when the CLI is unattended.

Line 23 defaults interactive to true, and the only callsite on line 57 relies on that default without passing an explicit value. The IntegrationContext passed to integration.run() at index.ts:626 does not include CI/test mode information, even though BATI_IS_CI and BATI_TEST are already available in meta at that location. This prevents integrations from disabling prompts in CI, test, or scripted runs.

Add an interactive field to IntegrationContext (derived from BATI_IS_CI and BATI_TEST in index.ts) and pass it to initStorybook() at line 57.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/storybook.ts` around lines 20 - 37, Add an explicit interactive
flag to the integration context and thread it into initStorybook so Storybook is
non-interactive in CI/test runs: extend IntegrationContext with an interactive
boolean derived from BATI_IS_CI || BATI_TEST in index.ts, ensure
integration.run(...) receives that context, and update the callsite that invokes
initStorybook (the call to initStorybook(...) around the integration.run path)
to pass the interactive value instead of relying on the default; this ensures
initStorybook(interactive) will run with interactive=false for unattended
CI/test environments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will indeed need a non-interactive mode for the CI, with sensible default values

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@magne4000 Agreed! Here are the three changes needed to wire up a proper non-interactive mode:

1. packages/cli/types.ts — extend IntegrationContext

 export interface IntegrationContext {
   project: string;
   flags: string[];
   allFeatures: ReadonlyArray<Feature>;
   packageManagerExec: string;
+  interactive: boolean;
 }

2. packages/cli/index.ts — derive interactive from BATI_IS_CI / BATI_TEST when building the context

-const context: IntegrationContext = {
+const context: IntegrationContext = {
   project,
   flags,
   allFeatures: features,
   packageManagerExec,
+  interactive: !meta.BATI_IS_CI && !meta.BATI_TEST,
 };

3. packages/cli/storybook.ts — pass context.interactive to initStorybook

-  const result = await initStorybook(context.project, context.packageManagerExec);
+  const result = await initStorybook(context.project, context.packageManagerExec, context.interactive);

This way:

  • Interactive terminal (normal developer flow): prompts are shown, defaulting to --yes if the user accepts.
  • CI / test runs (BATI_IS_CI or BATI_TEST): interactive = false, so shouldUseDefaultConfig = true and --yes is always passed, skipping all prompts with sensible defaults.

🧠 Learnings used
Learnt from: CR
Repo: vikejs/bati PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T09:49:22.825Z
Learning: Applies to boilerplates/*/bati.config.ts : Create UI-framework-specific boilerplate variants (e.g., react-sentry, vue-sentry) when features require framework-specific code, using combined conditions in bati.config.ts

Comment on lines +6 to +39
test("storybook config files", async () => {
const fs = await import("fs/promises");
const path = await import("path");

const storybookDir = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();

const configExtensions = ["ts", "js", "mjs", "cjs"];
let configFileExists = false;

for (const ext of configExtensions) {
const configPath = path.join(storybookDir, ".storybook", `main.${ext}`);
try {
await fs.access(configPath);
configFileExists = true;
break;
} catch {
// Continue to next extension
}
}

expect(configFileExists).toBe(true);
});

test("storybook scripts", async () => {
const fs = await import("fs/promises");
const path = await import("path");

const cwd = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();

const packageJsonPath = path.join(cwd, "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));

expect(packageJson.scripts.storybook).toBeTruthy();
expect(packageJson.scripts["build-storybook"]).toBeTruthy();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use process.cwd() instead of shelling out to pwd.

Line 10 and Line 33 invoke pwd, which isn’t portable to Windows shells. That will break this new E2E spec as soon as the matrix runs on Windows; process.cwd() is cheaper and cross-platform.

💡 Suggested change
   test("storybook config files", async () => {
     const fs = await import("fs/promises");
     const path = await import("path");
 
-    const storybookDir = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();
+    const storybookDir = process.cwd();
@@
   test("storybook scripts", async () => {
     const fs = await import("fs/promises");
     const path = await import("path");
 
-    const cwd = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();
+    const cwd = process.cwd();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("storybook config files", async () => {
const fs = await import("fs/promises");
const path = await import("path");
const storybookDir = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();
const configExtensions = ["ts", "js", "mjs", "cjs"];
let configFileExists = false;
for (const ext of configExtensions) {
const configPath = path.join(storybookDir, ".storybook", `main.${ext}`);
try {
await fs.access(configPath);
configFileExists = true;
break;
} catch {
// Continue to next extension
}
}
expect(configFileExists).toBe(true);
});
test("storybook scripts", async () => {
const fs = await import("fs/promises");
const path = await import("path");
const cwd = (await import("child_process")).execSync("pwd", { encoding: "utf-8" }).trim();
const packageJsonPath = path.join(cwd, "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
expect(packageJson.scripts.storybook).toBeTruthy();
expect(packageJson.scripts["build-storybook"]).toBeTruthy();
test("storybook config files", async () => {
const fs = await import("fs/promises");
const path = await import("path");
const storybookDir = process.cwd();
const configExtensions = ["ts", "js", "mjs", "cjs"];
let configFileExists = false;
for (const ext of configExtensions) {
const configPath = path.join(storybookDir, ".storybook", `main.${ext}`);
try {
await fs.access(configPath);
configFileExists = true;
break;
} catch {
// Continue to next extension
}
}
expect(configFileExists).toBe(true);
});
test("storybook scripts", async () => {
const fs = await import("fs/promises");
const path = await import("path");
const cwd = process.cwd();
const packageJsonPath = path.join(cwd, "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
expect(packageJson.scripts.storybook).toBeTruthy();
expect(packageJson.scripts["build-storybook"]).toBeTruthy();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tests/tests/FRAMEWORK`+storybook.spec.ts around lines 6 - 39,
Replace the use of child_process.execSync("pwd", ...) with the cross-platform
Node API process.cwd() in both tests: use process.cwd() for storybookDir in the
"storybook config files" test (where storybookDir is set) and for cwd in the
"storybook scripts" test (where packageJsonPath is built); update references to
storybookDir and cwd accordingly so the tests no longer shell out and work on
Windows.

Copy link
Member

@magne4000 magne4000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should reuse (and expand if necessary) the hooks/after.ts pattern for adding those kind of integrations.

Also, features.ts must be used for all integrations, as it is normalized and used by both the CLI, the Web App and the Web Component.
We probably need to create a new sub category (lets call it "Tooling") under "Utilities".

Comment on lines 612 to 614
for (const onafter of hooksMap.get("after") ?? []) {
await onafter(args.project, meta);
}
Copy link
Member

@magne4000 magne4000 Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should now sort after hooks in reverse enforce order (See

boilerplates.sort((b1, b2) => {
).
This will ensure that the shared after hook is executed last.

@magne4000
Copy link
Member

Replaced by #690

@magne4000 magne4000 closed this Mar 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants