Skip to content

Commit 6f0ce3e

Browse files
committed
refactor to createBrowserChannel
1 parent 23a54f4 commit 6f0ce3e

File tree

13 files changed

+184
-131
lines changed

13 files changed

+184
-131
lines changed

code/builders/builder-vite/src/codegen-set-addon-channel.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
export async function generateAddonSetupCode() {
22
return `
3-
import { createPostMessageChannel } from '@storybook/channels';
3+
import { createBrowserChannel } from '@storybook/channels';
44
import { addons } from '@storybook/preview-api';
55
6-
const channel = createPostMessageChannel({ page: 'preview' });
6+
const channel = createBrowserChannel({ page: 'preview' });
77
addons.setChannel(channel);
88
window.__STORYBOOK_ADDONS_CHANNEL__ = channel;
99

code/builders/builder-webpack5/templates/virtualModuleModernEntry.js.handlebars

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { global } from '@storybook/global';
22

33
import { ClientApi, PreviewWeb, addons, composeConfigs } from '@storybook/preview-api';
4-
import { createPostMessageChannel } from '@storybook/channels';
4+
import { createBrowserChannel } from '@storybook/channels';
55

66
import { importFn } from './{{storiesFilename}}';
77

88
const getProjectAnnotations = () =>
99
composeConfigs([{{#each previewAnnotations}}require('{{this}}'),{{/each}}]);
1010

11-
const channel = createPostMessageChannel({ page: 'preview' });
11+
const channel = createBrowserChannel({ page: 'preview' });
1212
addons.setChannel(channel);
1313

1414
if (global.CONFIG_TYPE === 'DEVELOPMENT'){

code/lib/channels/src/index.ts

+32-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,40 @@
22
/// <reference path="./typings.d.ts" />
33

44
import { Channel } from './main';
5+
import { PostMessageTransport } from './postmessage';
6+
import type { ChannelTransport, Config } from './types';
7+
import { WebsocketTransport } from './websocket';
58

69
export * from './main';
710

811
export default Channel;
912

10-
export { createChannel as createPostMessageChannel } from './postmessage';
11-
export { createChannel as createWebSocketChannel } from './websocket';
13+
export { createChannel as createPostMessageChannel, PostMessageTransport } from './postmessage';
14+
export { createChannel as createWebSocketChannel, WebsocketTransport } from './websocket';
15+
16+
type Options = Config & {
17+
extraTransports?: ChannelTransport[];
18+
};
19+
20+
/**
21+
* Creates a new browser channel instance.
22+
* @param {Options} options - The options object.
23+
* @param {Page} options.page - The puppeteer page instance.
24+
* @param {ChannelTransport[]} [options.extraTransports=[]] - An optional array of extra channel transports.
25+
* @returns {Channel} - The new channel instance.
26+
*/
27+
export function createBrowserChannel({ page, extraTransports = [] }: Options): Channel {
28+
const transports: ChannelTransport[] = [new PostMessageTransport({ page }), ...extraTransports];
29+
30+
if (CONFIG_TYPE === 'DEVELOPMENT') {
31+
const protocol = window.location.protocol === 'http:' ? 'ws' : 'wss';
32+
const { hostname, port } = window.location;
33+
const channelUrl = `${protocol}://${hostname}:${port}/storybook-server-channel`;
34+
35+
transports.push(new WebsocketTransport({ url: channelUrl, onError: () => {} }));
36+
}
37+
38+
return new Channel({ transports });
39+
}
40+
41+
export type { Listener, ChannelEvent, ChannelTransport, ChannelHandler } from './types';

code/lib/channels/src/main.ts

+9-30
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,14 @@
11
/// <reference types="node" />
22

3-
export type ChannelHandler = (event: ChannelEvent) => void;
4-
5-
export interface ChannelTransport {
6-
send(event: ChannelEvent, options?: any): void;
7-
setHandler(handler: ChannelHandler): void;
8-
}
9-
10-
export interface ChannelEvent {
11-
type: string; // eventName
12-
from: string;
13-
args: any[];
14-
}
15-
16-
export interface Listener {
17-
(...args: any[]): void;
18-
}
19-
20-
interface EventsKeyValue {
21-
[key: string]: Listener[];
22-
}
23-
24-
type ChannelArgs = ChannelArgsSingle | ChannelArgsMulti;
25-
interface ChannelArgsSingle {
26-
transport?: ChannelTransport;
27-
async?: boolean;
28-
}
29-
interface ChannelArgsMulti {
30-
transports: ChannelTransport[];
31-
async?: boolean;
32-
}
3+
import type {
4+
ChannelArgs,
5+
ChannelArgsMulti,
6+
EventsKeyValue,
7+
ChannelTransport,
8+
ChannelArgsSingle,
9+
Listener,
10+
ChannelEvent,
11+
} from './types';
3312

