Skip to content

Commit

Permalink
Import CLI configuration into add-on (#1)
Browse files Browse the repository at this point in the history
* Move provider list to a dedicated file

* Load state from an environment variable

* Pass CLI state to add-on

* Fetch branches only if a provider is configured

* Log generic errors during Storybook build

* Fix typo in environment variable

* Allow CLI to be executed on the project

* Move hostname to Storybook add-on parameters

* Add script_name option to README
  • Loading branch information
utarwyn authored Jan 3, 2024
1 parent 8d05a2f commit c414789
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 82 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
dist/
node_modules/
storybook-bundle/
storybook-static/
build-storybook.log
.DS_Store
Expand Down
5 changes: 5 additions & 0 deletions .storybook/.branches.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"from": "storybook-static",
"to": "storybook-bundle",
"default_root": true
}
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ Example : `sb-branch-switcher --config libs/storybook-host/.storybook/.branches.

Here is the explanation of all available options:

| Key | Default | Description |
|----------------|----------------|-----------------------------------------------------------------------|
| from | - | **(mandatory)** Where the Storybook instance is located after a build |
| to | - | **(mandatory)** Where all Storybook instances will be copied |
| directory | current folder | Absolute path where the project belongs |
| default_branch | master | Your default Git branch |
| default_root | true | Copy instance for default branch into root folder |
| provider | - | Configuration to retrieve branches and commits to process |
| Key | Default | Description |
|----------------|-----------------|-----------------------------------------------------------------------|
| from | - | **(mandatory)** Where the Storybook instance is located after a build |
| to | - | **(mandatory)** Where all Storybook instances will be copied |
| directory | current folder | Absolute path where the project belongs |
| script_name | build-storybook | Name of the NPM script that builds the Storybook |
| default_branch | master | Your default Git branch |
| default_root | true | Copy instance for default branch into root folder |
| provider | - | Configuration to retrieve branches and commits to process |

### Bitbucket (opened PRs)

Expand Down
51 changes: 38 additions & 13 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

import "zx/globals";
import { fetchBranches } from "./run/branches";
import { buildStorybook, checkoutCommit, cleanPreviousBundle, prepareStorybook } from "./run/build";
import {
buildStorybook,
checkoutCommit,
cleanPreviousBundle,
prepareAddonState,
prepareStorybook,
} from "./run/build";
import { extractConfiguration, verifyGit, verifyNode } from "./run/setup";

export interface ProviderConfig {
Expand All @@ -18,14 +24,14 @@ export interface Config {
to: string;
default_branch?: string;
default_root?: boolean;
provider: ProviderConfig;
provider?: ProviderConfig;
}

void async function() {
void (async function () {
const configPath = argv["c"] ?? argv["config"] ?? ".storybook/.branches.json";
let config: Config;

console.log(chalk.green('😎 Verify if the workspace is ready'));
console.log(chalk.green("😎 Verifying the workspace..."));
try {
config = await extractConfiguration(configPath);
if (config.directory) {
Expand All @@ -42,21 +48,40 @@ void async function() {
const branches = await fetchBranches(config.provider);
const defaultBranch = config.default_branch ?? branches[0].id;

console.log(chalk.green('Start building the bundle...'));
console.log(chalk.green("Building the bundle..."));
await cleanPreviousBundle(config.to);

for (const branch of branches) {
prepareAddonState({
list: branches.map((branch) => branch.id),
currentBranch: branch.id,
defaultBranch,
});

try {
await spinner(`Fetching ${branch.id}...`, () => checkoutCommit(branch.commit));
await spinner(`Building Storybook for branch ${branch.id}...`, () => buildStorybook(config.script_name));
await spinner(`Copying Storybook of branch ${branch.id}...`, () => prepareStorybook(
config.from, defaultBranch === branch.id && config.default_root ? config.to : path.join(config.to, branch.id)
));
await spinner(`Fetching ${branch.id}...`, () =>
checkoutCommit(branch.commit),
);
await spinner(`Building Storybook for branch ${branch.id}...`, () =>
buildStorybook(config.script_name),
);
await spinner(`Copying Storybook of branch ${branch.id}...`, () =>
prepareStorybook(
config.from,
defaultBranch === branch.id && config.default_root
? config.to
: path.join(config.to, branch.id),
),
);
} catch (error) {
console.error("❌ ", chalk.red(`Cannot build Storybook for branch ${branch.id}:`), error.stderr.trim());
console.error(
"❌ ",
chalk.red(`Cannot build Storybook for branch ${branch.id}: `),
error.stderr?.trim() ?? error,
);
}
}

console.log(chalk.green('🧹 Clean up...'));
console.log(chalk.green("🧹 Cleaning up..."));
await $`git checkout ${defaultBranch}`;
}();
})();
46 changes: 31 additions & 15 deletions src/branch-switcher.tsx → src/components/branch-switcher.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
import { IconButton, TooltipLinkList, WithTooltip } from "@storybook/components";
import {
IconButton,
TooltipLinkList,
WithTooltip,
} from "@storybook/components";
import { BranchIcon } from "@storybook/icons";
import { useParameter } from "@storybook/manager-api";
import { styled } from "@storybook/theming";
import React, { Fragment, useCallback } from "react";
import { BRANCH_SWITCHER_ID, BranchSwitcherParameters, DEFAULT_ADDON_PARAMETERS, PARAM_KEY } from "./constants";
import { generateLink } from "./util/location";
import type { BranchSwitcherParameters } from "../constants";
import {
BRANCH_SWITCHER_ID,
DEFAULT_ADDON_PARAMETERS,
PARAM_KEY,
} from "../constants";
import { generateLink } from "../util/location";
import { BranchSwitcherState, state } from "../state";
import { useParameter } from "@storybook/manager-api";

const IconButtonLabel = styled.div(({ theme }) => ({
fontSize: theme.typography.size.s2 - 1,
marginLeft: 10
marginLeft: 10,
}));

const hasMultipleBranches = (branchList: BranchSwitcherParameters["list"]) => branchList.length > 1;
const hasMultipleBranches = (branchList: BranchSwitcherState["list"]) =>
branchList.length > 1;

export const BranchSwitcher = () => {
const {
list,
currentBranch,
defaultBranch,
hostname
} = useParameter<BranchSwitcherParameters>(PARAM_KEY, DEFAULT_ADDON_PARAMETERS);
const { hostname } = useParameter<BranchSwitcherParameters>(
PARAM_KEY,
DEFAULT_ADDON_PARAMETERS,
);
const { list, currentBranch, defaultBranch } = state;

const changeBranch = useCallback((branch) => {
if (branch !== currentBranch) {
window.parent.location = generateLink(location, hostname, defaultBranch, currentBranch, branch);
window.parent.location = generateLink(
location,
hostname,
defaultBranch,
currentBranch,
branch,
);
}
}, []);
}, [hostname, defaultBranch, currentBranch]);

return hasMultipleBranches(list) ? (
<Fragment>
Expand All @@ -43,7 +59,7 @@ export const BranchSwitcher = () => {
onClick: () => {
changeBranch(branch);
onHide();
}
},
}))}
/>
);
Expand Down
9 changes: 1 addition & 8 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
export const ADDON_ID = "storybook-branch-switcher";
export const PARAM_KEY = "branches" as const;
export const GLOBAL_KEY = "branch" as const;
export const BRANCH_SWITCHER_ID = `${ADDON_ID}/switcher` as const;

export interface BranchSwitcherParameters {
list: string[];
defaultBranch: string;
currentBranch: string;
hostname?: string;
}

export const DEFAULT_ADDON_PARAMETERS: BranchSwitcherParameters = {
list: [],
defaultBranch: "master",
currentBranch: "master",
hostname: undefined
hostname: undefined,
};
6 changes: 3 additions & 3 deletions src/manager.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { addons, types } from "@storybook/manager-api";
import { BranchSwitcher } from "./branch-switcher";
import { BranchSwitcher } from "./components/branch-switcher";
import { ADDON_ID, BRANCH_SWITCHER_ID, PARAM_KEY } from "./constants";

addons.register(ADDON_ID, () => {
addons.add(BRANCH_SWITCHER_ID, {
title: 'Branches',
title: "Branches",
type: types.TOOL,
match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)),
render: BranchSwitcher,
paramKey: PARAM_KEY,
paramKey: PARAM_KEY
});
});
6 changes: 1 addition & 5 deletions src/preview.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import type { ProjectAnnotations, Renderer } from "@storybook/types";
import { GLOBAL_KEY } from "./constants";

export const globals: ProjectAnnotations<Renderer>['globals'] = {
// Required to make sure SB picks this up from URL params
[GLOBAL_KEY]: '',
};
export const globals: ProjectAnnotations<Renderer>["globals"] = {};
42 changes: 20 additions & 22 deletions src/run/branches.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
import type { ProviderConfig } from "../cli";
import bitbucketPullRequests from "./providers/bitbucket-pull-requests";
import providers from "./providers";

export interface Branch {
id: string;
commit: string;
}

export interface BranchProvider {
isApplicable: (config: ProviderConfig) => boolean;
fetcher: (config: ProviderConfig) => Promise<Branch[]>;
}

const providers: BranchProvider[] = [
bitbucketPullRequests
];

export const fetchBranches = async (config: ProviderConfig): Promise<Branch[]> => {
export const fetchBranches = async (
config?: ProviderConfig,
): Promise<Branch[]> => {
const branches: Branch[] = [];

// Add current branch to the list
const currentBranch = await $`git branch --show-current`
const currentCommit = await $`git log --format="%H" -n 1`
branches.push({ id: currentBranch.toString().trim(), commit: currentCommit.toString().trim() })
const currentBranch = await $`git branch --show-current`;
const currentCommit = await $`git log --format="%H" -n 1`;
branches.push({
id: currentBranch.toString().trim(),
commit: currentCommit.toString().trim(),
});

// Fetch other branches from a provider, based on configuration
await spinner("Fetching branches..", async () => {
const provider = providers.find((p) => p.isApplicable(config));
if (!provider) {
throw new Error("Cannot find a branch provider");
}
branches.push(...await provider.fetcher(config));
});
if (config) {
await spinner("Fetching branches..", async () => {
const provider = providers.find((p) => p.isApplicable(config));
if (!provider) {
throw new Error("Cannot find a branch provider");
}
branches.push(...(await provider.fetcher(config)));
});
}

return branches;
}
};
13 changes: 11 additions & 2 deletions src/run/build.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { BranchSwitcherState } from "../state";

export const cleanPreviousBundle = async (directory: string): Promise<void> => {
fs.removeSync(directory);
};
Expand All @@ -7,10 +9,17 @@ export const checkoutCommit = async (commit: string): Promise<void> => {
await $`git checkout ${commit}`;
};

export const prepareAddonState = (state: BranchSwitcherState): void => {
process.env["STORYBOOK_BRANCH_SWITCHER_STATE"] = JSON.stringify(state);
};

export const buildStorybook = async (scriptName?: string): Promise<void> => {
await $`npm run ${scriptName ?? 'build-storybook'}`;
await $`npm run ${scriptName ?? "build-storybook"}`;
};

export const prepareStorybook = async (from: string, to: string): Promise<void> => {
export const prepareStorybook = async (
from: string,
to: string,
): Promise<void> => {
fs.cpSync(from, to, { recursive: true });
};
19 changes: 13 additions & 6 deletions src/run/providers/bitbucket-pull-requests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ProviderConfig } from "../../cli";
import type { BranchProvider } from "../branches";
import { BranchProvider } from "./index";

interface BitbucketProviderConfig extends ProviderConfig {
type: "bitbucket";
Expand All @@ -9,21 +9,28 @@ interface BitbucketProviderConfig extends ProviderConfig {
}

const isApplicable = (config: BitbucketProviderConfig) =>
config.type === "bitbucket" && config.project != null && config.repository != null;
config.type === "bitbucket" &&
config.project != null &&
config.repository != null;

const fetcher = async (config: BitbucketProviderConfig) => {
const host = config.url ?? "https://bitbucket.org";
const url = `${host}/rest/api/1.0/projects/${config.project}/repos/${config.repository}/pull-requests?state=open`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${$.env["BITBUCKET_TOKEN"]}`
}
Authorization: `Bearer ${$.env["BITBUCKET_TOKEN"]}`,
},
});
if (response.ok) {
const data = await response.json();
return data.values.map((v: any) => ({ id: `PR-${v.id}`, commit: v.fromRef.latestCommit }));
return data.values.map((v: any) => ({
id: `PR-${v.id}`,
commit: v.fromRef.latestCommit,
}));
} else {
throw new Error(`Error during fetch: ${response.status} ${response.statusText}`);
throw new Error(
`Error during fetch: ${response.status} ${response.statusText}`,
);
}
};

Expand Down
14 changes: 14 additions & 0 deletions src/run/providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ProviderConfig } from "../../cli";
import type { Branch } from "../branches";
import bitbucketPullRequests from "./bitbucket-pull-requests";

export interface BranchProvider {
isApplicable: (config: ProviderConfig) => boolean;
fetcher: (config: ProviderConfig) => Promise<Branch[]>;
}

const providers: BranchProvider[] = [
bitbucketPullRequests
];

export default providers;
16 changes: 16 additions & 0 deletions src/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface BranchSwitcherState {
list: string[];
defaultBranch: string;
currentBranch: string;
}

const DEFAULT_ADDON_STATE: BranchSwitcherState = {
list: [],
defaultBranch: "master",
currentBranch: "master",
};

export const state: BranchSwitcherState = Object.assign(
DEFAULT_ADDON_STATE,
JSON.parse(process.env["STORYBOOK_BRANCH_SWITCHER_STATE"] ?? "{}"),
);

0 comments on commit c414789

Please sign in to comment.