From db60041745030bae8922927db5c81caecfa8106c Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 9 Feb 2022 15:27:52 +1100 Subject: [PATCH 1/3] Add an `extract` function to `PreviewWeb` --- lib/preview-web/src/PreviewWeb.test.ts | 157 ++++++++++++++++++++++++- lib/preview-web/src/PreviewWeb.tsx | 29 +++++ 2 files changed, 185 insertions(+), 1 deletion(-) diff --git a/lib/preview-web/src/PreviewWeb.test.ts b/lib/preview-web/src/PreviewWeb.test.ts index 99b039120cea..359d4e22f87f 100644 --- a/lib/preview-web/src/PreviewWeb.test.ts +++ b/lib/preview-web/src/PreviewWeb.test.ts @@ -4,8 +4,8 @@ import merge from 'lodash/merge'; import Events, { IGNORED_EXCEPTION } from '@storybook/core-events'; import { logger } from '@storybook/client-logger'; import addons, { mockChannel as createMockChannel } from '@storybook/addons'; -import { ModuleImportFn } from '@storybook/store'; import { AnyFramework } from '@storybook/csf'; +import type { ModuleImportFn } from '@storybook/store'; import { PreviewWeb } from './PreviewWeb'; import { @@ -2725,4 +2725,159 @@ describe('PreviewWeb', () => { ); }); }); + + describe('extract', () => { + // NOTE: if you are using storyStoreV6, and your `preview.js` throws, we do not currently + // detect it (as we do not wrap the import of `preview.js` in a `try/catch`). The net effect + // of that is that the `PreviewWeb`/`StoryStore` end up in an uninitalized state. + it('throws an error if the preview is uninitialized', async () => { + const preview = new PreviewWeb(); + await expect(preview.extract()).rejects.toThrow(/Failed to initialize/); + }); + + it('throws an error if preview.js throws', async () => { + const err = new Error('meta error'); + const preview = new PreviewWeb(); + await expect( + preview.initialize({ + importFn, + getProjectAnnotations: () => { + throw err; + }, + }) + ).rejects.toThrow(err); + + await expect(preview.extract()).rejects.toThrow(err); + }); + + it('shows an error if the stories.json endpoint 500s', async () => { + const err = new Error('sort error'); + mockFetchResult = { status: 500, text: async () => err.toString() }; + + const preview = new PreviewWeb(); + await expect(preview.initialize({ importFn, getProjectAnnotations })).rejects.toThrow( + 'sort error' + ); + + await expect(preview.extract()).rejects.toThrow('sort error'); + }); + + it('waits for stories to be cached', async () => { + const [gate, openGate] = createGate(); + + const gatedImportFn = async (path) => { + await gate; + return importFn(path); + }; + + const preview = await createAndRenderPreview({ importFn: gatedImportFn }); + + let extracted = false; + preview.extract().then(() => { + extracted = true; + }); + + expect(extracted).toBe(false); + + openGate(); + await new Promise((r) => setTimeout(r, 0)); // Let the promise resolve + expect(extracted).toBe(true); + + expect(await preview.extract()).toMatchInlineSnapshot(` + Object { + "component-one--a": Object { + "argTypes": Object { + "foo": Object { + "name": "foo", + "type": Object { + "name": "string", + }, + }, + }, + "args": Object { + "foo": "a", + }, + "component": undefined, + "componentId": "component-one", + "id": "component-one--a", + "initialArgs": Object { + "foo": "a", + }, + "kind": "Component One", + "name": "A", + "parameters": Object { + "__isArgsStory": false, + "docs": Object { + "container": [MockFunction], + }, + "fileName": "./src/ComponentOne.stories.js", + }, + "story": "A", + "subcomponents": undefined, + "title": "Component One", + }, + "component-one--b": Object { + "argTypes": Object { + "foo": Object { + "name": "foo", + "type": Object { + "name": "string", + }, + }, + }, + "args": Object { + "foo": "b", + }, + "component": undefined, + "componentId": "component-one", + "id": "component-one--b", + "initialArgs": Object { + "foo": "b", + }, + "kind": "Component One", + "name": "B", + "parameters": Object { + "__isArgsStory": false, + "docs": Object { + "container": [MockFunction], + }, + "fileName": "./src/ComponentOne.stories.js", + }, + "story": "B", + "subcomponents": undefined, + "title": "Component One", + }, + "component-two--c": Object { + "argTypes": Object { + "foo": Object { + "name": "foo", + "type": Object { + "name": "string", + }, + }, + }, + "args": Object { + "foo": "c", + }, + "component": undefined, + "componentId": "component-two", + "id": "component-two--c", + "initialArgs": Object { + "foo": "c", + }, + "kind": "Component Two", + "name": "C", + "parameters": Object { + "__isArgsStory": false, + "fileName": "./src/ComponentTwo.stories.js", + }, + "playFunction": undefined, + "story": "C", + "subcomponents": undefined, + "title": "Component Two", + }, + } + `); + }); + }); }); diff --git a/lib/preview-web/src/PreviewWeb.tsx b/lib/preview-web/src/PreviewWeb.tsx index 813a671164c8..c8c6f7d84be5 100644 --- a/lib/preview-web/src/PreviewWeb.tsx +++ b/lib/preview-web/src/PreviewWeb.tsx @@ -80,6 +80,8 @@ export class PreviewWeb { renderToDOM: WebProjectAnnotations['renderToDOM']; + previewEntryError?: Error; + previousSelection: Selection; previousStory: Story; @@ -294,6 +296,8 @@ export class PreviewWeb { }: { getProjectAnnotations: () => MaybePromise>; }) { + delete this.previewEntryError; + const projectAnnotations = await this.getProjectAnnotationsOrRenderError(getProjectAnnotations); if (!this.storyStore.projectAnnotations) { await this.initializeWithProjectAnnotations(projectAnnotations); @@ -306,6 +310,8 @@ export class PreviewWeb { } async onStoryIndexChanged() { + delete this.previewEntryError; + if (!this.storyStore.projectAnnotations) { // We haven't successfully set project annotations yet, // we need to do that before we can do anything else. @@ -709,6 +715,28 @@ export class PreviewWeb { }; } + // API + async extract(options?: { includeDocsOnly: boolean }) { + if (this.previewEntryError) { + throw this.previewEntryError; + } + + if (!this.storyStore.projectAnnotations) { + // In v6 mode, if your preview.js throws, we never get a chance to initialize the preview + // or store, and the error is simply logged to the browser console. This is the best we can do + throw new Error(dedent`Failed to initialize Storybook. + + Do you have an error in your \`preview.js\`? Check your Storybook's browser console for errors.`); + } + + if (global.FEATURES?.storyStoreV7) { + await this.storyStore.cacheAllCSFFiles(); + } + + return this.storyStore.extract(options); + } + + // UTILITIES async cleanupPreviousRender({ unmountDocs = true }: { unmountDocs?: boolean } = {}) { const previousViewMode = this.previousStory?.parameters?.docsOnly ? 'docs' @@ -724,6 +752,7 @@ export class PreviewWeb { } renderPreviewEntryError(reason: string, err: Error) { + this.previewEntryError = err; logger.error(reason); logger.error(err); this.view.showErrorDisplay(err); From c49fbf093ca613c69cbcd5bc0b254a1377205508 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 9 Feb 2022 15:29:37 +1100 Subject: [PATCH 2/3] Wait for `PreviewWeb.extract` in extract script --- lib/cli/src/extract.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli/src/extract.ts b/lib/cli/src/extract.ts index 92adfb024989..ef8b7f9fb3d2 100644 --- a/lib/cli/src/extract.ts +++ b/lib/cli/src/extract.ts @@ -12,7 +12,7 @@ const read = async (url: string) => { await page.goto(url); await page.waitForFunction( - 'window.__STORYBOOK_STORY_STORE__ && window.__STORYBOOK_STORY_STORE__.extract && window.__STORYBOOK_STORY_STORE__.extract()' + 'window.__STORYBOOK_PREVIEW__ && window.__STORYBOOK_PREVIEW__.extract && window.__STORYBOOK_PREVIEW__.extract()' ); const data = JSON.parse( await page.evaluate(async () => { From 609a595c2f257eb7d6d40b5df5431f5c7c51ab75 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Thu, 10 Feb 2022 14:13:42 +0800 Subject: [PATCH 3/3] Fix PreviewWeb extract running against old SB --- lib/cli/src/extract.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/cli/src/extract.ts b/lib/cli/src/extract.ts index ef8b7f9fb3d2..670c2a2219b8 100644 --- a/lib/cli/src/extract.ts +++ b/lib/cli/src/extract.ts @@ -11,9 +11,12 @@ const read = async (url: string) => { await page.goto(url); - await page.waitForFunction( - 'window.__STORYBOOK_PREVIEW__ && window.__STORYBOOK_PREVIEW__.extract && window.__STORYBOOK_PREVIEW__.extract()' - ); + // we don't know whether we are running against a new or old storybook + // FIXME: add tests for both + await page.waitForFunction(` + (window.__STORYBOOK_PREVIEW__ && window.__STORYBOOK_PREVIEW__.extract && window.__STORYBOOK_PREVIEW__.extract()) || + (window.__STORYBOOK_STORY_STORE__ && window.__STORYBOOK_STORY_STORE__.extract && window.__STORYBOOK_STORY_STORE__.extract()) + `); const data = JSON.parse( await page.evaluate(async () => { // eslint-disable-next-line no-undef