3413
const isMulti = (args: ChannelArgs): args is ChannelArgsMulti => {
3514
// @ts-expect-error (we guard against this right here)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { logger } from '@storybook/client-logger';
2+
3+
export const getEventSourceUrl = (event: MessageEvent) => {
4+
const frames: HTMLIFrameElement[] = Array.from(
5+
document.querySelectorAll('iframe[data-is-storybook]')
6+
);
7+
// try to find the originating iframe by matching it's contentWindow
8+
// This might not be cross-origin safe
9+
const [frame, ...remainder] = frames.filter((element) => {
10+
try {
11+
return element.contentWindow === event.source;
12+
} catch (err) {
13+
// continue
14+
}
15+
16+
const src = element.getAttribute('src');
17+
let origin;
18+
19+
try {
20+
if (!src) {
21+
return false;
22+
}
23+
24+
({ origin } = new URL(src, document.location.toString()));
25+
} catch (err) {
26+
return false;
27+
}
28+
return origin === event.origin;
29+
});
30+
31+
const src = frame?.getAttribute('src');
32+
if (src && remainder.length === 0) {
33+
const { protocol, host, pathname } = new URL(src, document.location.toString());
34+
return `${protocol}//${host}${pathname}`;
35+
}
36+
37+
if (remainder.length > 0) {
38+
// If we found multiple matches, there's going to be trouble
39+
logger.error('found multiple candidates for event source');
40+
}
41+
42+
// If we found no frames of matches
43+
return null;
44+
};

code/lib/channels/src/postmessage/index.ts

+24-69
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,17 @@ import { logger, pretty } from '@storybook/client-logger';
77
import { isJSON, parse, stringify } from 'telejson';
88
import qs from 'qs';
99
import invariant from 'tiny-invariant';
10-
import { WebsocketTransport } from '../websocket/index';
11-
import type { ChannelHandler, ChannelEvent, ChannelTransport } from '../main';
1210
import { Channel } from '../main';
11+
import type {
12+
ChannelTransport,
13+
BufferedEvent,
14+
ChannelHandler,
15+
Config,
16+
ChannelEvent,
17+
} from '../types';
18+
import { getEventSourceUrl } from './getEventSourceUrl';
1319

14-
const { document, location, CONFIG_TYPE } = global;
15-
16-
interface Config {
17-
page: 'manager' | 'preview';
18-
}
19-
20-
interface BufferedEvent {
21-
event: ChannelEvent;
22-
resolve: (value?: any) => void;
23-
reject: (reason?: any) => void;
24-
}
20+
const { document, location } = global;
2521

2622
export const KEY = 'storybook-channel';
2723

@@ -31,7 +27,7 @@ const defaultEventOptions = { allowFunction: true, maxDepth: 25 };
3127
// that way we can send postMessage to child windows as well, not just iframe
3228
// https://stackoverflow.com/questions/6340160/how-to-get-the-references-of-all-already-opened-child-windows
3329

34-
export class PostmsgTransport implements ChannelTransport {
30+
export class PostMessageTransport implements ChannelTransport {
3531
private buffer: BufferedEvent[];
3632

3733
private handler?: ChannelHandler;
@@ -246,65 +242,24 @@ export class PostmsgTransport implements ChannelTransport {
246242
}
247243
}
248244

249-
const getEventSourceUrl = (event: MessageEvent) => {
250-
const frames: HTMLIFrameElement[] = Array.from(
251-
document.querySelectorAll('iframe[data-is-storybook]')
252-
);
253-
// try to find the originating iframe by matching it's contentWindow
254-
// This might not be cross-origin safe
255-
const [frame, ...remainder] = frames.filter((element) => {
256-
try {
257-
return element.contentWindow === event.source;
258-
} catch (err) {
259-
// continue
260-
}
261-
262-
const src = element.getAttribute('src');
263-
let origin;
264-
265-
try {
266-
if (!src) {
267-
return false;
268-
}
269-
270-
({ origin } = new URL(src, document.location.toString()));
271-
} catch (err) {
272-
return false;
273-
}
274-
return origin === event.origin;
275-
});
276-
277-
const src = frame?.getAttribute('src');
278-
if (src && remainder.length === 0) {
279-
const { protocol, host, pathname } = new URL(src, document.location.toString());
280-
return `${protocol}//${host}${pathname}`;
281-
}
282-
283-
if (remainder.length > 0) {
284-
// If we found multiple matches, there's going to be trouble
285-
logger.error('found multiple candidates for event source');
286-
}
287-
288-
// If we found no frames of matches
289-
return null;
290-
};
245+
/**
246+
* @deprecated This export is deprecated. Use `PostMessageTransport` instead. This API will be removed in 8.0.
247+
*/
248+
export const PostmsgTransport = PostMessageTransport;
291249

292250
/**
293-
* Creates a channel which communicates with an iframe or child window.
251+
* @deprecated This function is deprecated. Use the `createBrowserChannel` factory function from `@storybook/channels` instead. This API will be removed in 8.0.
252+
* @param {Config} config - The configuration object.
253+
* @returns {Channel} The channel instance.
294254
*/
295255
export function createChannel({ page }: Config): Channel {
296-
const transports: ChannelTransport[] = [new PostmsgTransport({ page })];
297-
298-
if (CONFIG_TYPE === 'DEVELOPMENT') {
299-
const protocol = window.location.protocol === 'http:' ? 'ws' : 'wss';
300-
const { hostname, port } = window.location;
301-
const channelUrl = `${protocol}://${hostname}:${port}/storybook-server-channel`;
302-
303-
transports.push(new WebsocketTransport({ url: channelUrl, onError: () => {} }));
304-
}
305-
306-
return new Channel({ transports });
256+
const transport = new PostmsgTransport({ page });
257+
return new Channel({ transport });
307258
}
308259

309-
// backwards compat with builder-vite
260+
/**
261+
* @deprecated This function is deprecated. Use the `createBrowserChannel` factory function from `@storybook/channels` instead. This API will be removed in 8.0.
262+
* @param {Config} config - The configuration object.
263+
* @returns {Channel} The channel instance.
264+
*/
310265
export default createChannel;

code/lib/channels/src/types.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export interface Config {
2+
page: 'manager' | 'preview';
3+
}
4+
5+
export interface BufferedEvent {
6+
event: ChannelEvent;
7+
resolve: (value?: any) => void;
8+
reject: (reason?: any) => void;
9+
}
10+
11+
export type ChannelHandler = (event: ChannelEvent) => void;
12+
13+
export interface ChannelTransport {
14+
send(event: ChannelEvent, options?: any): void;
15+
setHandler(handler: ChannelHandler): void;
16+
}
17+
18+
export interface ChannelEvent {
19+
type: string; // eventName
20+
from: string;
21+
args: any[];
22+
}
23+
24+
export interface Listener {
25+
(...args: any[]): void;
26+
}
27+
28+
export interface EventsKeyValue {
29+
[key: string]: Listener[];
30+
}
31+
32+
export type ChannelArgs = ChannelArgsSingle | ChannelArgsMulti;
33+
export interface ChannelArgsSingle {
34+
transport?: ChannelTransport;
35+
async?: boolean;
36+
}
37+
export interface ChannelArgsMulti {
38+
transports: ChannelTransport[];
39+
async?: boolean;
40+
}

code/lib/channels/src/websocket/index.ts

+17-19
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ import { global } from '@storybook/global';
55
import { logger } from '@storybook/client-logger';
66
import { isJSON, parse, stringify } from 'telejson';
77
import invariant from 'tiny-invariant';
8-
import type { ChannelHandler, ChannelTransport } from '../main';
98
import { Channel } from '../main';
10-
11-
const { CONFIG_TYPE } = global;
9+
import type { ChannelTransport, ChannelHandler } from '../types';
1210

1311
const { WebSocket } = global;
1412

@@ -25,12 +23,12 @@ interface CreateChannelArgs {
2523
onError?: OnError;
2624
}
2725

28-
export class WebsocketTransport {
29-
private socket: WebSocket;
26+
export class WebsocketTransport implements ChannelTransport {
27+
private buffer: string[] = [];
3028

3129
private handler?: ChannelHandler;
3230

33-
private buffer: string[] = [];
31+
private socket: WebSocket;
3432

3533
private isReady = false;
3634

@@ -80,28 +78,28 @@ export class WebsocketTransport {
8078
}
8179
}
8280

81+
/**
82+
* @deprecated This function is deprecated. Use the `createBrowserChannel` factory function from `@storybook/channels` instead. This API will be removed in 8.0.
83+
* @param {CreateChannelArgs} options - The options for creating the channel.
84+
* @param {string} [options.url] - The URL of the WebSocket server to connect to.
85+
* @param {boolean} [options.async=false] - Whether the channel should be asynchronous.
86+
* @param {OnError} [options.onError] - A function to handle errors that occur during the channel's lifetime.
87+
* @returns {Channel} - The newly created channel.
88+
*/
8389
export function createChannel({
8490
url,
8591
async = false,
8692
onError = (err) => logger.warn(err),
8793
}: CreateChannelArgs) {
88-
const transports: ChannelTransport[] = [];
89-
90-
if (url) {
91-
transports.push(new WebsocketTransport({ url, onError }));
92-
}
93-
94-
const isUrlServerChannel = !!url?.includes('storybook-server-channel');
95-
96-
if (CONFIG_TYPE === 'DEVELOPMENT' && isUrlServerChannel === false) {
94+
let channelUrl = url;
95+
if (!channelUrl) {
9796
const protocol = window.location.protocol === 'http:' ? 'ws' : 'wss';
9897
const { hostname, port } = window.location;
99-
const channelUrl = `${protocol}://${hostname}:${port}/storybook-server-channel`;
100-
101-
transports.push(new WebsocketTransport({ url: channelUrl, onError: () => {} }));
98+
channelUrl = `${protocol}://${hostname}:${port}/storybook-server-channel`;
10299
}
103100

104-
return new Channel({ transports, async });
101+
const transport = new WebsocketTransport({ url: channelUrl, onError });
102+
return new Channel({ transport, async });
105103
}
106104

107105
// backwards compat with builder-vite

0 commit comments

Comments
 (0)