-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
/
CsfDocsRender.ts
153 lines (125 loc) · 5.16 KB
/
CsfDocsRender.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import type {
IndexEntry,
Renderer,
CSFFile,
PreparedStory,
StoryId,
RenderContextCallbacks,
} from '@storybook/types';
import type { Channel } from '@storybook/channels';
import { DOCS_RENDERED } from '@storybook/core-events';
import type { StoryStore } from '../../../store';
import type { Render, RenderType } from './Render';
import { PREPARE_ABORTED } from './Render';
import type { DocsContextProps } from '../docs-context/DocsContextProps';
import type { DocsRenderFunction } from '../docs-context/DocsRenderFunction';
import { DocsContext } from '../docs-context/DocsContext';
/**
* A CsfDocsRender is a render of a docs entry that is rendered based on a CSF file.
*
* The expectation is the primary CSF file which is the `importPath` for the entry will
* define a story which may contain the actual rendered JSX code for the template in the
* `docs.page` parameter.
*
* Use cases:
* - Autodocs, where there is no story, and we fall back to the globally defined template.
* - *.stories.mdx files, where the MDX compiler produces a CSF file with a `.parameter.docs.page`
* parameter containing the compiled content of the MDX file.
*/
export class CsfDocsRender<TRenderer extends Renderer> implements Render<TRenderer> {
public readonly type: RenderType = 'docs';
public readonly subtype = 'csf';
public readonly id: StoryId;
public story?: PreparedStory<TRenderer>;
public rerender?: () => Promise<void>;
public teardownRender?: (options: { viewModeChanged?: boolean }) => Promise<void>;
public torndown = false;
public readonly disableKeyListeners = false;
public preparing = false;
public csfFiles?: CSFFile<TRenderer>[];
constructor(
protected channel: Channel,
protected store: StoryStore<TRenderer>,
public entry: IndexEntry,
private callbacks: RenderContextCallbacks<TRenderer>
) {
this.id = entry.id;
}
isPreparing() {
return this.preparing;
}
async prepare() {
this.preparing = true;
const { entryExports, csfFiles = [] } = await this.store.loadEntry(this.id);
if (this.torndown) throw PREPARE_ABORTED;
const { importPath, title } = this.entry;
const primaryCsfFile = this.store.processCSFFileWithCache<TRenderer>(
entryExports,
importPath,
title
);
// We use the first ("primary") story from the CSF as the "current" story on the context.
// - When rendering "true" CSF files, this is for back-compat, where templates may expect
// a story to be current (even though now we render a separate docs entry from the stories)
// - when rendering a "docs only" (story) id, this will end up being the same story as
// this.id, as such "CSF files" have only one story
const primaryStoryId = Object.keys(primaryCsfFile.stories)[0];
this.story = this.store.storyFromCSFFile({ storyId: primaryStoryId, csfFile: primaryCsfFile });
this.csfFiles = [primaryCsfFile, ...csfFiles];
this.preparing = false;
}
isEqual(other: Render<TRenderer>): boolean {
return !!(
this.id === other.id &&
this.story &&
this.story === (other as CsfDocsRender<TRenderer>).story
);
}
docsContext(renderStoryToElement: DocsContextProps<TRenderer>['renderStoryToElement']) {
if (!this.csfFiles) throw new Error('Cannot render docs before preparing');
const docsContext = new DocsContext<TRenderer>(
this.channel,
this.store,
renderStoryToElement,
this.csfFiles
);
// All referenced CSF files should be attached for CSF docs
// - When you create two CSF files that both reference the same title, they are combined into
// a single CSF docs entry with a `storiesImport` defined.
this.csfFiles.forEach((csfFile) => docsContext.attachCSFFile(csfFile));
return docsContext;
}
async renderToElement(
canvasElement: TRenderer['canvasElement'],
renderStoryToElement: DocsContextProps<TRenderer>['renderStoryToElement']
) {
if (!this.story || !this.csfFiles) throw new Error('Cannot render docs before preparing');
const docsContext = this.docsContext(renderStoryToElement);
const { docs: docsParameter } = this.story.parameters || {};
if (!docsParameter)
throw new Error(
`Cannot render a story in viewMode=docs if \`@storybook/addon-docs\` is not installed`
);
const renderer = await docsParameter.renderer();
const { render } = renderer as { render: DocsRenderFunction<TRenderer> };
const renderDocs = async () => {
try {
// NOTE: it isn't currently possible to use a docs renderer outside of "web" mode.
await render(docsContext, docsParameter, canvasElement as any);
this.channel.emit(DOCS_RENDERED, this.id);
} catch (err) {
this.callbacks.showException(err as Error);
}
};
this.rerender = async () => renderDocs();
this.teardownRender = async ({ viewModeChanged }: { viewModeChanged?: boolean }) => {
if (!viewModeChanged || !canvasElement) return;
renderer.unmount(canvasElement);
};
return renderDocs();
}
async teardown({ viewModeChanged }: { viewModeChanged?: boolean } = {}) {
this.teardownRender?.({ viewModeChanged });
this.torndown = true;
}
}