diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts index dff869df19b..ff6da280f2b 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts @@ -956,7 +956,8 @@ describe('StoryIndexGenerator', () => { [normalizeStoriesEntry('./src/docs2/MetaOf.mdx', options)], options ); - await expect(() => generator.initialize()).rejects.toThrowError( + await generator.initialize(); + await expect(() => generator.getIndex()).rejects.toThrowError( /Could not find "..\/A.stories" for docs file/ ); }); diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.ts index 7cefe60a208..d5dfe1baef2 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.ts @@ -35,7 +35,11 @@ type StoriesCacheEntry = { dependents: Path[]; type: 'stories'; }; -type CacheEntry = false | StoriesCacheEntry | DocsCacheEntry; +type ErrorEntry = { + type: 'error'; + err: Error; +}; +type CacheEntry = false | StoriesCacheEntry | DocsCacheEntry | ErrorEntry; type SpecifierStoriesCache = Record; export const AUTODOCS_TAG = 'autodocs'; @@ -165,7 +169,12 @@ export class StoryIndexGenerator { return Promise.all( Object.keys(entry).map(async (absolutePath) => { if (entry[absolutePath] && !overwrite) return; - entry[absolutePath] = await updater(specifier, absolutePath, entry[absolutePath]); + + try { + entry[absolutePath] = await updater(specifier, absolutePath, entry[absolutePath]); + } catch (err) { + entry[absolutePath] = { type: 'error', err }; + } }) ); }) @@ -176,7 +185,7 @@ export class StoryIndexGenerator { return /(? { + async ensureExtracted(): Promise<(IndexEntry | ErrorEntry)[]> { // First process all the story files. Then, in a second pass, // process the docs files. The reason for this is that the docs // files may use the `` syntax, which requires @@ -193,9 +202,10 @@ export class StoryIndexGenerator { return this.specifiers.flatMap((specifier) => { const cache = this.specifierToCache.get(specifier); - return Object.values(cache).flatMap((entry): IndexEntry[] => { + return Object.values(cache).flatMap((entry): (IndexEntry | ErrorEntry)[] => { if (!entry) return []; if (entry.type === 'docs') return [entry]; + if (entry.type === 'error') return [entry]; return entry.entries; }); }); @@ -481,7 +491,11 @@ export class StoryIndexGenerator { // Extract any entries that are currently missing // Pull out each file's stories into a list of stories, to be composed and sorted const storiesList = await this.ensureExtracted(); - const sorted = await this.sortStories(storiesList); + + const firstError = storiesList.find((entry) => entry.type === 'error'); + if (firstError) throw (firstError as ErrorEntry).err; + + const sorted = await this.sortStories(storiesList as IndexEntry[]); let compat = sorted; if (this.options.storiesV2Compatibility) { diff --git a/code/lib/core-server/src/utils/doTelemetry.ts b/code/lib/core-server/src/utils/doTelemetry.ts index 4b262f20aff..1a5b62d6ce9 100644 --- a/code/lib/core-server/src/utils/doTelemetry.ts +++ b/code/lib/core-server/src/utils/doTelemetry.ts @@ -1,10 +1,11 @@ -import type { CoreConfig, Options } from '@storybook/types'; +import type { CoreConfig, Options, StoryIndex } from '@storybook/types'; import { telemetry, getPrecedingUpgrade } from '@storybook/telemetry'; import { useStorybookMetadata } from './metadata'; import type { StoryIndexGenerator } from './StoryIndexGenerator'; import { summarizeIndex } from './summarizeIndex'; import { router } from './router'; import { versionStatus } from './versionStatus'; +import { sendTelemetryError } from '../withTelemetry'; export async function doTelemetry( core: CoreConfig, @@ -13,7 +14,18 @@ export async function doTelemetry( ) { if (!core?.disableTelemetry) { initializedStoryIndexGenerator.then(async (generator) => { - const storyIndex = await generator?.getIndex(); + let storyIndex: StoryIndex; + try { + storyIndex = await generator?.getIndex(); + } catch (err) { + // If we fail to get the index, treat it as a recoverable error, but send it up to telemetry + // as if we crashed. In the future we will revisit this to send a distinct error + sendTelemetryError(err, 'dev', { + cliOptions: options, + presetOptions: { ...options, corePresets: [], overridePresets: [] }, + }); + return; + } const { versionCheck, versionUpdates } = options; const payload = { precedingUpgrade: await getPrecedingUpgrade(), diff --git a/code/lib/core-server/src/withTelemetry.ts b/code/lib/core-server/src/withTelemetry.ts index 8efcf050b8f..bc43ca68118 100644 --- a/code/lib/core-server/src/withTelemetry.ts +++ b/code/lib/core-server/src/withTelemetry.ts @@ -58,6 +58,36 @@ async function getErrorLevel({ cliOptions, presetOptions }: TelemetryOptions): P return 'full'; } +export async function sendTelemetryError( + error: Error, + eventType: EventType, + options: TelemetryOptions +) { + try { + const errorLevel = await getErrorLevel(options); + if (errorLevel !== 'none') { + const precedingUpgrade = await getPrecedingUpgrade(); + + await telemetry( + 'error', + { + eventType, + precedingUpgrade, + error: errorLevel === 'full' ? error : undefined, + errorHash: oneWayHash(error.message), + }, + { + immediate: true, + configDir: options.cliOptions.configDir || options.presetOptions?.configDir, + enableCrashReports: errorLevel === 'full', + } + ); + } + } catch (err) { + // if this throws an error, we just move on + } +} + export async function withTelemetry( eventType: EventType, options: TelemetryOptions, @@ -69,30 +99,7 @@ export async function withTelemetry( try { await run(); } catch (error) { - try { - const errorLevel = await getErrorLevel(options); - if (errorLevel !== 'none') { - const precedingUpgrade = await getPrecedingUpgrade(); - - await telemetry( - 'error', - { - eventType, - precedingUpgrade, - error: errorLevel === 'full' ? error : undefined, - errorHash: oneWayHash(error.message), - }, - { - immediate: true, - configDir: options.cliOptions.configDir || options.presetOptions?.configDir, - enableCrashReports: errorLevel === 'full', - } - ); - } - } catch (err) { - // if this throws an error, we just move on - } - + await sendTelemetryError(error, eventType, options); throw error; } }