diff --git a/code/addons/docs/docs/recipes.md b/code/addons/docs/docs/recipes.md index 20faa8fba1a7..e98c89145b78 100644 --- a/code/addons/docs/docs/recipes.md +++ b/code/addons/docs/docs/recipes.md @@ -269,8 +269,8 @@ export const parameters = { transform: (src, storyContext) => { const match = SOURCE_REGEX.exec(src); return match ? match[1] : src; - } - } + }, + }, }, }; ``` diff --git a/code/lib/core-server/package.json b/code/lib/core-server/package.json index 9f832722a45a..a9c5033a1866 100644 --- a/code/lib/core-server/package.json +++ b/code/lib/core-server/package.json @@ -96,6 +96,7 @@ "telejson": "^7.2.0", "tiny-invariant": "^1.3.1", "ts-dedent": "^2.0.0", + "tsconfig-paths": "^4.2.0", "util": "^0.12.4", "util-deprecate": "^1.0.2", "watchpack": "^2.2.0", diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts index ccce3fe481bc..ca89c4a861a8 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts @@ -75,6 +75,7 @@ describe('StoryIndexGenerator', () => { { "entries": { "a--story-one": { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -107,6 +108,7 @@ describe('StoryIndexGenerator', () => { { "entries": { "f--story-one": { + "componentPath": undefined, "id": "f--story-one", "importPath": "./src/F.story.ts", "name": "Story One", @@ -138,6 +140,7 @@ describe('StoryIndexGenerator', () => { { "entries": { "stories--story-one": { + "componentPath": undefined, "id": "stories--story-one", "importPath": "./src/stories.ts", "name": "Story One", @@ -168,7 +171,44 @@ describe('StoryIndexGenerator', () => { expect(await generator.getIndex()).toMatchInlineSnapshot(` { "entries": { + "componentpath-extension--story-one": { + "componentPath": "./src/componentPath/component.js", + "id": "componentpath-extension--story-one", + "importPath": "./src/componentPath/extension.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/extension", + "type": "story", + }, + "componentpath-noextension--story-one": { + "componentPath": "./src/componentPath/component.js", + "id": "componentpath-noextension--story-one", + "importPath": "./src/componentPath/noExtension.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/noExtension", + "type": "story", + }, + "componentpath-package--story-one": { + "componentPath": "component-package", + "id": "componentpath-package--story-one", + "importPath": "./src/componentPath/package.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/package", + "type": "story", + }, "nested-button--story-one": { + "componentPath": undefined, "id": "nested-button--story-one", "importPath": "./src/nested/Button.stories.ts", "name": "Story One", @@ -181,6 +221,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "second-nested-g--story-one": { + "componentPath": undefined, "id": "second-nested-g--story-one", "importPath": "./src/second-nested/G.stories.ts", "name": "Story One", @@ -211,6 +252,7 @@ describe('StoryIndexGenerator', () => { { "entries": { "a--story-one": { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -224,6 +266,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "b--story-one": { + "componentPath": undefined, "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -235,7 +278,44 @@ describe('StoryIndexGenerator', () => { "title": "B", "type": "story", }, + "componentpath-extension--story-one": { + "componentPath": "./src/componentPath/component.js", + "id": "componentpath-extension--story-one", + "importPath": "./src/componentPath/extension.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/extension", + "type": "story", + }, + "componentpath-noextension--story-one": { + "componentPath": "./src/componentPath/component.js", + "id": "componentpath-noextension--story-one", + "importPath": "./src/componentPath/noExtension.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/noExtension", + "type": "story", + }, + "componentpath-package--story-one": { + "componentPath": "component-package", + "id": "componentpath-package--story-one", + "importPath": "./src/componentPath/package.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/package", + "type": "story", + }, "d--story-one": { + "componentPath": undefined, "id": "d--story-one", "importPath": "./src/D.stories.jsx", "name": "Story One", @@ -248,6 +328,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "first-nested-deeply-f--story-one": { + "componentPath": undefined, "id": "first-nested-deeply-f--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", "name": "Story One", @@ -259,6 +340,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "h--story-one": { + "componentPath": undefined, "id": "h--story-one", "importPath": "./src/H.stories.mjs", "name": "Story One", @@ -271,6 +353,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "nested-button--story-one": { + "componentPath": undefined, "id": "nested-button--story-one", "importPath": "./src/nested/Button.stories.ts", "name": "Story One", @@ -283,6 +366,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "second-nested-g--story-one": { + "componentPath": undefined, "id": "second-nested-g--story-one", "importPath": "./src/second-nested/G.stories.ts", "name": "Story One", @@ -318,6 +402,7 @@ describe('StoryIndexGenerator', () => { { "entries": { "a--story-one": { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -344,6 +429,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "b--story-one": { + "componentPath": undefined, "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -355,6 +441,42 @@ describe('StoryIndexGenerator', () => { "title": "B", "type": "story", }, + "componentpath-extension--story-one": { + "componentPath": "./src/componentPath/component.js", + "id": "componentpath-extension--story-one", + "importPath": "./src/componentPath/extension.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/extension", + "type": "story", + }, + "componentpath-noextension--story-one": { + "componentPath": "./src/componentPath/component.js", + "id": "componentpath-noextension--story-one", + "importPath": "./src/componentPath/noExtension.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/noExtension", + "type": "story", + }, + "componentpath-package--story-one": { + "componentPath": "component-package", + "id": "componentpath-package--story-one", + "importPath": "./src/componentPath/package.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/package", + "type": "story", + }, "d--docs": { "id": "d--docs", "importPath": "./src/D.stories.jsx", @@ -369,6 +491,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "d--story-one": { + "componentPath": undefined, "id": "d--story-one", "importPath": "./src/D.stories.jsx", "name": "Story One", @@ -381,6 +504,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "first-nested-deeply-f--story-one": { + "componentPath": undefined, "id": "first-nested-deeply-f--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", "name": "Story One", @@ -405,6 +529,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "h--story-one": { + "componentPath": undefined, "id": "h--story-one", "importPath": "./src/H.stories.mjs", "name": "Story One", @@ -417,6 +542,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "nested-button--story-one": { + "componentPath": undefined, "id": "nested-button--story-one", "importPath": "./src/nested/Button.stories.ts", "name": "Story One", @@ -429,6 +555,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "second-nested-g--story-one": { + "componentPath": undefined, "id": "second-nested-g--story-one", "importPath": "./src/second-nested/G.stories.ts", "name": "Story One", @@ -471,6 +598,12 @@ describe('StoryIndexGenerator', () => { "d--story-one", "h--docs", "h--story-one", + "componentpath-extension--docs", + "componentpath-extension--story-one", + "componentpath-noextension--docs", + "componentpath-noextension--story-one", + "componentpath-package--docs", + "componentpath-package--story-one", "first-nested-deeply-f--docs", "first-nested-deeply-f--story-one", "nested-button--docs", @@ -581,6 +714,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "b--story-one": { + "componentPath": undefined, "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -644,6 +778,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "b--story-one": { + "componentPath": undefined, "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -700,6 +835,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "a--story-one": { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -747,6 +883,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "duplicate-a--story-one": { + "componentPath": undefined, "id": "duplicate-a--story-one", "importPath": "./duplicate/A.stories.js", "name": "Story One", @@ -759,6 +896,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "duplicate-a--story-two": { + "componentPath": undefined, "id": "duplicate-a--story-two", "importPath": "./duplicate/SecondA.stories.js", "name": "Story Two", @@ -820,6 +958,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "my-component-a--story-one": { + "componentPath": undefined, "id": "my-component-a--story-one", "importPath": "./docs-id-generation/A.stories.jsx", "name": "Story One", @@ -881,6 +1020,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "a--story-one": { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -1014,6 +1154,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "a--story-one": { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -1085,6 +1226,7 @@ describe('StoryIndexGenerator', () => { { "entries": { "a--story-one": { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -1098,6 +1240,7 @@ describe('StoryIndexGenerator', () => { "type": "story", }, "b--story-one": { + "componentPath": undefined, "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -1165,6 +1308,7 @@ describe('StoryIndexGenerator', () => { "type": "docs", }, "my-component-b--story-one": { + "componentPath": undefined, "id": "my-component-b--story-one", "importPath": "./docs-id-generation/B.stories.jsx", "name": "Story One", @@ -1349,6 +1493,9 @@ describe('StoryIndexGenerator', () => { "componentreference--docs", "notitle--docs", "h--story-one", + "componentpath-extension--story-one", + "componentpath-noextension--story-one", + "componentpath-package--story-one", "first-nested-deeply-f--story-one", ] `); @@ -1367,7 +1514,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(7); + expect(readCsfMock).toHaveBeenCalledTimes(10); readCsfMock.mockClear(); await generator.getIndex(); @@ -1424,7 +1571,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(7); + expect(readCsfMock).toHaveBeenCalledTimes(10); generator.invalidate(specifier, './src/B.stories.ts', false); @@ -1509,7 +1656,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(7); + expect(readCsfMock).toHaveBeenCalledTimes(10); generator.invalidate(specifier, './src/B.stories.ts', true); @@ -1548,7 +1695,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(7); + expect(readCsfMock).toHaveBeenCalledTimes(10); generator.invalidate(specifier, './src/B.stories.ts', true); diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.ts index 3330a7e21a54..fd1eb40c9496 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.ts @@ -3,6 +3,8 @@ import chalk from 'chalk'; import fs from 'fs-extra'; import slash from 'slash'; import invariant from 'tiny-invariant'; +import * as TsconfigPaths from 'tsconfig-paths'; +import findUp from 'find-up'; import type { IndexEntry, @@ -278,6 +280,35 @@ export class StoryIndexGenerator { ); } + /** + * Try to find the component path from a raw import string and return it in + * the same format as `importPath`. Respect tsconfig paths if available. + * + * If no such file exists, assume that the import is from a package and + * return the raw path. + */ + resolveComponentPath( + rawComponentPath: Path, + absolutePath: Path, + matchPath: TsconfigPaths.MatchPath | undefined + ) { + let rawPath = rawComponentPath; + if (matchPath) { + rawPath = matchPath(rawPath) ?? rawPath; + } + + const absoluteComponentPath = path.resolve(path.dirname(absolutePath), rawPath); + const existing = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.mts'] + .map((ext) => `${absoluteComponentPath}${ext}`) + .find((candidate) => fs.existsSync(candidate)); + if (existing) { + const relativePath = path.relative(this.options.workingDir, existing); + return slash(normalizeStoryPath(relativePath)); + } + + return rawComponentPath; + } + async extractStories( specifier: NormalizedStoriesSpecifier, absolutePath: Path, @@ -299,11 +330,25 @@ export class StoryIndexGenerator { invariant(indexer, `No matching indexer found for ${absolutePath}`); const indexInputs = await indexer.createIndex(absolutePath, { makeTitle: defaultMakeTitle }); + const tsconfigPath = await findUp('tsconfig.json', { cwd: this.options.workingDir }); + const tsconfig = TsconfigPaths.loadConfig(tsconfigPath); + let matchPath: TsconfigPaths.MatchPath | undefined; + if (tsconfig.resultType === 'success') { + matchPath = TsconfigPaths.createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths, [ + 'browser', + 'module', + 'main', + ]); + } const entries: ((StoryIndexEntryWithMetaId | DocsCacheEntry) & { tags: Tag[] })[] = indexInputs.map((input) => { const name = input.name ?? storyNameFromExport(input.exportName); + const componentPath = + input.rawComponentPath && + this.resolveComponentPath(input.rawComponentPath, absolutePath, matchPath); const title = input.title ?? defaultMakeTitle(); + // eslint-disable-next-line no-underscore-dangle const id = input.__id ?? toId(input.metaId ?? title, storyNameFromExport(input.exportName)); const tags = combineTags(...projectTags, ...(input.tags ?? [])); @@ -315,6 +360,7 @@ export class StoryIndexGenerator { name, title, importPath, + componentPath, tags, }; }); diff --git a/code/lib/core-server/src/utils/__mockdata__/src/componentPath/component.js b/code/lib/core-server/src/utils/__mockdata__/src/componentPath/component.js new file mode 100644 index 000000000000..ff8b4c56321a --- /dev/null +++ b/code/lib/core-server/src/utils/__mockdata__/src/componentPath/component.js @@ -0,0 +1 @@ +export default {}; diff --git a/code/lib/core-server/src/utils/__mockdata__/src/componentPath/extension.stories.js b/code/lib/core-server/src/utils/__mockdata__/src/componentPath/extension.stories.js new file mode 100644 index 000000000000..8a2b7cff9b7e --- /dev/null +++ b/code/lib/core-server/src/utils/__mockdata__/src/componentPath/extension.stories.js @@ -0,0 +1,7 @@ +import component from './component.js'; + +export default { + component, +}; + +export const StoryOne = {}; diff --git a/code/lib/core-server/src/utils/__mockdata__/src/componentPath/noExtension.stories.js b/code/lib/core-server/src/utils/__mockdata__/src/componentPath/noExtension.stories.js new file mode 100644 index 000000000000..4bb5febc075f --- /dev/null +++ b/code/lib/core-server/src/utils/__mockdata__/src/componentPath/noExtension.stories.js @@ -0,0 +1,7 @@ +import component from './component'; + +export default { + component, +}; + +export const StoryOne = {}; diff --git a/code/lib/core-server/src/utils/__mockdata__/src/componentPath/package.stories.js b/code/lib/core-server/src/utils/__mockdata__/src/componentPath/package.stories.js new file mode 100644 index 000000000000..20509edcf2be --- /dev/null +++ b/code/lib/core-server/src/utils/__mockdata__/src/componentPath/package.stories.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line import/no-unresolved +import component from 'component-package'; + +export default { + component, +}; + +export const StoryOne = {}; diff --git a/code/lib/core-server/src/utils/__tests__/index-extraction.test.ts b/code/lib/core-server/src/utils/__tests__/index-extraction.test.ts index a7bc19ae8ab9..00d644f07409 100644 --- a/code/lib/core-server/src/utils/__tests__/index-extraction.test.ts +++ b/code/lib/core-server/src/utils/__tests__/index-extraction.test.ts @@ -64,6 +64,7 @@ describe('story extraction', () => { "dependents": [], "entries": [ { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "metaId": "a", @@ -75,6 +76,7 @@ describe('story extraction', () => { "type": "story", }, { + "componentPath": undefined, "id": "some-fully-custom-id", "importPath": "./src/A.stories.js", "metaId": "custom-id", @@ -118,6 +120,7 @@ describe('story extraction', () => { "dependents": [], "entries": [ { + "componentPath": undefined, "id": "f--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", "metaId": undefined, @@ -163,6 +166,7 @@ describe('story extraction', () => { "dependents": [], "entries": [ { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", "metaId": "a", @@ -210,6 +214,7 @@ describe('story extraction', () => { "dependents": [], "entries": [ { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "metaId": "a", @@ -275,6 +280,7 @@ describe('story extraction', () => { "dependents": [], "entries": [ { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "metaId": undefined, @@ -286,6 +292,7 @@ describe('story extraction', () => { "type": "story", }, { + "componentPath": undefined, "id": "custom-title--story-two", "importPath": "./src/A.stories.js", "metaId": undefined, @@ -297,6 +304,7 @@ describe('story extraction', () => { "type": "story", }, { + "componentPath": undefined, "id": "custom-meta-id--story-three", "importPath": "./src/A.stories.js", "metaId": "custom-meta-id", @@ -341,6 +349,7 @@ describe('story extraction', () => { "dependents": [], "entries": [ { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "metaId": undefined, @@ -390,6 +399,7 @@ describe('docs entries from story extraction', () => { "dependents": [], "entries": [ { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "metaId": undefined, @@ -449,6 +459,7 @@ describe('docs entries from story extraction', () => { "type": "docs", }, { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "metaId": undefined, @@ -497,6 +508,7 @@ describe('docs entries from story extraction', () => { "dependents": [], "entries": [ { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "metaId": undefined, @@ -557,6 +569,7 @@ describe('docs entries from story extraction', () => { "type": "docs", }, { + "componentPath": undefined, "id": "a--story-one", "importPath": "./src/A.stories.js", "metaId": undefined, 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 0a7a2f4a3b5c..5ba134673c83 100644 --- a/code/lib/core-server/src/utils/stories-json.test.ts +++ b/code/lib/core-server/src/utils/stories-json.test.ts @@ -165,6 +165,42 @@ describe('useStoriesJson', () => { "title": "B", "type": "story", }, + "componentpath-extension--story-one": { + "componentPath": "./src/componentPath/component.js", + "id": "componentpath-extension--story-one", + "importPath": "./src/componentPath/extension.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/extension", + "type": "story", + }, + "componentpath-noextension--story-one": { + "componentPath": "./src/componentPath/component.js", + "id": "componentpath-noextension--story-one", + "importPath": "./src/componentPath/noExtension.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/noExtension", + "type": "story", + }, + "componentpath-package--story-one": { + "componentPath": "component-package", + "id": "componentpath-package--story-one", + "importPath": "./src/componentPath/package.stories.js", + "name": "Story One", + "tags": [ + "dev", + "test", + ], + "title": "componentPath/package", + "type": "story", + }, "d--story-one": { "id": "d--story-one", "importPath": "./src/D.stories.jsx", diff --git a/code/lib/csf-tools/src/CsfFile.test.ts b/code/lib/csf-tools/src/CsfFile.test.ts index 669109d268ac..1c0ccc08f711 100644 --- a/code/lib/csf-tools/src/CsfFile.test.ts +++ b/code/lib/csf-tools/src/CsfFile.test.ts @@ -1203,4 +1203,116 @@ describe('CsfFile', () => { `); }); }); + + describe('componenent paths', () => { + it('no component', () => { + const { indexInputs } = loadCsf( + dedent` + import { Component } from '../src/Component.js'; + export default { + title: 'custom foo title', + }; + + export const A = { + render: () => {}, + }; + `, + { makeTitle, fileName: 'foo/bar.stories.js' } + ).parse(); + + expect(indexInputs).toMatchInlineSnapshot(` + - type: story + importPath: foo/bar.stories.js + exportName: A + name: A + title: custom foo title + tags: [] + __id: custom-foo-title--a + `); + }); + + it('local component', () => { + const { indexInputs } = loadCsf( + dedent` + const Component = (props) =>