diff --git a/code/builders/builder-vite/src/codegen-importfn-script.ts b/code/builders/builder-vite/src/codegen-importfn-script.ts index 48ce451b74f8..941519f49b4e 100644 --- a/code/builders/builder-vite/src/codegen-importfn-script.ts +++ b/code/builders/builder-vite/src/codegen-importfn-script.ts @@ -1,6 +1,6 @@ import * as path from 'path'; -import type { Options } from 'storybook/internal/types'; +import type { Options } from '@storybook/types'; import { listStories } from './list-stories'; @@ -14,7 +14,7 @@ import { listStories } from './list-stories'; * We want to deal in importPaths relative to the working dir, so we normalize */ function toImportPath(relativePath: string) { - return relativePath.startsWith('../') ? relativePath : `./${relativePath}`; + return /\.\.\/|virtual:/.test(relativePath) ? relativePath : `./${relativePath}`; } /** @@ -38,9 +38,17 @@ async function toImportFn(stories: string[]) { }; export async function importFn(path) { - return importers[path](); + if (/^virtual:/.test(path)) { + return import(/* @vite-ignore */ '/' + path); + } + + if (!(path in importers)) { + throw new Error(\`Storybook generated import script does no have an importer for "\${path}". Existing importers: \${Object.keys(importers)}\`); + } + + return importers[path](); } - `; +`; } export async function generateImportFnScriptCode(options: Options) { diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.ts b/code/core/src/core-server/utils/StoryIndexGenerator.ts index dfcf2d4a5f51..b6497913fe8a 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.ts @@ -246,6 +246,11 @@ export class StoryIndexGenerator { ); return Object.values(cache).flatMap((entry): (IndexEntry | ErrorEntry)[] => { if (!entry) return []; + + if (entry.type === 'docs' || entry.type === 'stories') { + entry.importPath = specifier.importPath; + } + if (entry.type === 'docs') return [entry]; if (entry.type === 'error') return [entry]; @@ -313,8 +318,20 @@ export class StoryIndexGenerator { absolutePath: Path, projectTags: Tag[] = [] ): Promise { - const relativePath = path.relative(this.options.workingDir, absolutePath); - const importPath = slash(normalizeStoryPath(relativePath)); + const formatPath = (filePath: string) => { + if (filePath.startsWith('virtual:')) { + return filePath; + } + + if (path.isAbsolute(filePath)) { + filePath = path.resolve(this.options.workingDir, filePath); + } + + return slash(normalizeStoryPath(filePath)); + }; + + const importPath = formatPath(absolutePath); + const defaultMakeTitle = (userTitle?: string) => { const title = userOrAutoTitleFromSpecifier(importPath, specifier, userTitle); invariant( @@ -358,8 +375,7 @@ export class StoryIndexGenerator { metaId: input.metaId, name, title, - importPath, - componentPath, + importPath: input.importPath ? formatPath(input.importPath) : importPath, tags, }; }); @@ -450,7 +466,7 @@ export class StoryIndexGenerator { invariant( csfEntry, dedent`Could not find or load CSF file at path "${result.of}" referenced by \`of={}\` in docs file "${relativePath}". - + - Does that file exist? - If so, is it a CSF file (\`.stories.*\`)? - If so, is it matched by the \`stories\` glob in \`main.js\`? @@ -722,15 +738,15 @@ export class StoryIndexGenerator { } catch (err) { once.warn(dedent` Unable to parse tags from project configuration. If defined, tags should be specified inline, e.g. - + export default { tags: ['foo'], } - + --- - + Received: - + ${previewCode} `); } diff --git a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts index 01a880479b99..4ebd2c902551 100644 --- a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts +++ b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts @@ -132,6 +132,47 @@ describe('story extraction', () => { `); }); + it('leaves virtual paths returned by indexers as is', async () => { + const relativePath = './src/first-nested/deeply/F.stories.js'; + const absolutePath = path.join(options.workingDir, relativePath); + const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); + + const generator = new StoryIndexGenerator([specifier], { + ...options, + indexers: [ + { + test: /\.stories\.(m?js|ts)x?$/, + createIndex: async (fileName) => [ + { + exportName: 'StoryOne', + type: 'story', + // importPath: "virtual:custom-indexer", + }, + ], + }, + ], + }); + const result = await generator.extractStories(specifier, absolutePath); + + expect(result).toMatchInlineSnapshot(` + { + "dependents": [], + "entries": [ + { + "id": "f--story-one", + "importPath": "virtual:custom-indexer", + "metaId": undefined, + "name": "Story One", + "tags": [], + "title": "F", + "type": "story", + }, + ], + "type": "stories", + } + `); + }); + it('auto-generates title from indexer inputs without title', async () => { const relativePath = './src/first-nested/deeply/F.stories.js'; const absolutePath = path.join(options.workingDir, relativePath); diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index d994449a2e72..de7b815d1a7a 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -69,6 +69,11 @@ export interface Presets { config: TypescriptOptions, args?: Options ): Promise; + apply( + extension: 'experimental_indexers', + config?: Indexer[], + args?: any + ): Promise; apply(extension: 'framework', config?: {}, args?: any): Promise; apply(extension: 'babel', config?: {}, args?: any): Promise; apply(extension: 'swc', config?: {}, args?: any): Promise; diff --git a/code/core/src/types/modules/indexer.ts b/code/core/src/types/modules/indexer.ts index ceb3bf915e51..dab71e200bdd 100644 --- a/code/core/src/types/modules/indexer.ts +++ b/code/core/src/types/modules/indexer.ts @@ -91,8 +91,11 @@ export type IndexEntry = StoryIndexEntry | DocsIndexEntry; * The base input for indexing a story or docs entry. */ export type BaseIndexInput = { - /** The file to import from e.g. the story file. */ - importPath: Path; + /** + * The file to import to render this story. + * If it's not defined, the path to file that was passed to the indexer will be used. + */ + importPath?: Path; /** The raw path/package of the file that provides meta.component, if one exists */ rawComponentPath?: Path; /** The name of the export to import. */ diff --git a/code/marko b/code/marko new file mode 160000 index 000000000000..9ac8191cede8 --- /dev/null +++ b/code/marko @@ -0,0 +1 @@ +Subproject commit 9ac8191cede89260787defaabb9dadc286697220