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

CSF: Autotitle fix multiple dots and handle stories.js #21840

Merged
merged 11 commits into from
Nov 28, 2023
13 changes: 13 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- [From version 7.x to 8.0.0](#from-version-7x-to-800)
- [Core changes](#core-changes)
- [Autotitle breaking fixes](#autotitle-breaking-fixes)
- [UI layout state has changed shape](#ui-layout-state-has-changed-shape)
- [New UI and props for Button and IconButton components](#new-ui-and-props-for-button-and-iconbutton-components)
- [Icons is deprecated](#icons-is-deprecated)
Expand Down Expand Up @@ -316,6 +317,18 @@

### Core changes

#### Autotitle breaking fixes

In Storybook 7, the file name `path/to/foo.bar.stories.js` would result in the [autotitle](https://storybook.js.org/docs/react/configure/overview#configure-story-loading) `path/to/foo`. In 8.0, this has been changed to generate `path/to/foo.bar`. We consider this a bugfix but it is also a breaking change if you depended on the old behavior. To get the old titles, you can manually specify the desired title in the default export of your story file. For example:

```js
export default {
title: 'path/to/foo',
}
```

Alternatively, if you need to achieve a different behavior for a large number of files, you can provide a [custom indexer](https://storybook.js.org/docs/7.0/vue/configure/sidebar-and-urls#processing-custom-titles) to generate the titles dynamically.

#### UI layout state has changed shape

In Storybook 7 it was possible to use `addons.setConfig({...});` to configure Storybook UI features and behavior as documented [here (v7)](https://storybook.js.org/docs/7.3/react/configure/features-and-behavior), [(latest)](https://storybook.js.org/docs/react/configure/features-and-behavior). The state and API for the UI layout has changed:
Expand Down
45 changes: 45 additions & 0 deletions code/lib/preview-api/src/modules/store/autoTitle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,39 @@ describe('userOrAutoTitleFromSpecifier', () => {
).toMatchInlineSnapshot(`to/button`);
});

it('match with trailing stories', () => {
expect(
userOrAuto(
'./path/to/button/stories.js',
normalizeStoriesEntry({ directory: './path', files: '**/?(*.)stories.*' }, options),
undefined
)
).toMatchInlineSnapshot(`to/button`);
});

it('match with trailing stories (windows path)', () => {
expect(
userOrAuto(
'./path/to/button/stories.js',
normalizeStoriesEntry(
{ directory: '.\\path\\', files: '**/?(*.)stories.*' },
winOptions
),
undefined
)
).toMatchInlineSnapshot(`to/button`);
});

it('match with dotted component', () => {
expect(
userOrAuto(
'./path/to/button/button.group.stories.js',
normalizeStoriesEntry({ directory: './path' }, options),
undefined
)
).toMatchInlineSnapshot(`to/button/button.group`);
});

it('match with hyphen path', () => {
expect(
userOrAuto(
Expand All @@ -207,6 +240,18 @@ describe('userOrAutoTitleFromSpecifier', () => {
).toMatchInlineSnapshot(`to_my/file`);
});

it('match with short path', () => {
// Make sure "stories" isn't trimmed as redundant when there won't be
// anything left.
expect(
userOrAuto(
'./path/stories.js',
normalizeStoriesEntry({ directory: './path', files: '**/?(*.)stories.*' }, options),
undefined
)
).toMatchInlineSnapshot(`stories`);
});

it('match with windows path', () => {
expect(
userOrAuto(
Expand Down
48 changes: 18 additions & 30 deletions code/lib/preview-api/src/modules/store/autoTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,18 @@ import type { NormalizedStoriesSpecifier } from '@storybook/types';
// FIXME: types duplicated type from `core-common', to be
// removed when we remove v6 back-compat.

const stripExtension = (path: string[]) => {
let parts = [...path];
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;
const stripExtension = (parts: string[]) => {
const last = parts[parts.length - 1]?.replace(/(?:[.](?:story|stories))?([.][^.]+)$/i, '');
return last ? [...parts.slice(0, -1), last] : parts;
};

const indexRe = /^index$/i;

// deal with files like "atoms/button/{button,index}.stories.js"
const removeRedundantFilename = (paths: string[]) => {
let prevVal: string;
return paths.filter((val, index) => {
if (index === paths.length - 1 && (val === prevVal || indexRe.test(val))) {
return false;
}
prevVal = val;
return true;
});
const removeRedundantFilename = (parts: string[]) => {
const last = parts[parts.length - 1];
const nextToLast = parts[parts.length - 2];
return last && nextToLast && (last === nextToLast || /^(?:index|story|stories)$/i.test(last))
? parts.slice(0, -1)
: parts;
};

/**
Expand All @@ -41,8 +28,10 @@ const removeRedundantFilename = (paths: string[]) => {
* @returns joined path string, with single '/' between parts
*/
function pathJoin(paths: string[]): string {
const slashes = new RegExp('/{1,}', 'g');
return paths.join('/').replace(slashes, '/');
return paths
.flatMap((p) => p.split('/'))
.filter(Boolean)
.join('/');
}

export const userOrAutoTitleFromSpecifier = (
Expand All @@ -67,18 +56,17 @@ export const userOrAutoTitleFromSpecifier = (
if (importPathMatcher.exec(normalizedFileName)) {
if (!userTitle) {
const suffix = normalizedFileName.replace(directory, '');
const titleAndSuffix = slash(pathJoin([titlePrefix, suffix]));
let path = titleAndSuffix.split('/');
path = stripExtension(path);
path = removeRedundantFilename(path);
return path.join('/');
let parts = pathJoin([titlePrefix, suffix]).split('/');
parts = stripExtension(parts);
parts = removeRedundantFilename(parts);
return parts.join('/');
}

if (!titlePrefix) {
return userTitle;
}

return slash(pathJoin([titlePrefix, userTitle]));
return pathJoin([titlePrefix, userTitle]);
}

return undefined;
Expand Down