feat(live-preview): add initial live preview package#441
feat(live-preview): add initial live preview package#441dipankarmaikap wants to merge 8 commits intomainfrom
Conversation
|
You have run out of free Bugbot PR reviews for this billing cycle. This will reset on March 1. To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial. |
packages/live-preview/src/index.ts
Outdated
| export { listenToStoryblokPreview } from './listenToStoryblokPreview'; | ||
| export { isInEditor } from './utils/isInEditor'; | ||
|
|
||
| // type exports |
There was a problem hiding this comment.
nitpick: the comments do not add value IMO
| * }); | ||
| * ``` | ||
| */ | ||
| export const subscribeToStoryblokPreview = async <T extends ISbComponentType<string> = any,>( |
There was a problem hiding this comment.
question: what is the benefit of having subscribeToStoryblokPreview and listenToStoryblokPreview?
I would recommend either removing this one completely because users can just add if (eventStoryId !== storyId) return; in their own callback, or make it one function and use function overloading.
export async function subscribeToStoryblokPreview<T extends ISbComponentType<string> = any>(
storyId: number,
callback: (newStory: ISbStoryData<T>) => void,
bridgeOptions?: BridgeParams,
): Promise<void>;
export async function subscribeToStoryblokPreview<T extends ISbComponentType<string> = any>(
callback: (newStory: ISbStoryData<T>) => void,
bridgeOptions?: BridgeParams,
): Promise<void>;
export async function subscribeToStoryblokPreview<T extends ISbComponentType<string> = any>(
storyIdOrCallback: number | ((newStory: ISbStoryData<T>) => void),
callbackOrBridgeOptions?: ((newStory: ISbStoryData<T>) => void) | BridgeParams,
maybeBridgeOptions?: BridgeParams,
): Promise<void> {
const storyId = typeof storyIdOrCallback === 'number' ? storyIdOrCallback : undefined;
const callback = typeof storyIdOrCallback === 'function' ? storyIdOrCallback : callbackOrBridgeOptions as (newStory: ISbStoryData<T>) => void;
const bridgeOptions = typeof storyIdOrCallback === 'function' ? callbackOrBridgeOptions as BridgeParams : maybeBridgeOptions;
if (!canUseStoryblokBridge()) return;
const bridge = await loadStoryblokBridge(bridgeOptions);
bridge.on(['input', 'change', 'published'], (event) => {
if (!event) return;
if (event.action === 'input') {
if (storyId !== undefined && event.story.id !== storyId) return;
callback(event.story as ISbStoryData<T>);
return;
}
if (event.action === 'change' || event.action === 'published') {
if (storyId !== undefined && event.storyId !== storyId) return;
window.location.reload();
}
});
}| // @ts-expect-error – intentional environment simulation | ||
| delete globalThis.window | ||
| expect(isBrowser()).toBe(false) | ||
| }) |
There was a problem hiding this comment.
opinion: Considering how simple those functions are, I don't think those tests are worth it.
| "type": "module", | ||
| "version": "0.0.0", | ||
| "private": false, | ||
| "packageManager": "pnpm@10.30.0", |
There was a problem hiding this comment.
nitpick: I've noticed that we have different values for packageManager throughout monoblok. My gut feeling tells me that we can/should remove this from individual packages and only add it to the root level package.json, or use the same version for all of them.
packages/live-preview/package.json
Outdated
| "prepublishOnly": "pnpm run build" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.2.3", |
There was a problem hiding this comment.
We specify 24.12.0 in .nvmrc, so we should use the matching types for version 24. Maybe you can fix this in all the other packages, too 🙏
| export const subscribeToStoryblokPreview = async <T extends ISbComponentType<string> = any,>( | ||
| storyId: number, | ||
| callback: (newStory: ISbStoryData<T>) => void, | ||
| bridgeOptions: BridgeParams, |
There was a problem hiding this comment.
If you decide to keep this function, I think bridgeOptions should be optional.
| * }); | ||
| * ``` | ||
| */ | ||
| export const listenToStoryblokPreview = async <T extends ISbComponentType<string> = any,>( |
There was a problem hiding this comment.
Not 100% happy with the naming. In the JS world, we have either addEventListener, when using listener wording, onX, or subscribeX if a cleanup/unsubscribe function is returned.
A couple of possible variants: onStoryblokPreviewEvent, onStoryblokEditorEvent.
There was a problem hiding this comment.
Also, not quite sure about the general concept: personally, I'd prefer onStoryblokEditorEvent('input', callback) and no default actions bound to change and publish.
If this is such a common case, then we should choose a very different naming like initLivePreview(onInput, { reloadOnSave: true }): Promise<StoryblokBridge>.
But I want to question the usefulness of reloading on change and published as a default. If your setup can handle live updating, then why not call callback on change and published, too? And if not (100% SSR) you probably don't need the input callback but only the reload on change and published.
There was a problem hiding this comment.
on publish and change you don't get the story. you get something like
reload: true
action: 'change'
slug: string
storyId: Story['id']
There was a problem hiding this comment.
I see.. I think most user friendly option would probably to have it all:
onStoryblokEditorEvent('input|change|publish', callback) and initLivePreview(onInput) (if we have both, I don't think reloadOnSave option is necessary).
But it's your decision, you're deeper into it. My statements are just gut feelings.
| describe('loadStoryblokBridge', () => { | ||
| beforeEach(() => { | ||
| vi.resetModules() | ||
| vi.clearAllMocks() |
There was a problem hiding this comment.
nitpick: formatting (is ESLint configured?)
| */ | ||
| export function loadStoryblokBridge(config?: BridgeParams) { | ||
| if (bridgePromise) { | ||
| if (config && !Object.is(config, storedConfig)) { |
There was a problem hiding this comment.
This would also throw an error for Object.is({ a: 'a' }, { a: 'a' }), which is not what we want. See packages/cli/src/api.ts#L21 (configsAreEqual).
alexjoverm
left a comment
There was a problem hiding this comment.
Looking great from my side! Let's wait to figure out the License thing in the internal discussion before publishing
| import { isBrowser } from './isBrowser'; | ||
| import { isInEditor } from './isInEditor'; | ||
|
|
||
| export function canUseStoryblokBridge(): boolean { |
There was a problem hiding this comment.
praise: nice helper, I've seen this logic used often!
| * console.log('Live updated story:', story) | ||
| * }) | ||
| * ``` | ||
| */ |
There was a problem hiding this comment.
praise: great name, way more self-descriptive than the current one we have in the SDKs
@storyblok/live-preview (initial version)
This PR introduces a new Live Preview package for Storyblok with a small, framework-agnostic API for handling preview updates on the client.
What’s included
1. High-level editor event helper (recommended)
onStoryblokEditorEventcovers the most common live preview use cases with minimal setup.Behavior:
inputevents with the updated storychangeandpublishedevents2. Low-level Preview Bridge access
For advanced use cases where full control is needed, the Preview Bridge can be accessed directly.
Notes:
3. Editor detection utility
An
isInEditorhelper is also exposed for cases where conditional editor-only logic is required. This requires a URL.