diff --git a/MIGRATION.md b/MIGRATION.md
index 483ed0b01fc5..cd78ece50ffd 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -2,6 +2,7 @@
- [From version 6.5.x to 7.0.0](#from-version-65x-to-700)
- [7.0 breaking changes](#70-breaking-changes)
+ - [Story context is prepared before for supporting fine grained updates](#story-context-is-prepared-before-for-supporting-fine-grained-updates)
- [Dropped support for Node 15 and below](#dropped-support-for-node-15-and-below)
- [ESM format in Main.js](#esm-format-in-mainjs)
- [Modern browser support](#modern-browser-support)
@@ -24,7 +25,7 @@
- [Addon-a11y: Removed deprecated withA11y decorator](#addon-a11y-removed-deprecated-witha11y-decorator)
- [Vite](#vite)
- [Vite builder uses Vite config automatically](#vite-builder-uses-vite-config-automatically)
- - [Vite cache moved to node_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook)
+ - [Vite cache moved to node\_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook)
- [Webpack](#webpack)
- [Webpack4 support discontinued](#webpack4-support-discontinued)
- [Postcss removed](#postcss-removed)
@@ -63,7 +64,7 @@
- [Dropped addon-docs manual babel configuration](#dropped-addon-docs-manual-babel-configuration)
- [Dropped addon-docs manual configuration](#dropped-addon-docs-manual-configuration)
- [Autoplay in docs](#autoplay-in-docs)
- - [Removed STORYBOOK_REACT_CLASSES global](#removed-storybook_react_classes-global)
+ - [Removed STORYBOOK\_REACT\_CLASSES global](#removed-storybook_react_classes-global)
- [7.0 Deprecations and default changes](#70-deprecations-and-default-changes)
- [storyStoreV7 enabled by default](#storystorev7-enabled-by-default)
- [`Story` type deprecated](#story-type-deprecated)
@@ -869,7 +870,7 @@ import * as ComponentStories from './some-component.stories';
```
-You can create as many docs entries as you like for a given component. Note that if you attach a docs entry to a component it will replace the automatically generated entry from Autodocs.
+You can create as many docs entries as you like for a given component. By default the docs entry will be named the same as the `.mdx` file (e.g. `Introduction.mdx` becomes `Introduction`). If the docs file is named the same as the component (e.g. `Button.mdx`, it will use the default autodocs name (`"Docs"`) and override autodocs).
By default docs entries are listed first for the component. You can sort them using story sorting.
diff --git a/code/addons/docs/template/stories/docs2/MetaOf.mdx b/code/addons/docs/template/stories/docs2/MetaOf.mdx
index 7bb445673c5c..ee40c7f95625 100644
--- a/code/addons/docs/template/stories/docs2/MetaOf.mdx
+++ b/code/addons/docs/template/stories/docs2/MetaOf.mdx
@@ -3,7 +3,7 @@ import * as ButtonStories from './button.stories.ts';
-# Docs with of
+# Docs with of, but no name
hello docs
diff --git a/code/addons/docs/template/stories/docs2/MetaOfNamed.mdx b/code/addons/docs/template/stories/docs2/MetaOfNamed.mdx
new file mode 100644
index 000000000000..9de44a95adb7
--- /dev/null
+++ b/code/addons/docs/template/stories/docs2/MetaOfNamed.mdx
@@ -0,0 +1,12 @@
+import { Meta, Story, Stories } from '@storybook/addon-docs';
+import * as ButtonStories from './button.stories.ts';
+
+
+
+# Docs with of, and name
+
+hello docs
+
+
+
+
diff --git a/code/addons/docs/template/stories/docs2/NoTitle.mdx b/code/addons/docs/template/stories/docs2/NoTitle.mdx
index 35afd95fa3e6..9689fb3502c7 100644
--- a/code/addons/docs/template/stories/docs2/NoTitle.mdx
+++ b/code/addons/docs/template/stories/docs2/NoTitle.mdx
@@ -1,3 +1,3 @@
-# Docs with no title
+# Unattached docs with no title
hello docs
diff --git a/code/addons/docs/template/stories/docs2/button.stories.ts b/code/addons/docs/template/stories/docs2/button.stories.ts
index 14f45cee42df..fd516fe58721 100644
--- a/code/addons/docs/template/stories/docs2/button.stories.ts
+++ b/code/addons/docs/template/stories/docs2/button.stories.ts
@@ -2,6 +2,7 @@ import { global as globalThis } from '@storybook/global';
export default {
component: globalThis.Components.Button,
+ tags: ['autodocs'],
args: { onClick: () => console.log('clicked!') },
parameters: {
chromatic: { disable: true },
diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts
index 0065f4492e1f..dff869df19bb 100644
--- a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts
+++ b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts
@@ -463,14 +463,14 @@ describe('StoryIndexGenerator', () => {
);
});
- it('throws an error if you attach a MetaOf entry to a tagged autodocs entry', async () => {
+ it('throws an error if you attach a named MetaOf entry which clashes with a tagged autodocs entry', async () => {
const csfSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
'./src/B.stories.ts',
options
);
const docsSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
- './errors/MetaOfAutodocs.mdx',
+ './errors/MetaOfClashingDefaultName.mdx',
options
);
@@ -478,10 +478,146 @@ describe('StoryIndexGenerator', () => {
await generator.initialize();
await expect(generator.getIndex()).rejects.toThrowError(
- `You created a component docs page for B (./errors/MetaOfAutodocs.mdx), but also tagged the CSF file (./src/B.stories.ts) with 'autodocs'. This is probably a mistake.`
+ `You created a component docs page for B (./errors/MetaOfClashingDefaultName.mdx), but also tagged the CSF file (./src/B.stories.ts) with 'autodocs'. This is probably a mistake.`
);
});
+ it('throws an error if you attach a unnamed MetaOf entry with the same name as the CSF file that clashes with a tagged autodocs entry', async () => {
+ const csfSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
+ './src/B.stories.ts',
+ options
+ );
+
+ const docsSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
+ './errors/B.mdx',
+ options
+ );
+
+ const generator = new StoryIndexGenerator([csfSpecifier, docsSpecifier], autodocsOptions);
+ await generator.initialize();
+
+ await expect(generator.getIndex()).rejects.toThrowError(
+ `You created a component docs page for B (./errors/B.mdx), but also tagged the CSF file (./src/B.stories.ts) with 'autodocs'. This is probably a mistake.`
+ );
+ });
+
+ it('allows you to create a second unnamed MetaOf entry that does not clash with autodocs', async () => {
+ const csfSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
+ './src/B.stories.ts',
+ options
+ );
+
+ const docsSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
+ './errors/MetaOfNoName.mdx',
+ options
+ );
+
+ const generator = new StoryIndexGenerator([csfSpecifier, docsSpecifier], autodocsOptions);
+ await generator.initialize();
+
+ expect(await generator.getIndex()).toMatchInlineSnapshot(`
+ Object {
+ "entries": Object {
+ "b--docs": Object {
+ "id": "b--docs",
+ "importPath": "./src/B.stories.ts",
+ "name": "docs",
+ "storiesImports": Array [],
+ "tags": Array [
+ "autodocs",
+ "docs",
+ ],
+ "title": "B",
+ "type": "docs",
+ },
+ "b--metaofnoname": Object {
+ "id": "b--metaofnoname",
+ "importPath": "./errors/MetaOfNoName.mdx",
+ "name": "MetaOfNoName",
+ "storiesImports": Array [
+ "./src/B.stories.ts",
+ ],
+ "tags": Array [
+ "docs",
+ ],
+ "title": "B",
+ "type": "docs",
+ },
+ "b--story-one": Object {
+ "id": "b--story-one",
+ "importPath": "./src/B.stories.ts",
+ "name": "Story One",
+ "tags": Array [
+ "autodocs",
+ "story",
+ ],
+ "title": "B",
+ "type": "story",
+ },
+ },
+ "v": 4,
+ }
+ `);
+ });
+ it('allows you to create a second MetaOf entry with a different name to autodocs', async () => {
+ const csfSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
+ './src/B.stories.ts',
+ options
+ );
+
+ const docsSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
+ './errors/MetaOfName.mdx',
+ options
+ );
+
+ const generator = new StoryIndexGenerator([csfSpecifier, docsSpecifier], autodocsOptions);
+ await generator.initialize();
+
+ expect(await generator.getIndex()).toMatchInlineSnapshot(`
+ Object {
+ "entries": Object {
+ "b--docs": Object {
+ "id": "b--docs",
+ "importPath": "./src/B.stories.ts",
+ "name": "docs",
+ "storiesImports": Array [],
+ "tags": Array [
+ "autodocs",
+ "docs",
+ ],
+ "title": "B",
+ "type": "docs",
+ },
+ "b--name": Object {
+ "id": "b--name",
+ "importPath": "./errors/MetaOfName.mdx",
+ "name": "name",
+ "storiesImports": Array [
+ "./src/B.stories.ts",
+ ],
+ "tags": Array [
+ "docs",
+ ],
+ "title": "B",
+ "type": "docs",
+ },
+ "b--story-one": Object {
+ "id": "b--story-one",
+ "importPath": "./src/B.stories.ts",
+ "name": "Story One",
+ "tags": Array [
+ "autodocs",
+ "story",
+ ],
+ "title": "B",
+ "type": "story",
+ },
+ },
+ "v": 4,
+ }
+ `);
+ });
+
it('allows you to override autodocs with MetaOf if it is automatic', async () => {
const csfSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
'./src/A.stories.js',
@@ -489,7 +625,7 @@ describe('StoryIndexGenerator', () => {
);
const docsSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
- './src/docs2/MetaOf.mdx',
+ './errors/A.mdx',
options
);
@@ -504,7 +640,7 @@ describe('StoryIndexGenerator', () => {
"entries": Object {
"a--docs": Object {
"id": "a--docs",
- "importPath": "./src/docs2/MetaOf.mdx",
+ "importPath": "./errors/A.mdx",
"name": "docs",
"storiesImports": Array [
"./src/A.stories.js",
@@ -613,10 +749,10 @@ describe('StoryIndexGenerator', () => {
expect(await generator.getIndex()).toMatchInlineSnapshot(`
Object {
"entries": Object {
- "a--docs": Object {
- "id": "a--docs",
+ "a--metaof": Object {
+ "id": "a--metaof",
"importPath": "./src/docs2/MetaOf.mdx",
- "name": "docs",
+ "name": "MetaOf",
"storiesImports": Array [
"./src/A.stories.js",
],
@@ -748,10 +884,10 @@ describe('StoryIndexGenerator', () => {
expect(await generator.getIndex()).toMatchInlineSnapshot(`
Object {
"entries": Object {
- "a--info": Object {
- "id": "a--info",
+ "a--metaof": Object {
+ "id": "a--metaof",
"importPath": "./src/docs2/MetaOf.mdx",
- "name": "Info",
+ "name": "MetaOf",
"storiesImports": Array [
"./src/A.stories.js",
],
@@ -829,7 +965,7 @@ describe('StoryIndexGenerator', () => {
describe('duplicates', () => {
it('warns when two MDX entries reference the same CSF file without a name', async () => {
const docsErrorSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
- './errors/DuplicateMetaOf.mdx',
+ './errors/**/A.mdx',
options
);
@@ -842,10 +978,11 @@ describe('StoryIndexGenerator', () => {
expect(Object.keys((await generator.getIndex()).entries)).toMatchInlineSnapshot(`
Array [
"a--story-one",
- "a--docs",
+ "a--metaof",
"notitle--docs",
"a--second-docs",
"docs2-yabbadabbadooo--docs",
+ "a--docs",
]
`);
@@ -870,7 +1007,7 @@ describe('StoryIndexGenerator', () => {
expect(Object.keys((await generator.getIndex()).entries)).toMatchInlineSnapshot(`
Array [
"a--story-one",
- "a--docs",
+ "a--metaof",
"notitle--docs",
"a--second-docs",
"docs2-yabbadabbadooo--docs",
@@ -884,15 +1021,24 @@ describe('StoryIndexGenerator', () => {
});
it('warns when a story has the default docs name', async () => {
- const generator = new StoryIndexGenerator([storiesSpecifier, docsSpecifier], {
- ...options,
- docs: { ...options.docs, defaultName: 'Story One' },
- });
+ const docsErrorSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
+ './errors/A.mdx',
+ options
+ );
+
+ const generator = new StoryIndexGenerator(
+ [storiesSpecifier, docsSpecifier, docsErrorSpecifier],
+ {
+ ...options,
+ docs: { ...options.docs, defaultName: 'Story One' },
+ }
+ );
await generator.initialize();
expect(Object.keys((await generator.getIndex()).entries)).toMatchInlineSnapshot(`
Array [
"a--story-one",
+ "a--metaof",
"notitle--story-one",
"a--second-docs",
"docs2-yabbadabbadooo--story-one",
@@ -952,7 +1098,7 @@ describe('StoryIndexGenerator', () => {
"d--story-one",
"b--story-one",
"nested-button--story-one",
- "a--docs",
+ "a--metaof",
"a--second-docs",
"a--story-one",
"second-nested-g--story-one",
@@ -1200,11 +1346,11 @@ describe('StoryIndexGenerator', () => {
await generator.getIndex();
expect(toId).toHaveBeenCalledTimes(5);
- expect(Object.keys((await generator.getIndex()).entries)).toContain('a--docs');
+ expect(Object.keys((await generator.getIndex()).entries)).toContain('a--metaof');
generator.invalidate(docsSpecifier, './src/docs2/MetaOf.mdx', true);
- expect(Object.keys((await generator.getIndex()).entries)).not.toContain('a--docs');
+ expect(Object.keys((await generator.getIndex()).entries)).not.toContain('a--metaof');
// this will throw if MetaOf is not removed from A's dependents
generator.invalidate(storiesSpecifier, './src/A.stories.js', false);
diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.ts
index 6d8817147f0f..67aedf38ec06 100644
--- a/code/lib/core-server/src/utils/StoryIndexGenerator.ts
+++ b/code/lib/core-server/src/utils/StoryIndexGenerator.ts
@@ -24,6 +24,7 @@ import { logger } from '@storybook/node-logger';
import { getStorySortParameter, NoMetaError } from '@storybook/csf-tools';
import { toId } from '@storybook/csf';
import { analyze } from '@storybook/docs-mdx';
+import { autoName } from './autoName';
/** A .mdx file will produce a docs entry */
type DocsCacheEntry = DocsIndexEntry;
@@ -323,24 +324,24 @@ export class StoryIndexGenerator {
// Also, if `result.of` is set, it means that we're using the `` syntax,
// so find the `title` defined the file that `meta` points to.
- let ofTitle: string;
+ let csfEntry: StoryIndexEntry;
if (result.of) {
const absoluteOf = makeAbsolute(result.of, normalizedPath, this.options.workingDir);
dependencies.forEach((dep) => {
if (dep.entries.length > 0) {
- const first = dep.entries[0];
+ const first = dep.entries.find((e) => e.type !== 'docs') as StoryIndexEntry;
if (
path
.normalize(path.resolve(this.options.workingDir, first.importPath))
.startsWith(path.normalize(absoluteOf))
) {
- ofTitle = first.title;
+ csfEntry = first;
}
}
});
- if (!ofTitle)
+ if (!csfEntry)
throw new Error(`Could not find "${result.of}" for docs file "${relativePath}".`);
}
@@ -349,8 +350,12 @@ export class StoryIndexGenerator {
dep.dependents.push(absolutePath);
});
- const title = ofTitle || userOrAutoTitleFromSpecifier(importPath, specifier, result.title);
- const name = result.name || this.options.docs.defaultName;
+ const title =
+ csfEntry?.title || userOrAutoTitleFromSpecifier(importPath, specifier, result.title);
+ const { defaultName } = this.options.docs;
+ const name =
+ result.name ||
+ (csfEntry ? autoName(importPath, csfEntry.importPath, defaultName) : defaultName);
const id = toId(title, name);
const docsEntry: DocsCacheEntry = {
diff --git a/code/lib/core-server/src/utils/__mockdata__/errors/DuplicateMetaOf.mdx b/code/lib/core-server/src/utils/__mockdata__/errors/A.mdx
similarity index 100%
rename from code/lib/core-server/src/utils/__mockdata__/errors/DuplicateMetaOf.mdx
rename to code/lib/core-server/src/utils/__mockdata__/errors/A.mdx
diff --git a/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfAutodocs.mdx b/code/lib/core-server/src/utils/__mockdata__/errors/B.mdx
similarity index 100%
rename from code/lib/core-server/src/utils/__mockdata__/errors/MetaOfAutodocs.mdx
rename to code/lib/core-server/src/utils/__mockdata__/errors/B.mdx
diff --git a/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfClashingDefaultName.mdx b/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfClashingDefaultName.mdx
new file mode 100644
index 000000000000..ade40c9ce4e5
--- /dev/null
+++ b/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfClashingDefaultName.mdx
@@ -0,0 +1,9 @@
+import * as BStories from '../src/B.stories';
+
+{/* This is the same name as the default name (i.e. autodocs entry) */}
+
+
+
+# Docs with of
+
+hello docs
diff --git a/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfName.mdx b/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfName.mdx
new file mode 100644
index 000000000000..7f55db43463d
--- /dev/null
+++ b/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfName.mdx
@@ -0,0 +1,7 @@
+import * as BStories from '../src/B.stories';
+
+
+
+# Docs with of
+
+hello docs
diff --git a/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfNoName.mdx b/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfNoName.mdx
new file mode 100644
index 000000000000..a96cbcfc1eba
--- /dev/null
+++ b/code/lib/core-server/src/utils/__mockdata__/errors/MetaOfNoName.mdx
@@ -0,0 +1,7 @@
+import * as BStories from '../src/B.stories';
+
+
+
+# Docs with of
+
+hello docs
diff --git a/code/lib/core-server/src/utils/__mockdata__/errors/duplicate/A.mdx b/code/lib/core-server/src/utils/__mockdata__/errors/duplicate/A.mdx
new file mode 100644
index 000000000000..4ed6ed2d9991
--- /dev/null
+++ b/code/lib/core-server/src/utils/__mockdata__/errors/duplicate/A.mdx
@@ -0,0 +1,7 @@
+import * as AStories from '../../src/A.stories';
+
+
+
+# Docs with of
+
+hello docs
diff --git a/code/lib/core-server/src/utils/__tests__/autoName.test.ts b/code/lib/core-server/src/utils/__tests__/autoName.test.ts
new file mode 100644
index 000000000000..d929d259858a
--- /dev/null
+++ b/code/lib/core-server/src/utils/__tests__/autoName.test.ts
@@ -0,0 +1,9 @@
+import { autoName } from '../autoName';
+
+it('pulls name from named MDX files', () => {
+ expect(autoName('Conventions.mdx', 'Button.stories.mdx', 'Docs')).toEqual('Conventions');
+});
+
+it('falls back for default named MDX files', () => {
+ expect(autoName('Button.mdx', 'Button.stories.mdx', 'Docs')).toEqual('Docs');
+});
diff --git a/code/lib/core-server/src/utils/autoName.ts b/code/lib/core-server/src/utils/autoName.ts
new file mode 100644
index 000000000000..414d21c3783e
--- /dev/null
+++ b/code/lib/core-server/src/utils/autoName.ts
@@ -0,0 +1,24 @@
+import type { Path } from '@storybook/types';
+import { basename } from 'path';
+
+/**
+ * Calculate a name to use for a docs entry if not specified. The rule is:
+ *
+ * 1. If the name of the MDX file is the "same" as the CSF file
+ * (e.g. Button.mdx, Button.stories.jsx) use the default name.
+ * 2. Else use the (ext-less) name of the MDX file
+ *
+ * @param mdxImportPath importPath of the MDX file with of={}
+ * @param csfImportPath importPath of the of CSF file
+ */
+export function autoName(mdxImportPath: Path, csfImportPath: Path, defaultName: string) {
+ const mdxBasename = basename(mdxImportPath);
+ const csfBasename = basename(csfImportPath);
+
+ const [mdxFilename] = mdxBasename.split('.');
+ const [csfFilename] = csfBasename.split('.');
+
+ if (mdxFilename === csfFilename) return defaultName;
+
+ return mdxFilename;
+}
diff --git a/code/lib/core-server/src/utils/stories-json.test.ts b/code/lib/core-server/src/utils/stories-json.test.ts
index b7921a8556c0..87c439237b11 100644
--- a/code/lib/core-server/src/utils/stories-json.test.ts
+++ b/code/lib/core-server/src/utils/stories-json.test.ts
@@ -117,10 +117,10 @@ describe('useStoriesJson', () => {
expect(JSON.parse(send.mock.calls[0][0])).toMatchInlineSnapshot(`
Object {
"entries": Object {
- "a--docs": Object {
- "id": "a--docs",
+ "a--metaof": Object {
+ "id": "a--metaof",
"importPath": "./src/docs2/MetaOf.mdx",
- "name": "docs",
+ "name": "MetaOf",
"storiesImports": Array [
"./src/A.stories.js",
],
@@ -277,20 +277,20 @@ describe('useStoriesJson', () => {
expect(JSON.parse(send.mock.calls[0][0])).toMatchInlineSnapshot(`
Object {
"stories": Object {
- "a--docs": Object {
- "id": "a--docs",
+ "a--metaof": Object {
+ "id": "a--metaof",
"importPath": "./src/docs2/MetaOf.mdx",
"kind": "A",
- "name": "docs",
+ "name": "MetaOf",
"parameters": Object {
- "__id": "a--docs",
+ "__id": "a--metaof",
"docsOnly": true,
"fileName": "./src/docs2/MetaOf.mdx",
},
"storiesImports": Array [
"./src/A.stories.js",
],
- "story": "docs",
+ "story": "MetaOf",
"tags": Array [
"docs",
],