Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
0309863
Implement open-service architecture runtime sync
ndelangen Jun 1, 2026
d4dd5a6
Implement open-service architecture runtime sync
ndelangen Jun 1, 2026
0beda2b
Refactor open-service channel integration and enhance state synchroni…
ndelangen Jun 2, 2026
1dc1df2
Enhance open-service integration and background handling
ndelangen Jun 2, 2026
8ec8992
Refactor postMessage integration and clean up unused event exports
ndelangen Jun 2, 2026
abf5677
Enhance ManagerProvider initialization and refine open-service tests
ndelangen Jun 2, 2026
10d2dd5
Refine open-service documentation and enhance type safety in tests
ndelangen Jun 2, 2026
b58178b
Merge branch 'jeppe-cursor/docgen-subscription-referential-equality-5…
ndelangen Jun 2, 2026
5f3eb96
Merge branch 'norbert/service-clients' of github.com:storybookjs/stor…
ndelangen Jun 3, 2026
bd0719a
Refactor open-service registration and channel integration
ndelangen Jun 3, 2026
17d8157
Refactor open-service channel events and enhance synchronization logic
ndelangen Jun 3, 2026
ee68aa1
Refactor open-service imports and enhance build configuration
ndelangen Jun 3, 2026
d6268e2
Refactor global channel assignment in common preset
ndelangen Jun 3, 2026
68c5ca1
Refactor open-service documentation and enhance channel sync tests
ndelangen Jun 3, 2026
88125e7
Merge branch 'next' into norbert/service-clients
ndelangen Jun 3, 2026
034ba47
Remove deprecated `getServiceChannel` export from multiple modules
ndelangen Jun 3, 2026
b61ab95
Merge branch 'norbert/service-clients' of github.com:storybookjs/stor…
ndelangen Jun 3, 2026
1bd402d
Add OpenServiceMissingChannelError for improved error handling
ndelangen Jun 3, 2026
6dcbd33
Update Playwright configuration and add new E2E tests
ndelangen Jun 3, 2026
40d9619
Refactor Open Service API exports for improved clarity and structure
ndelangen Jun 3, 2026
b3f7da7
Refactor Open Service API imports and exports for improved clarity
ndelangen Jun 3, 2026
1e2b76a
Update Playwright configuration and enhance E2E testing setup
ndelangen Jun 3, 2026
4ea789c
Refactor global channel assignment in common preset
ndelangen Jun 3, 2026
a160e59
wip
ndelangen Jun 3, 2026
8619d7b
Fix the OOM problem
ndelangen Jun 4, 2026
a3ba897
Refactor channel management in Storybook
ndelangen Jun 4, 2026
26d1e08
Enhance channel management and testing in Storybook
ndelangen Jun 4, 2026
c6883a3
Refactor channel management and enhance service registration in Story…
ndelangen Jun 4, 2026
96bae6d
cleanup
ndelangen Jun 4, 2026
952b7f0
Refactor Vitest plugin configuration and channel management
ndelangen Jun 4, 2026
654d283
Enhance channel management in Storybook stories
ndelangen Jun 4, 2026
6d8a916
Enhance test setup for service commands and queries
ndelangen Jun 4, 2026
d8181dd
Enhance Vitest configuration and channel management
ndelangen Jun 4, 2026
25439eb
Refactor mock channel implementation and introduce test channel
ndelangen Jun 4, 2026
e6c885e
Merge branch 'next' into norbert/service-clients
ndelangen Jun 4, 2026
b3df163
Update channel type casting in AddonStore
ndelangen Jun 4, 2026
7386dd2
Merge branch 'norbert/service-clients' of github.com:storybookjs/stor…
ndelangen Jun 4, 2026
715e480
Merge branch 'next' into norbert/service-clients
ndelangen Jun 5, 2026
e3a77cc
Refactor: Update background service registration import path
ndelangen Jun 5, 2026
88bd895
chore: Add TypeScript configuration for Storybook
ndelangen Jun 5, 2026
323725b
Refactor: Remove redundant global channel assignment in initTransport…
ndelangen Jun 5, 2026
9957d28
Refactor: Replace global object references with globalThis in channel…
ndelangen Jun 5, 2026
62a1bc7
Refactor: Remove channel assignment from common preset service initia…
ndelangen Jun 5, 2026
86afb92
Refactor: Consolidate mockChannel import paths and remove unused files
ndelangen Jun 5, 2026
b11f8d4
Refactor: Update import paths for getChannel in globals-runtime and r…
ndelangen Jun 5, 2026
5136549
Refactor: Update channel management in Storybook stories
ndelangen Jun 5, 2026
829b7e0
Refactor: Update import path for channel management in addons module
ndelangen Jun 5, 2026
e7323c5
Refactor: Simplify telemetry error handling in runtime
ndelangen Jun 5, 2026
bbb594d
Refactor: Remove open-service client module
ndelangen Jun 5, 2026
80c2529
Refactor: Remove state schema validation from open-service module
ndelangen Jun 5, 2026
ef87843
Merge branch 'next' into norbert/service-clients
ndelangen Jun 5, 2026
d506661
Refactor: Enhance error handling in channel-slot tests
ndelangen Jun 5, 2026
2b11a51
Refactor: Update channel type casting in addon store
ndelangen Jun 5, 2026
5551ec9
Refactor: Restrict query and command overrides in service registry
ndelangen Jun 5, 2026
ad42215
Refactor: Enhance validation in parseStampedSnapshot function
ndelangen Jun 5, 2026
8e8a1d3
Refactor: Remove unnecessary wait time in open-service background tests
ndelangen Jun 5, 2026
60873aa
Refactor: Add undefined output query service fixture and update tests
ndelangen Jun 5, 2026
a6c1bb0
Refactor: Update global channel options handling in PostMessageTransp…
ndelangen Jun 5, 2026
22412f5
wip
ndelangen Jun 5, 2026
1336062
Refactor: Standardize channel options handling in PostMessageTranspor…
ndelangen Jun 5, 2026
747b49c
Merge branch 'norbert/service-clients' into norbert/remote-command-ex…
ndelangen Jun 5, 2026
434e664
Refactor: Improve documentation for remote command execution in README
ndelangen Jun 5, 2026
333f57c
Merge branch 'norbert/remote-command-execution' of github.com:storybo…
ndelangen Jun 5, 2026
ef9f323
Merge pull request #35068 from storybookjs/norbert/remote-command-exe…
JReinhold Jun 5, 2026
191b318
Merge branch 'next' into norbert/service-clients
JReinhold Jun 5, 2026
743764e
refactor(open-service): tidy channel transport, deps, and query hook …
JReinhold Jun 5, 2026
57ab839
refactor(open-service): schema-validate channel payloads and constrai…
JReinhold Jun 6, 2026
5905767
Add open-service sync demos
JReinhold Jun 6, 2026
182c107
Format open-service sync demos
JReinhold Jun 6, 2026
17e1200
Fix open-service sync type errors
JReinhold Jun 6, 2026
062e88b
Revert unrelated addon-vitest configureVitest change
JReinhold Jun 6, 2026
ccccfc0
ci: retry CircleCI after flaky portable-react E2E
JReinhold Jun 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions code/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const config = defineMain({
directory: '../core/src/preview',
titlePrefix: 'preview',
},
{
directory: '../core/src/shared',
titlePrefix: 'core/shared',
},
{
directory: '../core/src/components/brand',
titlePrefix: 'brand',
Expand Down
2 changes: 2 additions & 0 deletions code/.storybook/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ addons.setConfig({
renderLabel: ({ name, type }) => (type === 'story' ? name : startCase(name)),
},
});

import '../core/src/shared/open-service/sync-test/manager.tsx';
2 changes: 1 addition & 1 deletion code/.storybook/open-service-debug-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as v from 'valibot';
import { logger } from 'storybook/internal/node-logger';
import type { StoryIndexGenerator } from '../core/src/core-server/utils/StoryIndexGenerator.ts';

import { defineService } from '../core/src/shared/open-service/index.ts';
import { defineService } from 'storybook/open-service';
import { describeService, registerService } from '../core/src/shared/open-service/server.ts';

const DEBUG_SERVICE_ID = 'storybook/internal/open-service-debug';
Expand Down
2 changes: 2 additions & 0 deletions code/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ sb.mock(import('lodash-es/sum'));
sb.mock(import('uuid'));
/* eslint-enable depend/ban-dependencies */

import '../core/src/shared/open-service/sync-test/preview.ts';

const { document } = global;
globalThis.CONFIG_TYPE = 'DEVELOPMENT';

Expand Down
9 changes: 5 additions & 4 deletions code/.storybook/services-preset.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type { Options, StorybookConfigRaw } from 'storybook/internal/types';

import { registerOpenServiceSyncDemos } from '../core/src/shared/open-service/sync-test/server.ts';
import { registerOpenServiceDebugService } from './open-service-debug-service.ts';

/**
* Preset hook that registers the internal open-service debug service.
* Preset hook that registers internal open-service examples and the opt-in debug service.
*
* Lives in its own preset file so the `services` slot stays out of the public `StorybookConfig`
* surface while still letting the internal Storybook self-test the registration path. Set
* `STORYBOOK_OPEN_SERVICE_DEBUG=true` to opt in.
* Set `STORYBOOK_OPEN_SERVICE_DEBUG=true` to additionally register the debug service.
*/
export const services = async (_value: void, options: Options): Promise<void> => {
registerOpenServiceSyncDemos();

if (process.env.STORYBOOK_OPEN_SERVICE_DEBUG === 'true') {
await registerOpenServiceDebugService(
options.presets.apply<NonNullable<StorybookConfigRaw['storyIndexGenerator']>>(
Expand Down
1 change: 1 addition & 0 deletions code/.storybook/storybook.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { setProjectAnnotations } from '@storybook/react';
import { userEvent as storybookEvent, expect as storybookExpect } from 'storybook/test';

import '../core/src/shared/utils/toHaveLiveRegion.ts';

import preview from './preview.tsx';

vi.spyOn(console, 'warn').mockImplementation((...args) => console.log(...args));
Expand Down
4 changes: 4 additions & 0 deletions code/.storybook/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["**/*"]
}
36 changes: 25 additions & 11 deletions code/addons/vitest/src/vitest-plugin/setup-file.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { Channel } from 'storybook/internal/channels';
import { Channel, clearChannel, getChannel, setChannel } from 'storybook/internal/channels';

import { type Task, initTransport, modifyErrorMessage } from './setup-file.ts';
import {
type Task,
initTransport,
modifyErrorMessage,
restoreDefaultChannel,
} from './setup-file.ts';

describe('initTransport', () => {
afterEach(() => {
// Cleanup the global channel so each test can assert initialization behavior independently.

(globalThis as { __STORYBOOK_ADDONS_CHANNEL__?: Channel }).__STORYBOOK_ADDONS_CHANNEL__ =
undefined;
clearChannel();
});

it('should initialize the addons channel when missing', () => {
(globalThis as { __STORYBOOK_ADDONS_CHANNEL__?: Channel }).__STORYBOOK_ADDONS_CHANNEL__ =
undefined;
clearChannel();

initTransport();

expect(getChannel()).toBeInstanceOf(Channel);
});

it('restoreDefaultChannel reinstalls the default when the slot was replaced', () => {
initTransport();
const defaultRef = getChannel();

setChannel(new Channel({ transport: { setHandler: vi.fn(), send: vi.fn() } }));

expect(globalThis.__STORYBOOK_ADDONS_CHANNEL__).toBeInstanceOf(Channel);
restoreDefaultChannel();

expect(getChannel()).toBe(defaultRef);
});

it('should not overwrite an existing addons channel', () => {
const transport = { setHandler: vi.fn(), send: vi.fn() };
const existingChannel = new Channel({ transport });
globalThis.__STORYBOOK_ADDONS_CHANNEL__ = existingChannel;
clearChannel();
(globalThis as { __STORYBOOK_ADDONS_CHANNEL__?: Channel }).__STORYBOOK_ADDONS_CHANNEL__ =
existingChannel;

initTransport();

expect(globalThis.__STORYBOOK_ADDONS_CHANNEL__).toBe(existingChannel);
expect(getChannel()).toBe(existingChannel);
});
});

Expand Down
30 changes: 28 additions & 2 deletions code/addons/vitest/src/vitest-plugin/setup-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, beforeAll, vi } from 'vitest';
import type { RunnerTask } from 'vitest';

import { Channel } from 'storybook/internal/channels';
import { getChannel, setChannel } from 'storybook/internal/channels';

import { COMPONENT_TESTING_PANEL_ID } from '../constants.ts';

Expand All @@ -16,9 +17,31 @@ export type Task = Partial<RunnerTask> & {
meta: Record<string, any>;
};

let defaultChannel: Channel | null = null;

export const initTransport = () => {
const existing = getChannel();
if (existing) {
defaultChannel ??= existing as Channel;
return;
}

const transport = { setHandler: vi.fn(), send: vi.fn() };
globalThis.__STORYBOOK_ADDONS_CHANNEL__ ??= new Channel({ transport });
const channel = new Channel({ transport });
defaultChannel = channel;
setChannel(channel);
};

/** Restore the channel installed for story tests (e.g. after manager stories swap in a mock). */
export const restoreDefaultChannel = () => {
if (!defaultChannel) {
initTransport();
return;
}

if (getChannel() !== defaultChannel) {
setChannel(defaultChannel);
}
};

export const modifyErrorMessage = ({ task }: { task: Task }) => {
Expand All @@ -44,4 +67,7 @@ beforeAll(() => {
}
});

afterEach(modifyErrorMessage);
afterEach((ctx) => {
restoreDefaultChannel();
modifyErrorMessage({ task: ctx.task });
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export async function generateAddonSetupCode() {

const channel = createBrowserChannel({ page: 'preview' });
addons.setChannel(channel);
window.__STORYBOOK_ADDONS_CHANNEL__ = channel;

if (window.CONFIG_TYPE === 'DEVELOPMENT'){
window.__STORYBOOK_SERVER_CHANNEL__ = channel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const preview = new PreviewWeb(importFn, getProjectAnnotations);

window.__STORYBOOK_PREVIEW__ = preview;
window.__STORYBOOK_STORY_STORE__ = preview.storyStore;
window.__STORYBOOK_ADDONS_CHANNEL__ = channel;

if (import.meta.webpackHot) {
import.meta.webpackHot.addStatusHandler((status) => {
Expand Down
4 changes: 4 additions & 0 deletions code/core/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ const config: BuildEntries = {
exportEntries: ['./internal/types'],
entryPoint: './src/types/index.ts',
},
{
exportEntries: ['./open-service'],
entryPoint: './src/shared/open-service/index.ts',
},
],
runtime: [
{
Expand Down
9 changes: 7 additions & 2 deletions code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@
"code": "./src/manager-api/index.ts",
"default": "./dist/manager-api/index.js"
},
"./open-service": {
"types": "./dist/shared/open-service/index.d.ts",
"code": "./src/shared/open-service/index.ts",
"default": "./dist/shared/open-service/index.js"
},
"./package.json": "./package.json",
"./preview-api": {
"types": "./dist/preview-api/index.d.ts",
Expand Down Expand Up @@ -238,7 +243,6 @@
"!src/**/*"
],
"dependencies": {
"@preact/signals-core": "^1.14.2",
"@storybook/global": "^5.0.0",
"@storybook/icons": "^2.0.2",
"@testing-library/dom": "^10.4.1",
Expand All @@ -247,7 +251,6 @@
"@vitest/expect": "3.2.4",
"@vitest/spy": "3.2.4",
"@webcontainer/env": "^1.1.1",
"deepsignal": "^1.6.0",
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0",
"open": "^10.2.0",
"oxc-parser": "^0.127.0",
Expand Down Expand Up @@ -275,6 +278,7 @@
"@happy-dom/global-registrator": "^20.0.11",
"@ngard/tiny-isequal": "^1.1.0",
"@polka/compression": "^1.0.0-next.28",
"@preact/signals-core": "^1.14.2",
"@radix-ui/react-scroll-area": "1.2.0-rc.7",
"@radix-ui/react-slot": "^1.0.2",
"@react-aria/interactions": "^3.25.5",
Expand Down Expand Up @@ -321,6 +325,7 @@
"copy-to-clipboard": "^3.3.1",
"cross-spawn": "^7.0.6",
"deep-object-diff": "^1.1.0",
"deepsignal": "^1.6.0",
"dequal": "^2.0.2",
"detect-indent": "^7.0.1",
"detect-port": "^1.6.1",
Expand Down
24 changes: 24 additions & 0 deletions code/core/src/channels/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,27 @@ class Transport {
```

For more information visit: [storybook.js.org](https://storybook.js.org)

## Channel access (internal)

Storybook installs one shared addons channel per runtime (manager, preview iframe, dev server).
Use the channel-slot API from `storybook/internal/channels` in TypeScript — not direct reads of
`__STORYBOOK_ADDONS_CHANNEL__`.

| Operation | API |
| --------- | --- |
| Read (nullable) | `getChannel()` |
| Read (installed) | `requireChannel()` — use when the runtime entry has already installed a channel |
| Install / replace | `setChannel(channel)` or `addons.setChannel(channel)` |
| Clear | `clearChannel()` / `setChannel(null)` |

The global `__STORYBOOK_ADDONS_CHANNEL__` is mirrored when `setChannel` runs so builder preamble and
legacy snippets stay in sync. `getChannel()` reads the global slot first so duplicate bundles still
see the live channel.

**Per-runtime install (call sites do not wait):**

- **Preview iframe:** builders run generated `addons.setChannel(createBrowserChannel(...))` before `preview.ts` loads.
- **Manager:** `addons.setChannel` during manager boot, before `addons.register` callbacks.
- **Node server:** `services` preset calls `setChannel(options.channel)` before registering services; a noop channel is also bootstrapped at import in non-browser realms.
- **Tests:** `setChannel(mock)` in `beforeEach`, or rely on the Node import bootstrap noop.
116 changes: 116 additions & 0 deletions code/core/src/channels/channel-slot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

import { Channel } from './main.ts';
import {
clearChannel,
ensureChannel,
getChannel,
installNoopChannel,
setChannel,
} from './channel-slot.ts';

describe('channel slot', () => {
afterEach(() => {
clearChannel();
vi.unstubAllGlobals();
});

it('returns null after clearChannel', () => {
setChannel(new Channel({}));
clearChannel();

expect(getChannel()).toBeNull();
expect(globalThis.__STORYBOOK_ADDONS_CHANNEL__).toBeUndefined();
});

it('mirrors setChannel to the global slot', () => {
const channel = new Channel({});

setChannel(channel);

expect(getChannel()).toBe(channel);
expect(globalThis.__STORYBOOK_ADDONS_CHANNEL__).toBe(channel);
});

it('hydrates the module slot from a pre-existing global assignment', () => {
const channel = new Channel({});
clearChannel();
vi.stubGlobal('__STORYBOOK_ADDONS_CHANNEL__', channel);

expect(getChannel()).toBe(channel);
});

it('prefers the global slot over a stale module-level noop', () => {
clearChannel();
installNoopChannel();

const real = new Channel({ transport: { setHandler: vi.fn(), send: vi.fn() } });
vi.stubGlobal('__STORYBOOK_ADDONS_CHANNEL__', real);

expect(getChannel()).toBe(real);
});

it('installNoopChannel provides an in-process channel', () => {
clearChannel();
installNoopChannel();

expect(getChannel()).toBeInstanceOf(Channel);
expect(getChannel()?.hasTransport).toBe(false);
});

it('ensureChannel is a no-op when a channel is already installed', () => {
const channel = new Channel({});
setChannel(channel);

ensureChannel();

expect(getChannel()).toBe(channel);
});

it('ensureChannel installs a noop channel when missing', () => {
clearChannel();

ensureChannel();

expect(getChannel()).toBeInstanceOf(Channel);
});

it('setChannel(null) clears both module and global slots', () => {
const channel = new Channel({});
setChannel(channel);

setChannel(null);

expect(getChannel()).toBeNull();
expect(globalThis.__STORYBOOK_ADDONS_CHANNEL__).toBeUndefined();
});

it('replaces an existing channel on setChannel', () => {
const first = new Channel({});
const second = new Channel({ transport: { setHandler: vi.fn(), send: vi.fn() } });

setChannel(first);
setChannel(second);

expect(getChannel()).toBe(second);
expect(globalThis.__STORYBOOK_ADDONS_CHANNEL__).toBe(second);
});
});

describe('module import', () => {
it('does not auto-install a channel in a browser-like environment', async () => {
vi.stubGlobal('window', {});
vi.stubGlobal('document', {});
vi.stubEnv('VITEST', '');

vi.resetModules();
try {
const { getChannel } = await import('./channel-slot.ts');
expect(getChannel()).toBeNull();
} finally {
vi.unstubAllGlobals();
vi.unstubAllEnvs();
vi.resetModules();
}
});
Comment thread
ndelangen marked this conversation as resolved.
});
Loading