Skip to content

Instantly share code, notes, and snippets.

@dannyhw
Last active December 7, 2025 23:32
Show Gist options
  • Select an option

  • Save dannyhw/1ea1fe055594367063f00fe4b9c2cd3a to your computer and use it in GitHub Desktop.

Select an option

Save dannyhw/1ea1fe055594367063f00fe4b9c2cd3a to your computer and use it in GitHub Desktop.
get react native list of stories with node
import { normalizeStories, loadMainConfig } from 'storybook/internal/common';
import { readFileSync } from 'node:fs';
import { sync as globSync } from 'glob';
import path from 'path';
import { CsfFile, loadCsf } from 'storybook/internal/csf-tools';
import { toId } from 'storybook/internal/csf';
import {
type StoryIndex,
type IndexedCSFFile,
type NormalizedStoriesSpecifier,
} from 'storybook/internal/types';
import { userOrAutoTitleFromSpecifier } from 'storybook/internal/preview-api';
const cwd = process.cwd();
const makeTitle = (fileName: string, specifier: NormalizedStoriesSpecifier, userTitle: string) => {
const title = userOrAutoTitleFromSpecifier(fileName, specifier, userTitle);
if (title) {
return title.replace('./', '');
} else if (userTitle) {
return userTitle.replace('./', '');
} else {
console.error('Could not generate title!!');
process.exit(1);
}
};
function ensureRelativePathHasDot(relativePath: string) {
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
}
export async function buildIndex({ configPath }: { configPath: string }) {
const main = await loadMainConfig({ configDir: configPath, cwd });
if (!main.stories || !Array.isArray(main.stories)) {
throw new Error('No stories found');
}
const storiesSpecifiers = normalizeStories(main.stories, {
configDir: configPath,
workingDir: cwd,
});
const specifierStoryPaths = storiesSpecifiers.map((specifier) => {
return globSync(specifier.files, {
cwd: path.resolve(process.cwd(), specifier.directory),
absolute: true,
// default to always ignore (exclude) anything in node_modules
ignore: ['**/node_modules'],
}).map((storyPath: string) => {
const normalizePathForWindows = (str: string) =>
path.sep === '\\' ? str.replace(/\\/g, '/') : str;
return normalizePathForWindows(storyPath);
});
});
const csfStories = specifierStoryPaths.reduce((acc, specifierStoryPathList, specifierIndex) => {
const paths = specifierStoryPathList.map((storyPath) => {
const code = readFileSync(storyPath, { encoding: 'utf-8' }).toString();
const relativePath = ensureRelativePathHasDot(path.posix.relative(cwd, storyPath));
return {
result: loadCsf(code, {
fileName: storyPath,
makeTitle: (userTitle) =>
makeTitle(relativePath, storiesSpecifiers[specifierIndex], userTitle),
}).parse(),
specifier: storiesSpecifiers[specifierIndex],
fileName: relativePath,
};
});
return [...acc, ...paths];
}, new Array<{ result: CsfFile & IndexedCSFFile; specifier: NormalizedStoriesSpecifier; fileName: string }>());
const index: StoryIndex = {
v: 5,
entries: {},
};
for (const { result, specifier, fileName } of csfStories) {
const { meta, stories } = result;
// console.log({ meta, stories, specifier, fileName });
if (stories && stories.length > 0) {
for (const story of stories) {
const id = toId(meta.title, story.name);
index.entries[id] = {
type: 'story',
id,
name: story.name,
title: meta.title,
importPath: `${specifier.directory}/${path.posix.relative(specifier.directory, fileName)}`,
tags: ['story'],
subtype: 'story',
};
}
} else {
console.log(`No stories found for ${fileName}`);
process.exit(1);
}
}
return index;
}
@dannyhw
Copy link
Author

dannyhw commented Dec 7, 2025

saving this for future reference, this makes it possible to get the story index without depending on storybooks standalone buildIndex which currently doesn't work consistently for react native storybook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment