Skip to content

Commit e026c13

Browse files
authored
feat(preview): propagate runtime preview errors (teambit#9676)
1 parent 5bd4625 commit e026c13

File tree

5 files changed

+55
-20
lines changed

5 files changed

+55
-20
lines changed

components/ui/rendering/html/html.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SsrStyles, removeSsrStyles } from './ssr-styles';
66
import { FullHeightStyle } from './full-height-style';
77

88
export const LOAD_EVENT = '_DOM_LOADED_';
9+
export const ERROR_EVENT = '_ERROR_OCCURRED_';
910

1011
export type Assets = Partial<{
1112
/** page title */
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { Html, ssrCleanup, LOAD_EVENT } from './html';
1+
export { Html, ssrCleanup, LOAD_EVENT, ERROR_EVENT } from './html';
22
export { MountPoint, mountPointId } from './mount-point';
33
export { popAssets } from './stored-assets';
44
export type { HtmlProps, Assets } from './html';

scopes/compositions/compositions/ui/composition-preview.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useMemo } from 'react';
22
import { ComponentPreview, ComponentPreviewProps } from '@teambit/preview.ui.component-preview';
3+
import { useWorkspaceMode } from '@teambit/workspace.ui.use-workspace-mode';
34

45
import { Composition } from '../composition';
56

@@ -11,6 +12,7 @@ export type ComponentCompositionProps = {
1112
} & ComponentPreviewProps;
1213

1314
export function ComponentComposition({ composition, component, queryParams = [], ...rest }: ComponentCompositionProps) {
15+
const { isMinimal } = useWorkspaceMode();
1416
const includesEnvTemplate = component.preview?.includesEnvTemplate;
1517
const isScaling = component.preview?.isScaling;
1618
const shouldAddNameParam = component.preview?.useNameParam || (isScaling && includesEnvTemplate === false);
@@ -30,6 +32,7 @@ export function ComponentComposition({ composition, component, queryParams = [],
3032
style={{ width: '100%', height: '100%' }}
3133
previewName="compositions"
3234
queryParams={compositionParams}
35+
propagateError={isMinimal}
3336
/>
3437
);
3538
}

scopes/preview/ui/component-preview/preview.tsx

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ import { compact } from 'lodash';
55
import { connectToChild } from 'penpal';
66
import { usePubSubIframe } from '@teambit/pubsub';
77
import { ComponentModel } from '@teambit/component';
8-
import { LOAD_EVENT } from '@teambit/ui-foundation.ui.rendering.html';
8+
import { ERROR_EVENT, LOAD_EVENT } from '@teambit/ui-foundation.ui.rendering.html';
99
import { toPreviewUrl } from './urls';
1010
import { computePreviewScale } from './compute-preview-scale';
1111
import { useIframeContentHeight } from './use-iframe-content-height';
1212
import styles from './preview.module.scss';
1313

1414
export type OnPreviewLoadProps = { height?: string; width?: string };
15-
1615
// omitting 'referrerPolicy' because of an TS error during build. Re-include when needed
1716
export interface ComponentPreviewProps extends Omit<IframeHTMLAttributes<HTMLIFrameElement>, 'src' | 'referrerPolicy'> {
1817
/**
@@ -76,6 +75,16 @@ export interface ComponentPreviewProps extends Omit<IframeHTMLAttributes<HTMLIFr
7675
* is preview being rendered in full height and should fit view height to content.
7776
*/
7877
fullContentHeight?: boolean;
78+
79+
/**
80+
* propagate error to the parent window from the iframe
81+
*/
82+
propagateError?: boolean;
83+
84+
/**
85+
* custom error handler for preview errors
86+
*/
87+
onPreviewError?: (errorData: any) => void;
7988
}
8089

8190
/**
@@ -97,6 +106,8 @@ export function ComponentPreview({
97106
onLoad,
98107
style,
99108
sandbox,
109+
propagateError,
110+
onPreviewError,
100111
...rest
101112
}: ComponentPreviewProps) {
102113
const [heightIframeRef, iframeHeight] = useIframeContentHeight({ skip: false, viewport });
@@ -113,16 +124,42 @@ export function ComponentPreview({
113124
// pubsubContext?.connect(iframeHeight);
114125

115126
useEffect(() => {
116-
const handleLoad = (event) => {
117-
if (event.data && (event.data.event === LOAD_EVENT || event.data.event === 'webpackInvalid')) {
127+
const handleMessage = (event) => {
128+
if ((event.data && event.data.event === LOAD_EVENT) ||
129+
(event.data && event.data.event === 'webpackInvalid')
130+
) {
118131
onLoad && onLoad(event);
119132
}
133+
134+
if (event.data && event.data.event === ERROR_EVENT) {
135+
const errorData = event.data.payload;
136+
onPreviewError?.(errorData)
137+
138+
if (propagateError && window.parent && window !== window.parent) {
139+
try {
140+
window.parent.postMessage({
141+
event: ERROR_EVENT,
142+
payload: {
143+
...errorData,
144+
forwardedFrom: {
145+
component: component.id,
146+
preview: previewName,
147+
}
148+
}
149+
}, '*');
150+
} catch (err) {
151+
// eslint-disable-next-line no-console
152+
console.error('failed to propagate error to parent', err);
153+
}
154+
}
155+
}
120156
};
121-
window.addEventListener('message', handleLoad);
157+
158+
window.addEventListener('message', handleMessage);
122159
return () => {
123-
window.removeEventListener('message', handleLoad);
160+
window.removeEventListener('message', handleMessage);
124161
};
125-
}, []);
162+
}, [component.id.toString(), onLoad, propagateError, onPreviewError]);
126163

127164
useEffect(() => {
128165
if (!iframeRef.current) return;

scopes/ui-foundation/ui/ui-server.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { flatten } from 'lodash';
21
import { ExpressMain } from '@teambit/express';
32
import { GraphqlMain } from '@teambit/graphql';
43
import { Logger } from '@teambit/logger';
@@ -204,25 +203,22 @@ export class UIServer {
204203
this.logger.error(e.message);
205204
});
206205

207-
const proxyEntries = await this.getProxyFromPlugins();
208-
209206
server.on('upgrade', (req, socket, head) => {
210207
const reqUrl = req.url?.replace(/\?.+$/, '') || '';
211208
const path = stripTrailingChar(reqUrl, '/');
212-
209+
const proxyEntries = this.getProxyFromPlugins();
213210
const entry = proxyEntries.find((proxy) =>
214211
proxy.context.some((item) => item === stripTrailingChar(path, '/'))
215212
);
216-
217213
if (!entry) {
218214
return;
219215
}
220-
221216
proxyServer.ws(req, socket, head, {
222217
target: entry.target,
223218
});
224219
});
225220

221+
const proxyEntries = this.getProxyFromPlugins();
226222
proxyEntries.forEach((entry) => {
227223
entry.context.forEach((route) => {
228224
this._proxyRoutes.add(route);
@@ -304,16 +300,15 @@ export class UIServer {
304300
return Port.getPortFromRange(portRange || [3100, 3200]);
305301
}
306302

307-
private async getProxyFromPlugins(): Promise<ProxyEntry[]> {
308-
const proxiesByPlugin = this.plugins.map((plugin) => {
303+
private getProxyFromPlugins() {
304+
const proxiesByPlugin = this.plugins.flatMap((plugin) => {
309305
return plugin.getProxy ? plugin.getProxy() : [];
310306
});
311-
312-
return flatten(await Promise.all(proxiesByPlugin));
307+
return proxiesByPlugin;
313308
}
314309

315310
private async getProxy(port = 4000) {
316-
const proxyEntries = await this.getProxyFromPlugins();
311+
const proxyEntries = this.getProxyFromPlugins();
317312
const catchAllProxies: ProxyEntry[] = [
318313
{
319314
context: ['/preview'],
@@ -339,7 +334,6 @@ export class UIServer {
339334
ws: true,
340335
},
341336
];
342-
343337
return gqlProxies.concat(proxyEntries).concat(catchAllProxies);
344338
}
345339

0 commit comments

Comments
 (0)