From 6b427b602609aea94f93b4459dc39b41beb79737 Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 20 Sep 2025 14:41:23 +0100 Subject: [PATCH 1/4] sveltekit: add context provider component for mocking sveltekit contexts --- code/frameworks/sveltekit/build-config.ts | 3 + code/frameworks/sveltekit/package.json | 2 + code/frameworks/sveltekit/src/preview.ts | 138 ++---------------- .../sveltekit/static/MockProvider.svelte | 130 +++++++++++++++++ code/frameworks/sveltekit/tsconfig.json | 1 - 5 files changed, 145 insertions(+), 129 deletions(-) create mode 100644 code/frameworks/sveltekit/static/MockProvider.svelte diff --git a/code/frameworks/sveltekit/build-config.ts b/code/frameworks/sveltekit/build-config.ts index 79b871767e32..e0bb8de19c7d 100644 --- a/code/frameworks/sveltekit/build-config.ts +++ b/code/frameworks/sveltekit/build-config.ts @@ -45,6 +45,9 @@ const config: BuildEntries = { }, ], }, + extraOutputs: { + './internal/MockProvider.svelte': './static/MockProvider.svelte', + }, }; export default config; diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index 4a837b931409..94330690eef6 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -32,6 +32,7 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./internal/MockProvider.svelte": "./static/MockProvider.svelte", "./internal/mocks/app/forms": "./dist/mocks/app/forms.js", "./internal/mocks/app/navigation": "./dist/mocks/app/navigation.js", "./internal/mocks/app/stores": "./dist/mocks/app/stores.js", @@ -46,6 +47,7 @@ }, "files": [ "dist/**/*", + "static/**/*", "template/**/*", "README.md", "*.js", diff --git a/code/frameworks/sveltekit/src/preview.ts b/code/frameworks/sveltekit/src/preview.ts index 64e301550af1..ee5384e10d3b 100644 --- a/code/frameworks/sveltekit/src/preview.ts +++ b/code/frameworks/sveltekit/src/preview.ts @@ -1,138 +1,20 @@ import type { Decorator } from '@storybook/svelte'; +import ContextProvider 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(); - }; - }); + const story = Story(); - return Story(); + return { + Component: ContextProvider, + props: { + Story: story, + svelteKitParameters, + }, + }; }; export const decorators: Decorator[] = [svelteKitMocksDecorator]; diff --git a/code/frameworks/sveltekit/static/MockProvider.svelte b/code/frameworks/sveltekit/static/MockProvider.svelte new file mode 100644 index 000000000000..f6646e7779ab --- /dev/null +++ b/code/frameworks/sveltekit/static/MockProvider.svelte @@ -0,0 +1,130 @@ + + + \ No newline at end of file diff --git a/code/frameworks/sveltekit/tsconfig.json b/code/frameworks/sveltekit/tsconfig.json index c749496d9a6e..39029c2ce294 100644 --- a/code/frameworks/sveltekit/tsconfig.json +++ b/code/frameworks/sveltekit/tsconfig.json @@ -4,7 +4,6 @@ "paths": { "storybook/internal/*": ["../../lib/cli/core/*"] }, - "rootDir": "./src" }, "extends": "../../tsconfig.json", "include": ["src/**/*"] From 6a41bf039554b3141fa3e616ad649b7284c36106 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 23 Sep 2025 15:58:36 +0200 Subject: [PATCH 2/4] enable experimental async and remote functions in all Svelte sandboxes --- scripts/tasks/sandbox-parts.ts | 55 +++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index 6d354b340fc5..c9312ae02b1a 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -17,7 +17,11 @@ import { SupportedLanguage } from '../../code/core/src/cli/project_types'; import { JsPackageManagerFactory } from '../../code/core/src/common/js-package-manager'; import storybookPackages from '../../code/core/src/common/versions'; import type { ConfigFile } from '../../code/core/src/csf-tools'; -import { formatConfig, writeConfig } from '../../code/core/src/csf-tools'; +import { + readConfig as csfReadConfig, + formatConfig, + writeConfig, +} from '../../code/core/src/csf-tools'; import type { TemplateKey } from '../../code/lib/cli-storybook/src/sandbox-templates'; import type { PassedOptionValues, Task, TemplateDetails } from '../task'; import { executeCLIStep, steps } from '../utils/cli-step'; @@ -170,19 +174,24 @@ export const init: Task['run'] = async ( const cwd = sandboxDir; let extra = {}; - if (template.expected.renderer === '@storybook/html') { - extra = { type: 'html' }; - } else if (template.expected.renderer === '@storybook/server') { - extra = { type: 'server' }; - } else if (template.expected.framework === '@storybook/react-native-web-vite') { - extra = { type: 'react_native_web' }; + + switch (template.expected.renderer) { + case '@storybook/html': + extra = { type: 'html' }; + break; + case '@storybook/server': + extra = { type: 'server' }; + break; + case '@storybook/svelte': + await prepareSvelteSandbox(cwd); + break; } switch (template.expected.framework) { case '@storybook/react-native-web-vite': + extra = { type: 'react_native_web' }; await prepareReactNativeWebSandbox(cwd); break; - default: } await executeCLIStep(steps.init, { @@ -878,6 +887,36 @@ async function prepareReactNativeWebSandbox(cwd: string) { } } +async function prepareSvelteSandbox(cwd: string) { + const svelteConfigJsPath = join(cwd, 'svelte.config.js'); + const svelteConfigTsPath = join(cwd, 'svelte.config.ts'); + + // Check which config file exists + const configPath = (await pathExists(svelteConfigTsPath)) + ? svelteConfigTsPath + : (await pathExists(svelteConfigJsPath)) + ? svelteConfigJsPath + : null; + + if (!configPath) { + throw new Error( + `No svelte.config.js or svelte.config.ts found in sandbox: ${cwd}, cannot modify config.` + ); + } + + const svelteConfig = await csfReadConfig(configPath); + + // Enable async components + // see https://svelte.dev/docs/svelte/await-expressions + svelteConfig.setFieldValue(['compilerOptions', 'experimental', 'async'], true); + + // Enable remote functions + // see https://svelte.dev/docs/kit/remote-functions + svelteConfig.setFieldValue(['kit', 'experimental', 'remoteFunctions'], true); + + await writeConfig(svelteConfig); +} + async function prepareAngularSandbox(cwd: string, templateName: string) { const angularJson = await readJson(join(cwd, 'angular.json')); From f0b7aadab18f4acf28f54ec78ebe3d0dcc14229c Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 23 Sep 2025 16:03:41 +0200 Subject: [PATCH 3/4] rely on automatic decorator children --- code/frameworks/sveltekit/src/preview.ts | 7 ++----- code/frameworks/sveltekit/static/MockProvider.svelte | 5 ++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/code/frameworks/sveltekit/src/preview.ts b/code/frameworks/sveltekit/src/preview.ts index ee5384e10d3b..83b6d452feb6 100644 --- a/code/frameworks/sveltekit/src/preview.ts +++ b/code/frameworks/sveltekit/src/preview.ts @@ -1,17 +1,14 @@ import type { Decorator } from '@storybook/svelte'; -import ContextProvider from '@storybook/sveltekit/internal/MockProvider.svelte'; +import MockProvider from '@storybook/sveltekit/internal/MockProvider.svelte'; import type { SvelteKitParameters } from './types'; const svelteKitMocksDecorator: Decorator = (Story, ctx) => { const svelteKitParameters: SvelteKitParameters = ctx.parameters?.sveltekit_experimental ?? {}; - const story = Story(); - return { - Component: ContextProvider, + Component: MockProvider, props: { - Story: story, svelteKitParameters, }, }; diff --git a/code/frameworks/sveltekit/static/MockProvider.svelte b/code/frameworks/sveltekit/static/MockProvider.svelte index f6646e7779ab..0b6e5a8b70fe 100644 --- a/code/frameworks/sveltekit/static/MockProvider.svelte +++ b/code/frameworks/sveltekit/static/MockProvider.svelte @@ -6,9 +6,8 @@ import { setNavigating, setPage, setUpdated } from '../src/mocks/app/stores'; - const{ Story, svelteKitParameters = {} } = $props(); + const{ svelteKitParameters = {}, children } = $props(); - const { Component } = Story; // Set context during component initialization - this happens before any child components setPage(svelteKitParameters?.stores?.page); @@ -127,4 +126,4 @@ }); - \ No newline at end of file +{@render children()} From 41b0ff0da44ece58f2de2b446d4d192122b31ccc Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 23 Sep 2025 21:44:18 +0200 Subject: [PATCH 4/4] change imports from src to entrypoint imports --- code/frameworks/sveltekit/package.json | 3 +-- code/frameworks/sveltekit/static/MockProvider.svelte | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index 8cb3655552d4..19df3ae3e7ca 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -51,8 +51,7 @@ "template/**/*", "README.md", "*.js", - "*.d.ts", - "src/mocks/**/*" + "*.d.ts" ], "scripts": { "check": "jiti ../../../scripts/check/check-package.ts", diff --git a/code/frameworks/sveltekit/static/MockProvider.svelte b/code/frameworks/sveltekit/static/MockProvider.svelte index 0b6e5a8b70fe..79d19e982108 100644 --- a/code/frameworks/sveltekit/static/MockProvider.svelte +++ b/code/frameworks/sveltekit/static/MockProvider.svelte @@ -2,8 +2,8 @@ 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'; + import { setAfterNavigateArgument } from '@storybook/sveltekit/internal/mocks/app/navigation'; + import { setNavigating, setPage, setUpdated } from '@storybook/sveltekit/internal/mocks/app/stores'; const{ svelteKitParameters = {}, children } = $props();