-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
SvelteKit: Fix set_context_after_init error when experimental async is enabled
#32513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
6b427b6
6a41bf0
f0b7aad
b5a1813
badcfc9
41b0ff0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,138 +1,17 @@ | ||
| import type { Decorator } from '@storybook/svelte'; | ||
| import MockProvider from '@storybook/sveltekit/internal/MockProvider.svelte'; | ||
|
|
||
| import { action } from 'storybook/actions'; | ||
| import { onMount } from 'svelte'; | ||
|
|
||
| import { setAfterNavigateArgument } from './mocks/app/navigation'; | ||
| import { setNavigating, setPage, setUpdated } from './mocks/app/stores'; | ||
| import type { HrefConfig, NormalizedHrefConfig, SvelteKitParameters } from './types'; | ||
|
|
||
| const normalizeHrefConfig = (hrefConfig: HrefConfig): NormalizedHrefConfig => { | ||
| if (typeof hrefConfig === 'function') { | ||
| return { callback: hrefConfig, asRegex: false }; | ||
| } | ||
| return hrefConfig; | ||
| }; | ||
| import type { SvelteKitParameters } from './types'; | ||
|
|
||
| const svelteKitMocksDecorator: Decorator = (Story, ctx) => { | ||
| const svelteKitParameters: SvelteKitParameters = ctx.parameters?.sveltekit_experimental ?? {}; | ||
| setPage(svelteKitParameters?.stores?.page); | ||
| setNavigating(svelteKitParameters?.stores?.navigating); | ||
| setUpdated(svelteKitParameters?.stores?.updated); | ||
| setAfterNavigateArgument(svelteKitParameters?.navigation?.afterNavigate); | ||
|
|
||
| onMount(() => { | ||
| const globalClickListener = (e: MouseEvent) => { | ||
| // we add a global click event listener and we check if there's a link in the composedPath | ||
| const path = e.composedPath(); | ||
| const element = path.findLast((el) => el instanceof HTMLElement && el.tagName === 'A'); | ||
| if (element && element instanceof HTMLAnchorElement) { | ||
| // if the element is an a-tag we get the href of the element | ||
| // and compare it to the hrefs-parameter set by the user | ||
| const to = element.getAttribute('href'); | ||
| if (!to) { | ||
| return; | ||
| } | ||
| e.preventDefault(); | ||
| const defaultActionCallback = () => action('navigate')(to, e); | ||
| if (!svelteKitParameters.hrefs) { | ||
| defaultActionCallback(); | ||
| return; | ||
| } | ||
|
|
||
| let callDefaultCallback = true; | ||
| // we loop over every href set by the user and check if the href matches | ||
| // if it does we call the callback provided by the user and disable the default callback | ||
| Object.entries(svelteKitParameters.hrefs).forEach(([href, hrefConfig]) => { | ||
| const { callback, asRegex } = normalizeHrefConfig(hrefConfig); | ||
| const isMatch = asRegex ? new RegExp(href).test(to) : to === href; | ||
| if (isMatch) { | ||
| callDefaultCallback = false; | ||
| callback?.(to, e); | ||
| } | ||
| }); | ||
| if (callDefaultCallback) { | ||
| defaultActionCallback(); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Function that create and add listeners for the event that are emitted by the mocked | ||
| * functions. The event name is based on the function name | ||
| * | ||
| * Eg. storybook:goto, storybook:invalidateAll | ||
| * | ||
| * @param baseModule The base module where the function lives (navigation|forms) | ||
| * @param functions The list of functions in that module that emit events | ||
| * @param {boolean} [defaultToAction] The list of functions in that module that emit events | ||
| * @returns A function to remove all the listener added | ||
| */ | ||
| function createListeners( | ||
| baseModule: keyof SvelteKitParameters, | ||
| functions: string[], | ||
| defaultToAction?: boolean | ||
| ) { | ||
| // the array of every added listener, we can use this in the return function | ||
| // to clean them | ||
| const toRemove: Array<{ | ||
| eventType: string; | ||
| listener: (event: { detail: any[] }) => void; | ||
| }> = []; | ||
| functions.forEach((func) => { | ||
| // we loop over every function and check if the user actually passed | ||
| // a function in sveltekit_experimental[baseModule][func] eg. sveltekit_experimental.navigation.goto | ||
| const hasFunction = | ||
| (svelteKitParameters as any)[baseModule]?.[func] && | ||
| (svelteKitParameters as any)[baseModule][func] instanceof Function; | ||
| // if we default to an action we still add the listener (this will be the case for goto, invalidate, invalidateAll) | ||
| if (hasFunction || defaultToAction) { | ||
| // we create the listener that will just get the detail array from the custom element | ||
| // and call the user provided function spreading this args in...this will basically call | ||
| // the function that the user provide with the same arguments the function is invoked to | ||
|
|
||
| // eg. if it calls goto("/my-route") inside the component the function sveltekit_experimental.navigation.goto | ||
| // it provided to storybook will be called with "/my-route" | ||
| const listener = ({ detail = [] as any[] }) => { | ||
| const args = Array.isArray(detail) ? detail : []; | ||
| // if it has a function in the parameters we call that function | ||
| // otherwise we invoke the action | ||
| const fnToCall = hasFunction | ||
| ? (svelteKitParameters as any)[baseModule][func] | ||
| : action(func); | ||
| fnToCall(...args); | ||
| }; | ||
| const eventType = `storybook:${func}`; | ||
| toRemove.push({ eventType, listener }); | ||
| // add the listener to window | ||
| (window.addEventListener as any)(eventType, listener); | ||
| } | ||
| }); | ||
| return () => { | ||
| // loop over every listener added and remove them | ||
| toRemove.forEach(({ eventType, listener }) => { | ||
| // @ts-expect-error apparently you can't remove a custom listener to the window with TS | ||
| window.removeEventListener(eventType, listener); | ||
| }); | ||
| }; | ||
| } | ||
|
|
||
| const removeNavigationListeners = createListeners( | ||
| 'navigation', | ||
| ['goto', 'invalidate', 'invalidateAll', 'pushState', 'replaceState'], | ||
| true | ||
| ); | ||
| const removeFormsListeners = createListeners('forms', ['enhance']); | ||
| window.addEventListener('click', globalClickListener); | ||
|
|
||
| return () => { | ||
| window.removeEventListener('click', globalClickListener); | ||
| removeNavigationListeners(); | ||
| removeFormsListeners(); | ||
| }; | ||
| }); | ||
|
|
||
| return Story(); | ||
| return { | ||
| Component: MockProvider, | ||
| props: { | ||
| svelteKitParameters, | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| export const decorators: Decorator[] = [svelteKitMocksDecorator]; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,129 @@ | ||||||||||||||||||||||||||
| <script> | ||||||||||||||||||||||||||
| import { onMount } from 'svelte'; | ||||||||||||||||||||||||||
| import { action } from 'storybook/actions'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import { setAfterNavigateArgument } from '../src/mocks/app/navigation'; | ||||||||||||||||||||||||||
| import { setNavigating, setPage, setUpdated } from '../src/mocks/app/stores'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const{ svelteKitParameters = {}, children } = $props(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Set context during component initialization - this happens before any child components | ||||||||||||||||||||||||||
| setPage(svelteKitParameters?.stores?.page); | ||||||||||||||||||||||||||
| setNavigating(svelteKitParameters?.stores?.navigating); | ||||||||||||||||||||||||||
| setUpdated(svelteKitParameters?.stores?.updated); | ||||||||||||||||||||||||||
| setAfterNavigateArgument(svelteKitParameters?.navigation?.afterNavigate); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const normalizeHrefConfig = (hrefConfig) => { | ||||||||||||||||||||||||||
| if (typeof hrefConfig === 'function') { | ||||||||||||||||||||||||||
| return { callback: hrefConfig, asRegex: false }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return hrefConfig; | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
Comment on lines
+18
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Normalize href config defensively If a non‑object slips in, return a safe default. Apply this diff: - const normalizeHrefConfig = (hrefConfig) => {
- if (typeof hrefConfig === 'function') {
- return { callback: hrefConfig, asRegex: false };
- }
- return hrefConfig;
- };
+ const normalizeHrefConfig = (hrefConfig) => {
+ if (typeof hrefConfig === 'function') return { callback: hrefConfig, asRegex: false };
+ return hrefConfig && typeof hrefConfig === 'object'
+ ? hrefConfig
+ : { callback: undefined, asRegex: false };
+ };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| onMount(() => { | ||||||||||||||||||||||||||
| const globalClickListener = (e) => { | ||||||||||||||||||||||||||
| // we add a global click event listener and we check if there's a link in the composedPath | ||||||||||||||||||||||||||
| const path = e.composedPath(); | ||||||||||||||||||||||||||
| const element = path.findLast((el) => el instanceof HTMLElement && el.tagName === 'A'); | ||||||||||||||||||||||||||
| if (element && element instanceof HTMLAnchorElement) { | ||||||||||||||||||||||||||
| // if the element is an a-tag we get the href of the element | ||||||||||||||||||||||||||
| // and compare it to the hrefs-parameter set by the user | ||||||||||||||||||||||||||
| const to = element.getAttribute('href'); | ||||||||||||||||||||||||||
| if (!to) { | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||
| const defaultActionCallback = () => action('navigate')(to, e); | ||||||||||||||||||||||||||
| if (!svelteKitParameters.hrefs) { | ||||||||||||||||||||||||||
| defaultActionCallback(); | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let callDefaultCallback = true; | ||||||||||||||||||||||||||
| // we loop over every href set by the user and check if the href matches | ||||||||||||||||||||||||||
| // if it does we call the callback provided by the user and disable the default callback | ||||||||||||||||||||||||||
| Object.entries(svelteKitParameters.hrefs).forEach(([href, hrefConfig]) => { | ||||||||||||||||||||||||||
| const { callback, asRegex } = normalizeHrefConfig(hrefConfig); | ||||||||||||||||||||||||||
| const isMatch = asRegex ? new RegExp(href).test(to) : to === href; | ||||||||||||||||||||||||||
| if (isMatch) { | ||||||||||||||||||||||||||
| callDefaultCallback = false; | ||||||||||||||||||||||||||
| callback?.(to, e); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
JReinhold marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
| if (callDefaultCallback) { | ||||||||||||||||||||||||||
| defaultActionCallback(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
JReinhold marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Function that create and add listeners for the event that are emitted by the mocked | ||||||||||||||||||||||||||
| * functions. The event name is based on the function name | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * Eg. storybook:goto, storybook:invalidateAll | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * @param baseModule The base module where the function lives (navigation|forms) | ||||||||||||||||||||||||||
| * @param functions The list of functions in that module that emit events | ||||||||||||||||||||||||||
| * @param {boolean} [defaultToAction] The list of functions in that module that emit events | ||||||||||||||||||||||||||
| * @returns A function to remove all the listener added | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| function createListeners(baseModule, functions, defaultToAction) { | ||||||||||||||||||||||||||
| // the array of every added listener, we can use this in the return function | ||||||||||||||||||||||||||
| // to clean them | ||||||||||||||||||||||||||
| const toRemove = []; | ||||||||||||||||||||||||||
| functions.forEach((func) => { | ||||||||||||||||||||||||||
| // we loop over every function and check if the user actually passed | ||||||||||||||||||||||||||
| // a function in sveltekit_experimental[baseModule][func] eg. sveltekit_experimental.navigation.goto | ||||||||||||||||||||||||||
| const hasFunction = | ||||||||||||||||||||||||||
| svelteKitParameters[baseModule]?.[func] && | ||||||||||||||||||||||||||
| svelteKitParameters[baseModule][func] instanceof Function; | ||||||||||||||||||||||||||
| // if we default to an action we still add the listener (this will be the case for goto, invalidate, invalidateAll) | ||||||||||||||||||||||||||
| if (hasFunction || defaultToAction) { | ||||||||||||||||||||||||||
| // we create the listener that will just get the detail array from the custom element | ||||||||||||||||||||||||||
| // and call the user provided function spreading this args in...this will basically call | ||||||||||||||||||||||||||
| // the function that the user provide with the same arguments the function is invoked to | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // eg. if it calls goto("/my-route") inside the component the function sveltekit_experimental.navigation.goto | ||||||||||||||||||||||||||
| // it provided to storybook will be called with "/my-route" | ||||||||||||||||||||||||||
| const listener = ({ detail = [] }) => { | ||||||||||||||||||||||||||
| const args = Array.isArray(detail) ? detail : []; | ||||||||||||||||||||||||||
| // if it has a function in the parameters we call that function | ||||||||||||||||||||||||||
| // otherwise we invoke the action | ||||||||||||||||||||||||||
| const fnToCall = hasFunction | ||||||||||||||||||||||||||
| ? svelteKitParameters[baseModule][func] | ||||||||||||||||||||||||||
| : action(func); | ||||||||||||||||||||||||||
| fnToCall(...args); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| const eventType = `storybook:${func}`; | ||||||||||||||||||||||||||
| toRemove.push({ eventType, listener }); | ||||||||||||||||||||||||||
| // add the listener to window | ||||||||||||||||||||||||||
| window.addEventListener(eventType, listener); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||
| // loop over every listener added and remove them | ||||||||||||||||||||||||||
| toRemove.forEach(({ eventType, listener }) => { | ||||||||||||||||||||||||||
| window.removeEventListener(eventType, listener); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const removeNavigationListeners = createListeners( | ||||||||||||||||||||||||||
| 'navigation', | ||||||||||||||||||||||||||
| ['goto', 'invalidate', 'invalidateAll', 'pushState', 'replaceState'], | ||||||||||||||||||||||||||
| true | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| const removeFormsListeners = createListeners('forms', ['enhance']); | ||||||||||||||||||||||||||
| window.addEventListener('click', globalClickListener); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||
| window.removeEventListener('click', globalClickListener); | ||||||||||||||||||||||||||
| removeNavigationListeners(); | ||||||||||||||||||||||||||
| removeFormsListeners(); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {@render children()} | ||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.