Skip to content
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

CSF3: Generate default titles based on file path #15376

Merged
merged 6 commits into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/react-ts/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StorybookConfig } from '@storybook/react/types';

const config: StorybookConfig = {
stories: ['./src/*.stories.*'],
stories: [{ directory: './src', root: 'Demo' }],
logLevel: 'debug',
addons: [
'@storybook/addon-essentials',
Expand Down
1 change: 0 additions & 1 deletion examples/react-ts/src/AccountForm.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import userEvent from '@testing-library/user-event';
import { AccountForm, AccountFormProps } from './AccountForm';

export default {
title: 'Demo/AccountForm',
component: AccountForm,
parameters: {
layout: 'centered',
Expand Down
4 changes: 3 additions & 1 deletion lib/builder-webpack4/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
useProgressReporting,
checkWebpackVersion,
Options,
normalizeStories,
StoriesEntry,
} from '@storybook/core-common';

let compilation: ReturnType<typeof webpackDevMiddleware>;
Expand All @@ -32,7 +34,7 @@ export const getConfig: WebpackBuilder['getConfig'] = async (options) => {
const typescriptOptions = await presets.apply('typescript', {}, options);
const babelOptions = await presets.apply('babel', {}, { ...options, typescriptOptions });
const entries = await presets.apply('entries', [], options);
const stories = await presets.apply('stories', [], options);
const stories = normalizeStories(await presets.apply('stories', [], options), options.configDir);
const frameworkOptions = await presets.apply(`${options.framework}Options`, {}, options);

return presets.apply(
Expand Down
9 changes: 5 additions & 4 deletions lib/builder-webpack4/src/preview/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { logger } from '@storybook/node-logger';
import stable from 'stable';
import dedent from 'ts-dedent';
import glob from 'glob-promise';
import { loadPreviewOrConfigFile } from '@storybook/core-common';
import { loadPreviewOrConfigFile, normalizeStories, StoriesEntry } from '@storybook/core-common';

export const sortEntries = (entries: string[]) => {
const isGenerated = /generated-(config|other)-entry/;
Expand Down Expand Up @@ -41,7 +41,7 @@ export async function createPreviewEntry(options: { configDir: string; presets:

const configs = getMainConfigs(options);
const other: string[] = await presets.apply('config', [], options);
const stories: string[] = await presets.apply('stories', [], options);
const stories = normalizeStories(await presets.apply('stories', [], options), configDir);

if (configs.length > 0) {
const noun = configs.length === 1 ? 'file' : 'files';
Expand All @@ -58,15 +58,16 @@ export async function createPreviewEntry(options: { configDir: string; presets:
if (stories && stories.length) {
entries.push(path.resolve(path.join(configDir, `generated-stories-entry.js`)));

const globs = stories.map((s) => s.glob);
const files = (
await Promise.all(stories.map((g) => glob(path.isAbsolute(g) ? g : path.join(configDir, g))))
await Promise.all(globs.map((g) => glob(path.isAbsolute(g) ? g : path.join(configDir, g))))
).reduce((a, b) => a.concat(b));

if (files.length === 0) {
logger.warn(dedent`
We found no files matching any of the following globs:

${stories.join('\n')}
${globs.join('\n')}

Maybe your glob was invalid?
see: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#correct-globs-in-mainjs
Expand Down
15 changes: 12 additions & 3 deletions lib/builder-webpack4/src/preview/iframe-webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
interpolate,
nodeModulesPaths,
Options,
NormalizedStoriesEntry,
} from '@storybook/core-common';
import { createBabelLoader } from './babel-loader-preview';

Expand Down Expand Up @@ -109,9 +110,16 @@ export default async ({
});
if (stories) {
const storiesFilename = path.resolve(path.join(configDir, `generated-stories-entry.js`));
virtualModuleMapping[storiesFilename] = interpolate(storyTemplate, { frameworkImportPath })
// Make sure we also replace quotes for this one
.replace("'{{stories}}'", stories.map(toRequireContextString).join(','));
// Make sure we also replace quotes for this one
virtualModuleMapping[storiesFilename] = interpolate(storyTemplate, {
frameworkImportPath,
}).replace(
"'{{stories}}'",
stories
.map((s: NormalizedStoriesEntry) => s.glob)
.map(toRequireContextString)
.join(',')
);
}

const shouldCheckTs = useBaseTsSupport(framework) && typescriptOptions.check;
Expand Down Expand Up @@ -155,6 +163,7 @@ export default async ({
LOGLEVEL: logLevel,
FRAMEWORK_OPTIONS: frameworkOptions,
FEATURES: features,
STORIES: stories,
},
headHtmlSnippet,
bodyHtmlSnippet,
Expand Down
7 changes: 6 additions & 1 deletion lib/builder-webpack5/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
useProgressReporting,
checkWebpackVersion,
Options,
normalizeStories,
StoriesEntry,
} from '@storybook/core-common';

let compilation: ReturnType<typeof webpackDevMiddleware>;
Expand All @@ -19,7 +21,10 @@ export const getConfig: WebpackBuilder['getConfig'] = async (options) => {
const typescriptOptions = await presets.apply('typescript', {}, options);
const babelOptions = await presets.apply('babel', {}, { ...options, typescriptOptions });
const entries = await presets.apply('entries', [], options);
const stories = await presets.apply('stories', [], options);
const stories = normalizeStories(
(await presets.apply('stories', [], options)) as StoriesEntry[],
options.configDir
);
const frameworkOptions = await presets.apply(`${options.framework}Options`, {}, options);

return presets.apply(
Expand Down
9 changes: 5 additions & 4 deletions lib/builder-webpack5/src/preview/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { logger } from '@storybook/node-logger';
import stable from 'stable';
import dedent from 'ts-dedent';
import glob from 'glob-promise';
import { loadPreviewOrConfigFile } from '@storybook/core-common';
import { loadPreviewOrConfigFile, normalizeStories } from '@storybook/core-common';

export const sortEntries = (entries: string[]) => {
const isGenerated = /generated-(config|other)-entry/;
Expand Down Expand Up @@ -41,7 +41,7 @@ export async function createPreviewEntry(options: { configDir: string; presets:

const configs = getMainConfigs(options);
const other: string[] = await presets.apply('config', [], options);
const stories: string[] = await presets.apply('stories', [], options);
const stories = normalizeStories(await presets.apply('stories', [], options), options.configDir);

if (configs.length > 0) {
const noun = configs.length === 1 ? 'file' : 'files';
Expand All @@ -58,15 +58,16 @@ export async function createPreviewEntry(options: { configDir: string; presets:
if (stories && stories.length) {
entries.push(path.resolve(path.join(configDir, `generated-stories-entry.js`)));

const globs = stories.map((s) => s.glob);
const files = (
await Promise.all(stories.map((g) => glob(path.isAbsolute(g) ? g : path.join(configDir, g))))
await Promise.all(globs.map((g) => glob(path.isAbsolute(g) ? g : path.join(configDir, g))))
).reduce((a, b) => a.concat(b));

if (files.length === 0) {
logger.warn(dedent`
We found no files matching any of the following globs:

${stories.join('\n')}
${globs.join('\n')}

Maybe your glob was invalid?
see: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#correct-globs-in-mainjs
Expand Down
10 changes: 9 additions & 1 deletion lib/builder-webpack5/src/preview/iframe-webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
interpolate,
Options,
hasDotenv,
NormalizedStoriesEntry,
} from '@storybook/core-common';
import { createBabelLoader } from './babel-loader-preview';

Expand Down Expand Up @@ -107,7 +108,13 @@ export default async ({
const storiesFilename = path.resolve(path.join(configDir, `generated-stories-entry.js`));
virtualModuleMapping[storiesFilename] = interpolate(storyTemplate, { frameworkImportPath })
// Make sure we also replace quotes for this one
.replace("'{{stories}}'", stories.map(toRequireContextString).join(','));
.replace(
"'{{stories}}'",
stories
.map((s: NormalizedStoriesEntry) => s.glob)
.map(toRequireContextString)
.join(',')
);
}

const shouldCheckTs = useBaseTsSupport(framework) && typescriptOptions.check;
Expand Down Expand Up @@ -156,6 +163,7 @@ export default async ({
LOGLEVEL: logLevel,
FRAMEWORK_OPTIONS: frameworkOptions,
FEATURES: features,
STORIES: stories,
},
headHtmlSnippet,
bodyHtmlSnippet,
Expand Down
62 changes: 62 additions & 0 deletions lib/core-client/src/preview/autoTitle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { autoTitleFromEntry as auto } from './autoTitle';

expect.addSnapshotSerializer({
print: (val: any) => val,
test: (val) => true,
});

describe('autoTitle', () => {
it('no directory', () => {
expect(auto('/path/to/file', { glob: '' })).toBeFalsy();
});

it('no match', () => {
expect(auto('/path/to/file', { glob: '', specifier: { directory: '/other' } })).toBeFalsy();
});

describe('trailing slash', () => {
shilman marked this conversation as resolved.
Show resolved Hide resolved
it('match with no root', () => {
expect(
auto('/path/to/file', { glob: '', specifier: { directory: '/path' } })
).toMatchInlineSnapshot(`to/file`);
});

it('match with root', () => {
expect(
auto('/path/to/file', { glob: '', specifier: { directory: '/path', root: 'atoms' } })
).toMatchInlineSnapshot(`atoms/to/file`);
});

it('match with extension', () => {
expect(
auto('/path/to/file.stories.tsx', {
glob: '',
specifier: { directory: '/path', root: 'atoms' },
})
).toMatchInlineSnapshot(`atoms/to/file`);
});
});

describe('no trailing slash', () => {
it('match with no root', () => {
expect(
auto('/path/to/file', { glob: '', specifier: { directory: '/path/' } })
).toMatchInlineSnapshot(`to/file`);
});

it('match with root', () => {
expect(
auto('/path/to/file', { glob: '', specifier: { directory: '/path/', root: 'atoms' } })
).toMatchInlineSnapshot(`atoms/to/file`);
});

it('match with extension', () => {
expect(
auto('/path/to/file.stories.tsx', {
glob: '',
specifier: { directory: '/path/', root: 'atoms' },
})
).toMatchInlineSnapshot(`atoms/to/file`);
});
});
});
47 changes: 47 additions & 0 deletions lib/core-client/src/preview/autoTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import global from 'global';
import path from 'path';
import type { NormalizedStoriesEntry } from '@storybook/core-common';

const { FEATURES = {}, STORIES = [] } = global;

interface Meta {
title?: string;
}

const autoTitleV2 = (meta: Meta, fileName: string) => {
return meta.title;
};

const stripExtension = (titleWithExtension: string) => {
let parts = titleWithExtension.split('/');
const last = parts[parts.length - 1];
const dotIndex = last.indexOf('.');
const stripped = dotIndex > 0 ? last.substr(0, dotIndex) : last;
parts[parts.length - 1] = stripped;
const [first, ...rest] = parts;
if (first === '') {
parts = rest;
}
return parts.join('/');
};

export const autoTitleFromEntry = (fileName: string, entry: NormalizedStoriesEntry) => {
const { directory, root = '' } = entry.specifier || {};
if (fileName.startsWith(directory)) {
const suffix = fileName.replace(directory, '');
return stripExtension(path.join(root, suffix));
}
return undefined;
};

const autoTitleV3 = (meta: Meta, fileName: string) => {
if (meta.title) return meta.title;
console.log({ fileName, STORIES });
shilman marked this conversation as resolved.
Show resolved Hide resolved
for (let i = 0; i < STORIES.length; i += 1) {
const title = autoTitleFromEntry(fileName, STORIES[i]);
if (title) return title;
}
return undefined;
};

export const autoTitle = FEATURES.previewCsfV3 ? autoTitleV3 : autoTitleV2;
13 changes: 9 additions & 4 deletions lib/core-client/src/preview/loadCsf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import deprecate from 'util-deprecate';

import { Loadable, LoaderFunction, RequireContext } from './types';
import { normalizeStory } from './normalizeStory';
import { autoTitle } from './autoTitle';

const duplicateKindWarning = deprecate(
(kindName: string) => {
Expand Down Expand Up @@ -80,14 +81,18 @@ const loadStories = (
return;
}

if (!fileExports.default.title) {
const { default: defaultExport, __namedExportsOrder, ...namedExports } = fileExports;
let exports = namedExports;

const fileName = currentExports.get(fileExports);
const title = autoTitle(defaultExport, fileName);
if (!title) {
throw new Error(
`Unexpected default export without title: ${JSON.stringify(fileExports.default)}`
);
}

const { default: meta, __namedExportsOrder, ...namedExports } = fileExports;
let exports = namedExports;
const meta = { ...defaultExport, title };

// prefer a user/loader provided `__namedExportsOrder` array if supplied
// we do this as es module exports are always ordered alphabetically
Expand Down Expand Up @@ -126,7 +131,7 @@ const loadStories = (
framework,
component,
subcomponents,
fileName: currentExports.get(fileExports),
fileName,
...kindParameters,
args: kindArgs,
argTypes: kindArgTypes,
Expand Down
1 change: 1 addition & 0 deletions lib/core-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export * from './utils/interpolate';
export * from './utils/validate-configuration-files';
export * from './utils/to-require-context';
export * from './utils/has-dotenv';
export * from './utils/normalize-stories';

export * from './types';
17 changes: 15 additions & 2 deletions lib/core-common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface Presets {
): Promise<TypescriptConfig>;
apply(extension: 'babel', config: {}, args: any): Promise<TransformOptions>;
apply(extension: 'entries', config: [], args: any): Promise<unknown>;
apply(extension: 'stories', config: [], args: any): Promise<unknown>;
apply(extension: 'stories', config: [], args: any): Promise<StoriesEntry[]>;
apply(
extension: 'webpack',
config: {},
Expand Down Expand Up @@ -218,6 +218,19 @@ export interface TypescriptOptions {
reactDocgenTypescriptOptions: PluginOptions;
}

interface StoriesSpecifier {
directory: string;
files?: string;
root?: string;
}

export type StoriesEntry = string | StoriesSpecifier;

export interface NormalizedStoriesEntry {
glob: string;
specifier?: StoriesSpecifier;
}

/**
* The interface for Storybook configuration in `main.ts` files.
*/
Expand Down Expand Up @@ -257,7 +270,7 @@ export interface StorybookConfig {
*
* @example `['./src/*.stories.@(j|t)sx?']`
*/
stories: string[];
stories: StoriesEntry[];
/**
* Controls how Storybook handles TypeScript files.
*/
Expand Down
Loading