diff --git a/code/addons/test/src/components/TestProviderRender.tsx b/code/addons/test/src/components/TestProviderRender.tsx
index dd765c3e2d5b..16197a7c7ced 100644
--- a/code/addons/test/src/components/TestProviderRender.tsx
+++ b/code/addons/test/src/components/TestProviderRender.tsx
@@ -129,7 +129,7 @@ export const TestProviderRender: FC<
padding="small"
active={state.watching}
onClick={() => api.setTestProviderWatchMode(state.id, !state.watching)}
- disabled={state.crashed || state.running || isEditing}
+ disabled={state.running || isEditing}
>
@@ -152,7 +152,7 @@ export const TestProviderRender: FC<
variant="ghost"
padding="small"
onClick={() => api.runTestProvider(state.id, { entryId })}
- disabled={state.crashed || state.running || isEditing}
+ disabled={state.running || isEditing}
>
diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json
index 5af8fa963c09..f97766fbc348 100644
--- a/code/builders/builder-vite/package.json
+++ b/code/builders/builder-vite/package.json
@@ -53,7 +53,9 @@
"es-module-lexer": "^1.5.0",
"find-cache-dir": "^3.0.0",
"glob": "^10.0.0",
+ "knitwork": "^1.1.0",
"magic-string": "^0.30.0",
+ "pathe": "^1.1.2",
"polka": "^1.0.0-next.28",
"sirv": "^2.0.4",
"slash": "^5.0.0",
diff --git a/code/builders/builder-vite/src/codegen-importfn-script.test.ts b/code/builders/builder-vite/src/codegen-importfn-script.test.ts
new file mode 100644
index 000000000000..4d3ce5bcb79a
--- /dev/null
+++ b/code/builders/builder-vite/src/codegen-importfn-script.test.ts
@@ -0,0 +1,56 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { toImportFn } from './codegen-importfn-script';
+
+describe('toImportFn', () => {
+ it('should correctly map story paths to import functions for absolute paths on Linux', async () => {
+ const root = '/absolute/path';
+ const stories = ['/absolute/path/to/story1.js', '/absolute/path/to/story2.js'];
+
+ const result = await toImportFn(root, stories);
+
+ expect(result).toMatchInlineSnapshot(`
+ "const importers = {
+ "./to/story1.js": () => import("/absolute/path/to/story1.js"),
+ "./to/story2.js": () => import("/absolute/path/to/story2.js")
+ };
+
+ export async function importFn(path) {
+ return await importers[path]();
+ }"
+ `);
+ });
+
+ it('should correctly map story paths to import functions for absolute paths on Windows', async () => {
+ const root = 'C:\\absolute\\path';
+ const stories = ['C:\\absolute\\path\\to\\story1.js', 'C:\\absolute\\path\\to\\story2.js'];
+
+ const result = await toImportFn(root, stories);
+
+ expect(result).toMatchInlineSnapshot(`
+ "const importers = {
+ "./to/story1.js": () => import("C:/absolute/path/to/story1.js"),
+ "./to/story2.js": () => import("C:/absolute/path/to/story2.js")
+ };
+
+ export async function importFn(path) {
+ return await importers[path]();
+ }"
+ `);
+ });
+
+ it('should handle an empty array of stories', async () => {
+ const root = '/absolute/path';
+ const stories: string[] = [];
+
+ const result = await toImportFn(root, stories);
+
+ expect(result).toMatchInlineSnapshot(`
+ "const importers = {};
+
+ export async function importFn(path) {
+ return await importers[path]();
+ }"
+ `);
+ });
+});
diff --git a/code/builders/builder-vite/src/codegen-importfn-script.ts b/code/builders/builder-vite/src/codegen-importfn-script.ts
index 0853b0549628..4b118ad9cc51 100644
--- a/code/builders/builder-vite/src/codegen-importfn-script.ts
+++ b/code/builders/builder-vite/src/codegen-importfn-script.ts
@@ -1,7 +1,9 @@
-import { relative } from 'node:path';
-
import type { Options } from 'storybook/internal/types';
+import { genDynamicImport, genImport, genObjectFromRawEntries } from 'knitwork';
+import { normalize, relative } from 'pathe';
+import { dedent } from 'ts-dedent';
+
import { listStories } from './list-stories';
/**
@@ -21,34 +23,33 @@ function toImportPath(relativePath: string) {
/**
* This function takes an array of stories and creates a mapping between the stories' relative paths
* to the working directory and their dynamic imports. The import is done in an asynchronous
- * function to delay loading. It then creates a function, `importFn(path)`, which resolves a path to
- * an import function and this is called by Storybook to fetch a story dynamically when needed.
+ * function to delay loading and to allow Vite to split the code into smaller chunks. It then
+ * creates a function, `importFn(path)`, which resolves a path to an import function and this is
+ * called by Storybook to fetch a story dynamically when needed.
*
* @param stories An array of absolute story paths.
*/
-async function toImportFn(stories: string[]) {
- const { normalizePath } = await import('vite');
+export async function toImportFn(root: string, stories: string[]) {
const objectEntries = stories.map((file) => {
- const relativePath = normalizePath(relative(process.cwd(), file));
+ const relativePath = relative(root, file);
- return ` '${toImportPath(relativePath)}': async () => import('/@fs/${file}')`;
+ return [toImportPath(relativePath), genDynamicImport(normalize(file))] as [string, string];
});
- return `
- const importers = {
- ${objectEntries.join(',\n')}
- };
+ return dedent`
+ const importers = ${genObjectFromRawEntries(objectEntries)};
export async function importFn(path) {
- return importers[path]();
+ return await importers[path]();
}
`;
}
-export async function generateImportFnScriptCode(options: Options) {
+export async function generateImportFnScriptCode(options: Options): Promise {
// First we need to get an array of stories and their absolute paths.
const stories = await listStories(options);
// We can then call toImportFn to create a function that can be used to load each story dynamically.
- return (await toImportFn(stories)).trim();
+ // eslint-disable-next-line @typescript-eslint/return-await
+ return await toImportFn(options.projectRoot || process.cwd(), stories);
}
diff --git a/code/builders/builder-vite/src/vite-config.ts b/code/builders/builder-vite/src/vite-config.ts
index 8983dfc137b3..de8f12ed8822 100644
--- a/code/builders/builder-vite/src/vite-config.ts
+++ b/code/builders/builder-vite/src/vite-config.ts
@@ -53,18 +53,18 @@ export async function commonConfig(
const { viteConfigPath } = await getBuilderOptions(options);
- const projectRoot = resolve(options.configDir, '..');
+ options.projectRoot = options.projectRoot || resolve(options.configDir, '..');
// I destructure away the `build` property from the user's config object
// I do this because I can contain config that breaks storybook, such as we had in a lit project.
// If the user needs to configure the `build` they need to do so in the viteFinal function in main.js.
const { config: { build: buildProperty = undefined, ...userConfig } = {} } =
- (await loadConfigFromFile(configEnv, viteConfigPath, projectRoot)) ?? {};
+ (await loadConfigFromFile(configEnv, viteConfigPath, options.projectRoot)) ?? {};
const sbConfig: InlineConfig = {
configFile: false,
cacheDir: resolvePathInStorybookCache('sb-vite', options.cacheKey),
- root: projectRoot,
+ root: options.projectRoot,
// Allow storybook deployed as subfolder. See https://github.com/storybookjs/builder-vite/issues/238
base: './',
plugins: await pluginConfig(options),
diff --git a/code/core/src/core-server/utils/whats-new.ts b/code/core/src/core-server/utils/whats-new.ts
index cb523f78318e..71c71984cb12 100644
--- a/code/core/src/core-server/utils/whats-new.ts
+++ b/code/core/src/core-server/utils/whats-new.ts
@@ -1,7 +1,8 @@
+/* eslint-disable no-underscore-dangle */
import { writeFile } from 'node:fs/promises';
import type { Channel } from '@storybook/core/channels';
-import { findConfigFile } from '@storybook/core/common';
+import { findConfigFile, loadMainConfig } from '@storybook/core/common';
import { telemetry } from '@storybook/core/telemetry';
import type { CoreConfig, Options } from '@storybook/core/types';
@@ -58,15 +59,9 @@ export function initializeWhatsNew(
throw response;
})) as WhatsNewResponse;
- const configFileName = findConfigFile('main', options.configDir);
- if (!configFileName) {
- throw new Error(`unable to find storybook main file in ${options.configDir}`);
- }
- const main = await readConfig(configFileName);
- const disableWhatsNewNotifications = main.getFieldValue([
- 'core',
- 'disableWhatsNewNotifications',
- ]);
+ const main = await loadMainConfig({ configDir: options.configDir, noCache: true });
+ const disableWhatsNewNotifications =
+ (main.core as CoreConfig)?.disableWhatsNewNotifications === true;
const cache: WhatsNewCache = (await options.cache.get(WHATS_NEW_CACHE)) ?? {};
const data = {
@@ -91,8 +86,14 @@ export function initializeWhatsNew(
const isTelemetryEnabled = coreOptions.disableTelemetry !== true;
try {
const mainPath = findConfigFile('main', options.configDir);
- invariant(mainPath, `unable to find storybook main file in ${options.configDir}`);
+ invariant(mainPath, `unable to find Storybook main file in ${options.configDir}`);
const main = await readConfig(mainPath);
+ if (!main._exportsObject) {
+ // eslint-disable-next-line local-rules/no-uncategorized-errors
+ throw new Error(
+ `Unable to parse Storybook main file while trying to read 'core' property`
+ );
+ }
main.setFieldValue(['core', 'disableWhatsNewNotifications'], disableWhatsNewNotifications);
await writeFile(mainPath, printConfig(main).code);
if (isTelemetryEnabled) {
diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts
index 9b2b1fb1dcfb..294355c73bc6 100644
--- a/code/core/src/manager-api/modules/experimental_testmodule.ts
+++ b/code/core/src/manager-api/modules/experimental_testmodule.ts
@@ -85,7 +85,7 @@ export const init: ModuleFn = ({ store, fullAPI }) => {
runTestProvider(id, options) {
const index = store.getState().index;
invariant(index, 'The index is currently unavailable');
- api.updateTestProviderState(id, { running: true });
+ api.updateTestProviderState(id, { running: true, failed: false, crashed: false });
const provider = store.getState().testProviders[id];
@@ -152,6 +152,7 @@ export const init: ModuleFn = ({ store, fullAPI }) => {
...config,
...initialTestProviderState,
...(state?.testProviders?.[id] || {}),
+ running: false,
} as TestProviders[0],
]
)
diff --git a/code/core/src/manager/components/sidebar/TestingModule.tsx b/code/core/src/manager/components/sidebar/TestingModule.tsx
index b7d5536ce203..b66512007c08 100644
--- a/code/core/src/manager/components/sidebar/TestingModule.tsx
+++ b/code/core/src/manager/components/sidebar/TestingModule.tsx
@@ -263,7 +263,7 @@ export const TestingModule = ({
onClick={(e: SyntheticEvent) => {
e.stopPropagation();
testProviders
- .filter((state) => !state.crashed && !state.running && state.runnable)
+ .filter((state) => !state.running && state.runnable)
.forEach(({ id }) => api.runTestProvider(id));
}}
disabled={isRunning}
diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts
index 66e4a7edce33..47f5aec6412b 100644
--- a/code/core/src/types/modules/addons.ts
+++ b/code/core/src/types/modules/addons.ts
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { FC, PropsWithChildren, ReactElement, ReactNode } from 'react';
-import type { ListItem } from '../../components';
import type { TestProviderConfig, TestingModuleProgressReportProgress } from '../../core-events';
import type { RenderData as RouterData } from '../../router/types';
import type { ThemeVars } from '../../theming/types';
diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts
index dbdabe22122c..07372bc308dd 100644
--- a/code/core/src/types/modules/core-common.ts
+++ b/code/core/src/types/modules/core-common.ts
@@ -194,6 +194,7 @@ export interface BuilderOptions {
ignorePreview?: boolean;
cache?: FileSystemCache;
configDir: string;
+ projectRoot?: string;
docsMode?: boolean;
features?: StorybookConfigRaw['features'];
versionCheck?: VersionCheck;
diff --git a/code/sandbox/nuxt-vite-default-ts/project.json b/code/sandbox/nuxt-vite-default-ts/project.json
new file mode 100644
index 000000000000..71bb3820c382
--- /dev/null
+++ b/code/sandbox/nuxt-vite-default-ts/project.json
@@ -0,0 +1,21 @@
+{
+ "name": "nuxt-vite/default-ts",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "implicitDependencies": [
+ "storybook",
+ "core",
+ "addon-essentials",
+ "addon-interactions",
+ "addon-links",
+ "addon-onboarding",
+ "blocks",
+ "vue3-vite"
+ ],
+ "targets": {
+ "sandbox": {},
+ "sb:dev": {},
+ "sb:build": {}
+ },
+ "tags": ["ci:normal", "ci:merged", "ci:daily"]
+}
diff --git a/code/yarn.lock b/code/yarn.lock
index 42fd23469029..1bada105b2f1 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -6172,7 +6172,9 @@ __metadata:
es-module-lexer: "npm:^1.5.0"
find-cache-dir: "npm:^3.0.0"
glob: "npm:^10.0.0"
+ knitwork: "npm:^1.1.0"
magic-string: "npm:^0.30.0"
+ pathe: "npm:^1.1.2"
polka: "npm:^1.0.0-next.28"
sirv: "npm:^2.0.4"
slash: "npm:^5.0.0"
@@ -19330,6 +19332,13 @@ __metadata:
languageName: node
linkType: hard
+"knitwork@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "knitwork@npm:1.1.0"
+ checksum: 10c0/e23c679d4ded01890ab2669ccde2e85e4d7e6ba327b1395ff657f8067c7d73dc134fc8cd8188c653de4a687be7fa9c130bd36c3e2f76d8685e8b97ff8b30779c
+ languageName: node
+ linkType: hard
+
"language-subtag-registry@npm:^0.3.20":
version: 0.3.22
resolution: "language-subtag-registry@npm:0.3.22"