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
161 changes: 160 additions & 1 deletion code/builders/builder-vite/src/vite-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { describe, expect, it, vi } from 'vitest';
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';

import { afterEach, describe, expect, it, vi } from 'vitest';

import { Channel } from 'storybook/internal/channels';
import type { Options, Presets } from 'storybook/internal/types';
Expand Down Expand Up @@ -36,6 +40,47 @@ const dummyOptions: Options = {
presetsList: [],
};

const createOptions = ({
configDir = '',
staticDirs = [],
}: {
configDir?: string;
staticDirs?: unknown[];
} = {}) =>
({
...dummyOptions,
configDir,
presets: {
apply: async (key: string) =>
(
({
framework: {
name: '',
},
addons: [],
core: {
builder: {},
},
options: {},
staticDirs,
}) as Record<string, unknown>
)[key],
} as Presets,
}) satisfies Options;

const createTempProject = () => {
const projectRoot = mkdtempSync(join(tmpdir(), 'storybook-builder-vite-'));
const configDir = join(projectRoot, '.storybook');

mkdirSync(configDir, { recursive: true });

return { projectRoot, configDir };
};

afterEach(() => {
loadConfigFromFileMock.mockReset();
});

describe('commonConfig', () => {
it('should set configFile to false and include plugins', async () => {
loadConfigFromFileMock.mockReturnValueOnce(
Expand All @@ -49,6 +94,120 @@ describe('commonConfig', () => {
expect(config.configFile).toBe(false);
expect(config.plugins).toBeDefined();
});

it('disables Vite publicDir for builds when staticDirs already includes the public directory', async () => {
const { projectRoot, configDir } = createTempProject();
const publicDir = join(projectRoot, 'public');
mkdirSync(publicDir, { recursive: true });

loadConfigFromFileMock.mockReturnValueOnce(
Promise.resolve({
config: {},
path: '',
dependencies: [],
})
);

try {
const config = await commonConfig(
createOptions({
configDir,
staticDirs: [{ from: '../public', to: '/foo' }],
}),
'build'
);

expect(config.publicDir).toBe(false);
} finally {
rmSync(projectRoot, { recursive: true, force: true });
}
});

it('keeps Vite publicDir enabled during development even when staticDirs includes the public directory', async () => {
const { projectRoot, configDir } = createTempProject();
const publicDir = join(projectRoot, 'public');
mkdirSync(publicDir, { recursive: true });

loadConfigFromFileMock.mockReturnValueOnce(
Promise.resolve({
config: {},
path: '',
dependencies: [],
})
);

try {
const config = await commonConfig(
createOptions({
configDir,
staticDirs: [{ from: '../public', to: '/foo' }],
}),
'development'
);

expect(config.publicDir).toBeUndefined();
} finally {
rmSync(projectRoot, { recursive: true, force: true });
}
});

it('disables a custom Vite publicDir when staticDirs already includes the same directory', async () => {
const { projectRoot, configDir } = createTempProject();
const assetsDir = join(projectRoot, 'assets');
mkdirSync(assetsDir, { recursive: true });

loadConfigFromFileMock.mockReturnValueOnce(
Promise.resolve({
config: { publicDir: 'assets' },
path: '',
dependencies: [],
})
);

try {
const config = await commonConfig(
createOptions({
configDir,
staticDirs: [{ from: '../assets', to: '/foo' }],
}),
'build'
);

expect(config.publicDir).toBe(false);
} finally {
rmSync(projectRoot, { recursive: true, force: true });
}
});

it('keeps Vite publicDir enabled for builds when staticDirs points somewhere else', async () => {
const { projectRoot, configDir } = createTempProject();
const publicDir = join(projectRoot, 'public');
const assetsDir = join(projectRoot, 'assets');
mkdirSync(publicDir, { recursive: true });
mkdirSync(assetsDir, { recursive: true });

loadConfigFromFileMock.mockReturnValueOnce(
Promise.resolve({
config: {},
path: '',
dependencies: [],
})
);

try {
const config = await commonConfig(
createOptions({
configDir,
staticDirs: [{ from: '../assets', to: '/foo' }],
}),
'build'
);

expect(config.publicDir).toBeUndefined();
} finally {
rmSync(projectRoot, { recursive: true, force: true });
}
});
});

describe('storybookConfigPlugin', () => {
Expand Down
36 changes: 36 additions & 0 deletions code/builders/builder-vite/src/vite-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { resolve } from 'node:path';

import { getBuilderOptions, resolvePathInStorybookCache } from 'storybook/internal/common';
import { mapStaticDir } from 'storybook/internal/core-server';
import type { Options } from 'storybook/internal/types';

import type {
Expand Down Expand Up @@ -34,6 +35,36 @@ const configEnvBuild: ConfigEnv = {
isSsrBuild: false,
};

const resolvePublicDir = (projectRoot: string, publicDir: ViteConfig['publicDir']) => {
if (publicDir === false) {
return undefined;
}

return resolve(projectRoot, publicDir ?? 'public');
};

const shouldDisableVitePublicDir = async (
options: Options,
projectRoot: string,
publicDir: ViteConfig['publicDir']
) => {
const resolvedPublicDir = resolvePublicDir(projectRoot, publicDir);

if (!resolvedPublicDir) {
return false;
}

const staticDirs = (await options.presets.apply('staticDirs', [], options)) ?? [];

return staticDirs.some((staticDir) => {
try {
return mapStaticDir(staticDir, options.configDir).staticPath === resolvedPublicDir;
} catch {
return false;
}
});
};

// Vite config that is common to development and production mode
export async function commonConfig(
options: Options,
Expand All @@ -52,6 +83,10 @@ export async function commonConfig(
const { config: { build: buildProperty = undefined, ...userConfig } = {} } =
(await loadConfigFromFile(configEnv, viteConfigPath, projectRoot)) ?? {};

const disableVitePublicDir =
_type === 'build' &&
(await shouldDisableVitePublicDir(options, projectRoot, userConfig.publicDir));

// Storybook's Vite config is assembled from self-contained plugins.
// The config plugin handles base settings (root, cacheDir, resolve conditions, etc.),
// while other plugins handle entry points, docgen, and runtime globals.
Expand All @@ -67,6 +102,7 @@ export async function commonConfig(
? { cacheDir: resolvePathInStorybookCache('sb-vite', options.cacheKey) }
: {}),
// Pass build.target option from user's vite config
...(disableVitePublicDir ? { publicDir: false } : {}),
build: {
target: buildProperty?.target,
},
Expand Down