-
-
Notifications
You must be signed in to change notification settings - Fork 37
Feat: Add Storybook support #687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { storybookIntegration } from "./storybook.js"; | ||
| import type { BatiArgDef, Integration, IntegrationContext } from "./types.js"; | ||
|
|
||
| export const integrations: ReadonlyArray<Integration> = [storybookIntegration]; | ||
|
|
||
| function isEnabled(args: Record<string, unknown>, flag: string): boolean { | ||
| return args[flag] === true; | ||
| } | ||
|
|
||
| export function getIntegrationArgDefs(): Record<string, BatiArgDef> { | ||
| return Object.fromEntries(integrations.map((integration) => [integration.flag, integration.arg])); | ||
| } | ||
|
|
||
| export function getEnabledIntegrations(args: Record<string, unknown>): Integration[] { | ||
| return integrations.filter((integration) => isEnabled(args, integration.flag)); | ||
| } | ||
|
|
||
| export async function runEnabledIntegrations( | ||
| enabledIntegrations: ReadonlyArray<Integration>, | ||
| context: IntegrationContext, | ||
| ): Promise<Integration[]> { | ||
| const appliedIntegrations: Integration[] = []; | ||
|
|
||
| for (const integration of enabledIntegrations) { | ||
| const wasApplied = await integration.run(context); | ||
| if (wasApplied === false) { | ||
| continue; | ||
| } | ||
|
|
||
| appliedIntegrations.push(integration); | ||
| } | ||
|
|
||
| return appliedIntegrations; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { execSync } from "node:child_process"; | ||
| import { confirm } from "@inquirer/prompts"; | ||
| import type { Feature } from "@batijs/features"; | ||
| import { red } from "colorette"; | ||
| import type { Integration } from "./types.js"; | ||
|
|
||
| const supportedStorybookFrameworks = ["react", "vue", "solid"]; | ||
|
|
||
| export function getUiFrameworkFlag(flags: string[], allFeatures: ReadonlyArray<Feature>) { | ||
| const uiFrameworkFlags: string[] = allFeatures | ||
| .filter((feature) => feature.category === "UI Framework") | ||
| .map((feature) => feature.flag); | ||
| return flags.find((flag) => uiFrameworkFlags.includes(flag)); | ||
| } | ||
|
|
||
| export function isStorybookFrameworkSupported(uiFramework: string | undefined) { | ||
| return Boolean(uiFramework && supportedStorybookFrameworks.includes(uiFramework)); | ||
| } | ||
|
|
||
| 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" }); | ||
| return true; | ||
| } | ||
|
|
||
| export const storybookIntegration: Integration = { | ||
| flag: "storybook", | ||
| label: "Storybook", | ||
| arg: { | ||
| type: "boolean", | ||
| description: "If true, initializes Storybook in the generated app (React, Vue, Solid only)", | ||
| required: false, | ||
| }, | ||
| async run({ project, flags, allFeatures, packageManagerExec }) { | ||
| const uiFramework = getUiFrameworkFlag(flags, allFeatures); | ||
|
|
||
| if (!isStorybookFrameworkSupported(uiFramework)) { | ||
| console.error(`${red("⚠")} The \`--storybook\` flag is currently supported only with React, Vue, or Solid.`); | ||
| process.exit(6); | ||
| } | ||
|
|
||
| return await initStorybook(project, packageManagerExec); | ||
| }, | ||
| nextSteps(packageManagerRun) { | ||
| return [{ type: "command", step: `${packageManagerRun} storybook` }]; | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { describeBati } from "@batijs/tests-utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const matrix = [["solid", "react", "vue"], "storybook", "eslint"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await describeBati(({ test, expect }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Line 10 and Line 33 invoke 💡 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: vikejs/bati
Length of output: 782
🏁 Script executed:
Repository: vikejs/bati
Length of output: 2090
🏁 Script executed:
Repository: vikejs/bati
Length of output: 2526
🏁 Script executed:
Repository: vikejs/bati
Length of output: 106
🏁 Script executed:
Repository: vikejs/bati
Length of output: 3008
🏁 Script executed:
Repository: vikejs/bati
Length of output: 1488
🏁 Script executed:
Repository: vikejs/bati
Length of output: 2789
Make
--storybookexplicitly non-interactive when the CLI is unattended.Line 23 defaults
interactivetotrue, and the only callsite on line 57 relies on that default without passing an explicit value. TheIntegrationContextpassed tointegration.run()at index.ts:626 does not include CI/test mode information, even thoughBATI_IS_CIandBATI_TESTare already available inmetaat that location. This prevents integrations from disabling prompts in CI, test, or scripted runs.Add an
interactivefield toIntegrationContext(derived fromBATI_IS_CIandBATI_TESTin index.ts) and pass it toinitStorybook()at line 57.🤖 Prompt for AI Agents
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@magne4000Agreed! Here are the three changes needed to wire up a proper non-interactive mode:1.
packages/cli/types.ts— extendIntegrationContextexport interface IntegrationContext { project: string; flags: string[]; allFeatures: ReadonlyArray<Feature>; packageManagerExec: string; + interactive: boolean; }2.
packages/cli/index.ts— deriveinteractivefromBATI_IS_CI/BATI_TESTwhen building the context3.
packages/cli/storybook.ts— passcontext.interactivetoinitStorybookThis way:
--yesif the user accepts.BATI_IS_CIorBATI_TEST):interactive = false, soshouldUseDefaultConfig = trueand--yesis always passed, skipping all prompts with sensible defaults.🧠 Learnings used