Skip to content

Commit

Permalink
Convert all Preview Web code to async/await
Browse files Browse the repository at this point in the history
  • Loading branch information
tmeasday committed Oct 30, 2023
1 parent 534e49c commit 5ebc055
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 125 deletions.
1 change: 0 additions & 1 deletion code/lib/preview-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"lodash": "^4.17.21",
"memoizerific": "^1.11.3",
"qs": "^6.10.0",
"synchronous-promise": "^2.0.15",
"ts-dedent": "^2.0.0",
"util-deprecate": "^1.0.2"
},
Expand Down
65 changes: 29 additions & 36 deletions code/lib/preview-api/src/modules/preview-web/Preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { dedent } from 'ts-dedent';
import { global } from '@storybook/global';
import { SynchronousPromise } from 'synchronous-promise';
import {
CONFIG_ERROR,
FORCE_REMOUNT,
Expand Down Expand Up @@ -75,7 +74,7 @@ export class Preview<TRenderer extends Renderer> {
// (Even simple things like `Promise.resolve()` and `await` involve the callback happening
// in the next promise "tick").
// See the comment in `storyshots-core/src/api/index.ts` for more detail.
initialize({
async initialize({
getStoryIndex,
importFn,
getProjectAnnotations,
Expand All @@ -93,9 +92,8 @@ export class Preview<TRenderer extends Renderer> {

this.setupListeners();

return this.getProjectAnnotationsOrRenderError(getProjectAnnotations).then(
(projectAnnotations) => this.initializeWithProjectAnnotations(projectAnnotations)
);
const projectAnnotations = await this.getProjectAnnotationsOrRenderError(getProjectAnnotations);
return this.initializeWithProjectAnnotations(projectAnnotations);
}

setupListeners() {
Expand All @@ -107,49 +105,46 @@ export class Preview<TRenderer extends Renderer> {
this.channel.on(FORCE_REMOUNT, this.onForceRemount.bind(this));
}

getProjectAnnotationsOrRenderError(
async getProjectAnnotationsOrRenderError(
getProjectAnnotations: () => MaybePromise<ProjectAnnotations<TRenderer>>
): Promise<ProjectAnnotations<TRenderer>> {
return SynchronousPromise.resolve()
.then(getProjectAnnotations)
.then((projectAnnotations) => {
if (projectAnnotations.renderToDOM)
deprecate(`\`renderToDOM\` is deprecated, please rename to \`renderToCanvas\``);

this.renderToCanvas = projectAnnotations.renderToCanvas || projectAnnotations.renderToDOM;
if (!this.renderToCanvas) {
throw new Error(dedent`
try {
const projectAnnotations = await getProjectAnnotations();
if (projectAnnotations.renderToDOM)
deprecate(`\`renderToDOM\` is deprecated, please rename to \`renderToCanvas\``);

this.renderToCanvas = projectAnnotations.renderToCanvas || projectAnnotations.renderToDOM;
if (!this.renderToCanvas) {
throw new Error(dedent`
Expected your framework's preset to export a \`renderToCanvas\` field.
Perhaps it needs to be upgraded for Storybook 6.4?
More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field
`);
}
return projectAnnotations;
})
.catch((err) => {
// This is an error extracting the projectAnnotations (i.e. evaluating the previewEntries) and
// needs to be show to the user as a simple error
this.renderPreviewEntryError('Error reading preview.js:', err);
throw err;
});
}
return projectAnnotations;
} catch (err) {
// This is an error extracting the projectAnnotations (i.e. evaluating the previewEntries) and
// needs to be show to the user as a simple error
this.renderPreviewEntryError('Error reading preview.js:', err as Error);
throw err;
}
}

// If initialization gets as far as project annotations, this function runs.
initializeWithProjectAnnotations(projectAnnotations: ProjectAnnotations<TRenderer>) {
async initializeWithProjectAnnotations(projectAnnotations: ProjectAnnotations<TRenderer>) {
this.storyStore.setProjectAnnotations(projectAnnotations);

this.setInitialGlobals();

const storyIndexPromise = this.getStoryIndexFromServer();

return storyIndexPromise
.then((storyIndex: StoryIndex) => this.initializeWithStoryIndex(storyIndex))
.catch((err) => {
this.renderPreviewEntryError('Error loading story index:', err);
throw err;
});
try {
const storyIndex = await this.getStoryIndexFromServer();
return await this.initializeWithStoryIndex(storyIndex);
} catch (err) {
this.renderPreviewEntryError('Error loading story index:', err as Error);
throw err;
}
}

async setInitialGlobals() {
Expand Down Expand Up @@ -360,9 +355,7 @@ export class Preview<TRenderer extends Renderer> {
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();
}
await this.storyStore.cacheAllCSFFiles();

return this.storyStore.extract(options);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { dedent } from 'ts-dedent';
import { global } from '@storybook/global';
import {
CURRENT_STORY_WAS_SET,
DOCS_PREPARED,
PRELOAD_ENTRIES,
PREVIEW_KEYDOWN,
SET_CURRENT_STORY,
SET_INDEX,
STORY_ARGS_UPDATED,
STORY_CHANGED,
STORY_ERRORED,
Expand Down Expand Up @@ -115,10 +113,10 @@ export class PreviewWithSelection<TFramework extends Renderer> extends Preview<T
}

// If initialization gets as far as the story index, this function runs.
initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike<void> {
return super.initializeWithStoryIndex(storyIndex).then(() => {
return this.selectSpecifiedStory();
});
async initializeWithStoryIndex(storyIndex: StoryIndex): Promise<void> {
await super.initializeWithStoryIndex(storyIndex);

return this.selectSpecifiedStory();
}

// Use the selection specifier to choose a story, then render it
Expand Down
102 changes: 28 additions & 74 deletions code/lib/preview-api/src/modules/store/StoryStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import memoize from 'memoizerific';
import type {
IndexEntry,
Renderer,
API_PreparedStoryIndex,
ComponentTitle,
Parameters,
Path,
Expand All @@ -24,7 +23,6 @@ import type {
} from '@storybook/types';
import mapValues from 'lodash/mapValues.js';
import pick from 'lodash/pick.js';
import { SynchronousPromise } from 'synchronous-promise';

import { HooksContext } from '../addons';
import { StoryIndexStore } from './StoryIndexStore';
Expand Down Expand Up @@ -63,7 +61,7 @@ export class StoryStore<TRenderer extends Renderer> {

prepareStoryWithCache: typeof prepareStory;

initializationPromise: SynchronousPromise<void>;
initializationPromise: Promise<void>;

// This *does* get set in the constructor but the semantics of `new SynchronousPromise` trip up TS
resolveInitializationPromise!: () => void;
Expand All @@ -80,7 +78,7 @@ export class StoryStore<TRenderer extends Renderer> {
this.prepareStoryWithCache = memoize(STORY_CACHE_SIZE)(prepareStory) as typeof prepareStory;

// We cannot call `loadStory()` until we've been initialized properly. But we can wait for it.
this.initializationPromise = new SynchronousPromise((resolve) => {
this.initializationPromise = new Promise((resolve) => {
this.resolveInitializationPromise = resolve;
});
}
Expand All @@ -100,19 +98,15 @@ export class StoryStore<TRenderer extends Renderer> {
initialize({
storyIndex,
importFn,
cache = false,
}: {
storyIndex?: StoryIndex;
importFn: ModuleImportFn;
cache?: boolean;
}): Promise<void> {
}): void {
this.storyIndex = new StoryIndexStore(storyIndex);
this.importFn = importFn;

// We don't need the cache to be loaded to call `loadStory`, we just need the index ready
this.resolveInitializationPromise();

return cache ? this.cacheAllCSFFiles() : SynchronousPromise.resolve();
}

// This means that one of the CSF files has changed.
Expand Down Expand Up @@ -142,18 +136,18 @@ export class StoryStore<TRenderer extends Renderer> {
}

// To load a single CSF file to service a story we need to look up the importPath in the index
loadCSFFileByStoryId(storyId: StoryId): Promise<CSFFile<TRenderer>> {
async loadCSFFileByStoryId(storyId: StoryId): Promise<CSFFile<TRenderer>> {
if (!this.storyIndex || !this.importFn)
throw new Error(`loadCSFFileByStoryId called before initialization`);

const { importPath, title } = this.storyIndex.storyIdToEntry(storyId);
return this.importFn(importPath).then((moduleExports) =>
// We pass the title in here as it may have been generated by autoTitle on the server.
this.processCSFFileWithCache(moduleExports, importPath, title)
);
const moduleExports = await this.importFn(importPath);

// We pass the title in here as it may have been generated by autoTitle on the server.
return this.processCSFFileWithCache(moduleExports, importPath, title);
}

loadAllCSFFiles({ batchSize = EXTRACT_BATCH_SIZE } = {}): Promise<
async loadAllCSFFiles({ batchSize = EXTRACT_BATCH_SIZE } = {}): Promise<
StoryStore<TRenderer>['cachedCSFFiles']
> {
if (!this.storyIndex) throw new Error(`loadAllCSFFiles called before initialization`);
Expand All @@ -163,44 +157,36 @@ export class StoryStore<TRenderer extends Renderer> {
storyId,
]);

const loadInBatches = (
const loadInBatches = async (
remainingImportPaths: typeof importPaths
): Promise<{ importPath: Path; csfFile: CSFFile<TRenderer> }[]> => {
if (remainingImportPaths.length === 0) return SynchronousPromise.resolve([]);
if (remainingImportPaths.length === 0) return Promise.resolve([]);

const csfFilePromiseList = remainingImportPaths
.slice(0, batchSize)
.map(([importPath, storyId]) =>
this.loadCSFFileByStoryId(storyId).then((csfFile) => ({
importPath,
csfFile,
}))
);

return SynchronousPromise.all(csfFilePromiseList).then((firstResults) =>
loadInBatches(remainingImportPaths.slice(batchSize)).then((restResults) =>
firstResults.concat(restResults)
)
);
.map(async ([importPath, storyId]) => ({
importPath,
csfFile: await this.loadCSFFileByStoryId(storyId),
}));

const firstResults = await Promise.all(csfFilePromiseList);
const restResults = await loadInBatches(remainingImportPaths.slice(batchSize));
return firstResults.concat(restResults);
};

return loadInBatches(importPaths).then((list) =>
list.reduce((acc, { importPath, csfFile }) => {
acc[importPath] = csfFile;
return acc;
}, {} as Record<Path, CSFFile<TRenderer>>)
);
const list = await loadInBatches(importPaths);
return list.reduce((acc, { importPath, csfFile }) => {
acc[importPath] = csfFile;
return acc;
}, {} as Record<Path, CSFFile<TRenderer>>);
}

cacheAllCSFFiles(): Promise<void> {
return this.initializationPromise.then(() =>
this.loadAllCSFFiles().then((csfFiles) => {
this.cachedCSFFiles = csfFiles;
})
);
async cacheAllCSFFiles(): Promise<void> {
await this.initializationPromise;
this.cachedCSFFiles = await this.loadAllCSFFiles();
}

preparedMetaFromCSFFile({ csfFile }: { csfFile: CSFFile<TRenderer> }): PreparedMeta<TRenderer> {
aspreparedMetaFromCSFFile({ csfFile }: { csfFile: CSFFile<TRenderer> }): PreparedMeta<TRenderer> {
if (!this.projectAnnotations) throw new Error(`storyFromCSFFile called before initialization`);

const componentAnnotations = csfFile.meta;
Expand Down Expand Up @@ -393,38 +379,6 @@ export class StoryStore<TRenderer extends Renderer> {
};
};

getSetIndexPayload(): API_PreparedStoryIndex {
if (!this.storyIndex) throw new Error('getSetIndexPayload called before initialization');
if (!this.cachedCSFFiles)
throw new Error('Cannot call getSetIndexPayload() unless you call cacheAllCSFFiles() first');
const { cachedCSFFiles } = this;

const stories = this.extract({ includeDocsOnly: true });

return {
v: 4,
entries: Object.fromEntries(
Object.entries(this.storyIndex.entries).map(([id, entry]) => [
id,
stories[id]
? {
...entry,
args: stories[id].initialArgs,
initialArgs: stories[id].initialArgs,
argTypes: stories[id].argTypes,
parameters: stories[id].parameters,
}
: {
...entry,
parameters: this.preparedMetaFromCSFFile({
csfFile: cachedCSFFiles[entry.importPath],
}).parameters,
},
])
),
};
}

raw(): BoundStory<TRenderer>[] {
return Object.values(this.extract())
.map(({ id }: { id: StoryId }) => this.fromId(id))
Expand Down
8 changes: 0 additions & 8 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6940,7 +6940,6 @@ __metadata:
qs: ^6.10.0
react: ^18.2.0
slash: ^5.0.0
synchronous-promise: ^2.0.15
ts-dedent: ^2.0.0
util-deprecate: ^1.0.2
languageName: unknown
Expand Down Expand Up @@ -28863,13 +28862,6 @@ __metadata:
languageName: node
linkType: hard

"synchronous-promise@npm:^2.0.15":
version: 2.0.17
resolution: "synchronous-promise@npm:2.0.17"
checksum: 1babe643d8417789ef6e5a2f3d4b8abcda2de236acd09bbe2c98f6be82c0a2c92ed21a6e4f934845fa8de18b1435a9cba1e8c3d945032e8a532f076224c024b1
languageName: node
linkType: hard

"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1":
version: 2.2.1
resolution: "tapable@npm:2.2.1"
Expand Down

0 comments on commit 5ebc055

Please sign in to comment.