Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
## CES Info

### Group Members

| Student | ID |
| --------------------- | --------- |
| Beatriz Oziel de Lima | 202510621 |
| Rubens Brock Silva | 202510624 |
| Matvii Suk | 202514197 |

### Issues

| Issue | Resources |
| --------------------------------------------------------------- | ----------------------------------- |
| [#34258](https://github.com/storybookjs/storybook/issues/34258) | [Report doc](issue-34258-report.md) |
| [#34566](https://github.com/storybookjs/storybook/issues/34566) | [Report doc](issue-34566-report.md) |

## Rest of the Original README

<p align="center">
<a href="https://storybook.js.org/?ref=readme">
<picture>
Expand Down
10 changes: 7 additions & 3 deletions code/core/src/core-server/presets/common-override-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ export const framework: PresetProperty<'framework'> = async (config) => {
};

export const stories: PresetProperty<'stories'> = async (entries, options) => {
const resolvedEntries = typeof entries === 'function'
? await entries()
: entries;

if (options?.build?.test?.disableMDXEntries) {
return removeMDXEntries(entries, options);
return removeMDXEntries(resolvedEntries, options);
}
return entries;

return resolvedEntries;
};

export const typescript: PresetProperty<'typescript'> = async (input, options) => {
if (options?.build?.test?.disableDocgen) {
return { ...(input ?? {}), reactDocgen: false, check: false };
Expand Down
9 changes: 9 additions & 0 deletions code/core/src/manager-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum Category {
MANAGER_CORE_EVENTS = 'MANAGER_CORE-EVENTS',
MANAGER_ROUTER = 'MANAGER_ROUTER',
MANAGER_THEMING = 'MANAGER_THEMING',
MANAGER_UNIVERSAL_STORE = 'MANAGER_UNIVERSAL-STORE',
}

export class ProviderDoesNotExtendBaseProviderError extends StorybookError {
Expand Down Expand Up @@ -47,6 +48,14 @@ export class UncaughtManagerError extends StorybookError {
}
}

export {
UniversalStoreFollowerTimeoutError,
UniversalStoreIdRequiredError,
UniversalStoreMissingSubscribeArgumentError,
UniversalStoreNotConstructableError,
UniversalStoreNotReadyError,
} from './shared/universal-store/errors';

export class StatusTypeIdMismatchError extends StorybookError {
constructor(
public data: {
Expand Down
5 changes: 5 additions & 0 deletions code/core/src/manager/globals/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,11 @@ export default {
'ProviderDoesNotExtendBaseProviderError',
'StatusTypeIdMismatchError',
'UncaughtManagerError',
'UniversalStoreFollowerTimeoutError',
'UniversalStoreIdRequiredError',
'UniversalStoreMissingSubscribeArgumentError',
'UniversalStoreNotConstructableError',
'UniversalStoreNotReadyError',
],
'storybook/internal/router': [
'BaseLocationProvider',
Expand Down
60 changes: 60 additions & 0 deletions code/core/src/shared/universal-store/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { StorybookError } from '../../storybook-error';

export abstract class UniversalStoreError extends StorybookError {
constructor(props: { code: number; message: string; name: string }) {
super({
...props,
category: 'MANAGER_UNIVERSAL-STORE',
});
}
}

export class UniversalStoreFollowerTimeoutError extends UniversalStoreError {
constructor(public data: { id: string }) {
super({
name: 'UniversalStoreFollowerTimeoutError',
code: 1,
message: `No existing state found for follower with id: '${data.id}'. Make sure a leader with the same id exists before creating a follower.`,
});
}
}

export class UniversalStoreNotConstructableError extends UniversalStoreError {
constructor() {
super({
name: 'UniversalStoreNotConstructableError',
code: 1001,
message: 'UniversalStore is not constructable - use UniversalStore.create() instead',
});
}
}

export class UniversalStoreIdRequiredError extends UniversalStoreError {
constructor() {
super({
name: 'UniversalStoreIdRequiredError',
code: 1002,
message: 'id is required and must be a string, when creating a UniversalStore',
});
}
}

export class UniversalStoreNotReadyError extends UniversalStoreError {
constructor(public data: { id: string; action: 'set state' | 'send event' }) {
super({
name: 'UniversalStoreNotReadyError',
code: 1003,
message: `Cannot ${data.action} before store with id '${data.id}' is ready. You can get the current status with store.status, or await store.readyPromise to wait for the store to be ready before sending events.`,
});
Comment on lines +43 to +48

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Error message points to a non-existent readiness API.

UniversalStore exposes untilReady(), but this message tells users to await store.readyPromise, which is misleading and not available.

Proposed change
-      message: `Cannot ${data.action} before store with id '${data.id}' is ready. You can get the current status with store.status, or await store.readyPromise to wait for the store to be ready before sending events.`,
+      message: `Cannot ${data.action} before store with id '${data.id}' is ready. You can get the current status with store.status, or await store.untilReady() to wait for the store to be ready before sending events.`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(public data: { id: string; action: 'set state' | 'send event' }) {
super({
name: 'UniversalStoreNotReadyError',
code: 1003,
message: `Cannot ${data.action} before store with id '${data.id}' is ready. You can get the current status with store.status, or await store.readyPromise to wait for the store to be ready before sending events.`,
});
constructor(public data: { id: string; action: 'set state' | 'send event' }) {
super({
name: 'UniversalStoreNotReadyError',
code: 1003,
message: `Cannot ${data.action} before store with id '${data.id}' is ready. You can get the current status with store.status, or await store.untilReady() to wait for the store to be ready before sending events.`,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/core/src/shared/universal-store/errors.ts` around lines 43 - 48, The
error message in UniversalStoreNotReadyError's constructor references a
non-existent API (readyPromise); update the message to point users to the
correct readiness API (untilReady). Modify the message string in the
UniversalStoreNotReadyError constructor so it suggests checking store.status or
awaiting store.untilReady() (e.g., "You can get the current status with
store.status, or await store.untilReady() to wait for the store to be ready
before sending events."), leaving the rest of the error object (name, code,
data) unchanged.

}
}

export class UniversalStoreMissingSubscribeArgumentError extends UniversalStoreError {
constructor(public data: { id: string }) {
super({
name: 'UniversalStoreMissingSubscribeArgumentError',
code: 1004,
message: `Missing first subscribe argument, or second if first is the event type, when subscribing to a UniversalStore with id '${data.id}'`,
});
}
}
40 changes: 5 additions & 35 deletions code/core/src/shared/universal-store/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@ describe('UniversalStore', () => {
leader: true,
})
).toThrowErrorMatchingInlineSnapshot(
`[TypeError: UniversalStore is not constructable - use UniversalStore.create() instead]`
`[SB_MANAGER_UNIVERSAL-STORE_1001 (UniversalStoreNotConstructableError): UniversalStore is not constructable - use UniversalStore.create() instead]`
);
});

it('should throw when id is not provided', () => {
// Arrange, Act, Assert - creating an instance without an id and expect it to throw
expect(() => (UniversalStore as any).create()).toThrowErrorMatchingInlineSnapshot(
`[TypeError: id is required and must be a string, when creating a UniversalStore]`
`[SB_MANAGER_UNIVERSAL-STORE_1002 (UniversalStoreIdRequiredError): id is required and must be a string, when creating a UniversalStore]`
);
});

Expand Down Expand Up @@ -691,7 +691,7 @@ You should reuse the existing instance instead of trying to create a new one.`);
// Assert - eventually the follower.untilReady() promise should throw an error when the timeout is reached
vi.advanceTimersToNextTimer();
await expect(follower.untilReady()).rejects.toThrowErrorMatchingInlineSnapshot(
`[TypeError: No existing state found for follower with id: 'env1:test'. Make sure a leader with the same id exists before creating a follower.]`
`[SB_MANAGER_UNIVERSAL-STORE_0001 (UniversalStoreFollowerTimeoutError): No existing state found for follower with id: 'env1:test'. Make sure a leader with the same id exists before creating a follower.]`
);
expect(follower.status).toBe(UniversalStore.Status.ERROR);
});
Expand Down Expand Up @@ -942,22 +942,7 @@ You should reuse the existing instance instead of trying to create a new one.`);
expect(follower.status).toBe(UniversalStore.Status.SYNCING);

// Act & Assert - set state on the follower before it is ready and expect it to throw
expect(() => follower.setState({ count: 1 })).toThrowErrorMatchingInlineSnapshot(`
[TypeError: Cannot set state before store is ready. You can get the current status with store.status,
or await store.readyPromise to wait for the store to be ready before sending events.
{
"newState": {
"count": 1
},
"id": "env2:test",
"actor": {
"id": "m7405c0077777777778",
"type": "FOLLOWER",
"environment": "MANAGER"
},
"environment": "MANAGER"
}]
`);
expect(() => follower.setState({ count: 1 })).toThrowErrorMatchingInlineSnapshot(`[SB_MANAGER_UNIVERSAL-STORE_1003 (UniversalStoreNotReadyError): Cannot set state before store with id 'env2:test' is ready. You can get the current status with store.status, or await store.readyPromise to wait for the store to be ready before sending events.]`);
});
});

Expand Down Expand Up @@ -1127,22 +1112,7 @@ You should reuse the existing instance instead of trying to create a new one.`);
expect(follower.status).toBe(UniversalStore.Status.SYNCING);

// Act & Assert - send an event with the follower before it is ready and expect it to throw
expect(() => follower.send({ type: 'TOO_EARLY' })).toThrowErrorMatchingInlineSnapshot(`
[TypeError: Cannot send event before store is ready. You can get the current status with store.status,
or await store.readyPromise to wait for the store to be ready before sending events.
{
"event": {
"type": "TOO_EARLY"
},
"id": "env2:test",
"actor": {
"id": "m7405c0077777777778",
"type": "FOLLOWER",
"environment": "MANAGER"
},
"environment": "MANAGER"
}]
`);
expect(() => follower.send({ type: 'TOO_EARLY' })).toThrowErrorMatchingInlineSnapshot(`[SB_MANAGER_UNIVERSAL-STORE_1003 (UniversalStoreNotReadyError): Cannot send event before store with id 'env2:test' is ready. You can get the current status with store.status, or await store.readyPromise to wait for the store to be ready before sending events.]`);
});
});

Expand Down
55 changes: 14 additions & 41 deletions code/core/src/shared/universal-store/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { dedent } from 'ts-dedent';

import { instances } from './instances';
import {
UniversalStoreFollowerTimeoutError,
UniversalStoreIdRequiredError,
UniversalStoreMissingSubscribeArgumentError,
UniversalStoreNotConstructableError,
UniversalStoreNotReadyError,
} from './errors';
import type {
Actor,
ChannelEvent,
Expand Down Expand Up @@ -251,9 +258,7 @@ export class UniversalStore<
// it can only be called from within the static factory method create()
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_propertiessimulating_private_constructors
if (!UniversalStore.isInternalConstructing) {
throw new TypeError(
'UniversalStore is not constructable - use UniversalStore.create() instead'
);
throw new UniversalStoreNotConstructableError();
}
UniversalStore.isInternalConstructing = false;

Expand Down Expand Up @@ -336,7 +341,7 @@ export class UniversalStore<
CustomEvent extends { type: string; payload?: any } = { type: string; payload?: any },
>(options: StoreOptions<State>): UniversalStore<State, CustomEvent> {
if (!options || typeof options?.id !== 'string') {
throw new TypeError('id is required and must be a string, when creating a UniversalStore');
throw new UniversalStoreIdRequiredError();
}
if (options.debug) {
console.debug(
Expand Down Expand Up @@ -390,24 +395,11 @@ export class UniversalStore<
this.debug('setState', { newState, previousState, updater });

if (this.status !== UniversalStore.Status.READY) {
throw new TypeError(
dedent`Cannot set state before store is ready. You can get the current status with store.status,
or await store.readyPromise to wait for the store to be ready before sending events.
${JSON.stringify(
{
newState,
id: this.id,
actor: this.actor,
environment: this.environment,
},
null,
2
)}`
);
throw new UniversalStoreNotReadyError({ id: this.id, action: 'set state' });
}

this.state = newState;
const event = {
const event: SetStateEvent<State> = {
type: UniversalStore.InternalEventType.SET_STATE,
payload: {
state: newState,
Expand Down Expand Up @@ -442,9 +434,7 @@ export class UniversalStore<
this.debug('subscribe', { eventType, listener });

if (!listener) {
throw new TypeError(
`Missing first subscribe argument, or second if first is the event type, when subscribing to a UniversalStore with id '${this.id}'`
);
throw new UniversalStoreMissingSubscribeArgumentError({ id: this.id });
}

if (!this.listeners.has(eventType)) {
Expand Down Expand Up @@ -485,20 +475,7 @@ export class UniversalStore<
public send = (event: CustomEvent) => {
this.debug('send', { event });
if (this.status !== UniversalStore.Status.READY) {
throw new TypeError(
dedent`Cannot send event before store is ready. You can get the current status with store.status,
or await store.readyPromise to wait for the store to be ready before sending events.
${JSON.stringify(
{
event,
id: this.id,
actor: this.actor,
environment: this.environment,
},
null,
2
)}`
);
throw new UniversalStoreNotReadyError({ id: this.id, action: 'send event' });
}
this.emitToListeners(event, { actor: this.actor });
this.emitToChannel(event, { actor: this.actor });
Expand Down Expand Up @@ -544,11 +521,7 @@ export class UniversalStore<
setTimeout(() => {
// if the state is already resolved by a response before this timeout,
// rejecting it doesn't do anything, it will be ignored
this.syncing!.reject!(
new TypeError(
`No existing state found for follower with id: '${this.id}'. Make sure a leader with the same id exists before creating a follower.`
)
);
this.syncing!.reject!(new UniversalStoreFollowerTimeoutError({ id: this.id }));
}, 1000);
}
}
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/types/modules/core-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ export interface StorybookConfigRaw {

build?: TestBuildConfig;

stories: StoriesEntry[];
stories: StoriesEntry[] | (() => Promise<StoriesEntry[]>);

framework?: Preset;

Expand Down
7 changes: 3 additions & 4 deletions code/frameworks/nextjs-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ReactPreview } from '@storybook/react';
import { __definePreview } from '@storybook/react';
import type { ReactTypes } from '@storybook/react';

import type vitePluginStorybookNextJs from 'vite-plugin-storybook-nextjs';
import type { storybookNextJsPlugin } from './vite-plugin';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit file extension to relative import.

The import path should include an explicit .ts extension. As per coding guidelines, relative code imports should use explicit file extensions like ./foo.ts or ./bar.tsx.

📝 Proposed fix
-import type { storybookNextJsPlugin } from './vite-plugin';
+import type { storybookNextJsPlugin } from './vite-plugin/index.ts';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/frameworks/nextjs-vite/src/index.ts` at line 8, The import of
storybookNextJsPlugin in index.ts uses a relative path without an explicit
extension; update the import statement for storybookNextJsPlugin to reference
the TypeScript file with its .ts extension (e.g., './vite-plugin.ts') so the
module resolver and coding guidelines are satisfied and the symbol
storybookNextJsPlugin is imported from the explicit file.


import * as nextPreview from './preview';
import type { NextJsTypes } from './types';
Expand All @@ -17,9 +17,8 @@ export * from './types';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
declare module '@storybook/nextjs-vite/vite-plugin' {
export const storybookNextJsPlugin: typeof vitePluginStorybookNextJs;
}

export type StorybookNextJsPlugin = typeof storybookNextJsPlugin;

export function definePreview<Addons extends PreviewAddon<never>[]>(
preview: { addons?: Addons } & ProjectAnnotations<ReactTypes & NextJsTypes & InferTypes<Addons>>
Expand Down
10 changes: 7 additions & 3 deletions code/lib/cli-storybook/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,10 +803,14 @@ export const getStoriesPathsFromConfig = async ({
return [];
}

const normalizedStories = normalizeStories(stories, {
configDir,
const resolvedStories = typeof stories === 'function'
? await stories()
: stories;

const normalizedStories = normalizeStories(resolvedStories, {
configDir,
workingDir,
});
});

const matchingStoryFiles = await StoryIndexGenerator.findMatchingFilesForSpecifiers(
normalizedStories,
Expand Down
Loading
Loading