diff --git a/code/addons/test/package.json b/code/addons/test/package.json index cd7a737260b7..9bcf07d466d2 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -109,6 +109,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "semver": "^7.6.3", + "sirv": "^2.0.4", "slash": "^5.0.0", "strip-ansi": "^7.1.0", "tinyglobby": "^0.2.10", diff --git a/code/addons/test/src/vitest-plugin/index.ts b/code/addons/test/src/vitest-plugin/index.ts index 111e53fd2a8f..18749f75b9b9 100644 --- a/code/addons/test/src/vitest-plugin/index.ts +++ b/code/addons/test/src/vitest-plugin/index.ts @@ -7,12 +7,13 @@ import { normalizeStories, validateConfigurationFiles, } from 'storybook/internal/common'; -import { StoryIndexGenerator } from 'storybook/internal/core-server'; +import { StoryIndexGenerator, mapStaticDir } from 'storybook/internal/core-server'; import { readConfig, vitestTransform } from 'storybook/internal/csf-tools'; import { MainFileMissingError } from 'storybook/internal/server-errors'; import type { DocsOptions, StoriesEntry } from 'storybook/internal/types'; import { join, resolve } from 'pathe'; +import sirv from 'sirv'; import { convertPathToPattern } from 'tinyglobby'; import type { InternalOptions, UserOptions } from './types'; @@ -63,6 +64,7 @@ export const storybookTest = (options?: UserOptions): Plugin => { let previewLevelTags: string[]; let storiesGlobs: StoriesEntry[]; let storiesFiles: string[]; + const statics: ReturnType[] = []; return { name: 'vite-plugin-storybook-test', @@ -111,6 +113,15 @@ export const storybookTest = (options?: UserOptions): Plugin => { const framework = await presets.apply('framework', undefined); const frameworkName = typeof framework === 'string' ? framework : framework.name; const storybookEnv = await presets.apply('env', {}); + const staticDirs = await presets.apply('staticDirs', []); + + for (const staticDir of staticDirs) { + try { + statics.push(mapStaticDir(staticDir, configDir)); + } catch (e) { + console.warn(e); + } + } // If we end up needing to know if we are running in browser mode later // const isRunningInBrowserMode = config.plugins.find((plugin: Plugin) => @@ -192,6 +203,18 @@ export const storybookTest = (options?: UserOptions): Plugin => { config.define.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = 'false'; } }, + configureServer(server) { + statics.map(({ staticPath, targetEndpoint }) => { + server.middlewares.use( + targetEndpoint, + sirv(staticPath, { + dev: true, + etag: true, + extensions: [], + }) + ); + }); + }, async transform(code, id) { if (process.env.VITEST !== 'true') { return code; diff --git a/code/core/src/core-server/index.ts b/code/core/src/core-server/index.ts index 3163e70875ea..e8898fb5767b 100644 --- a/code/core/src/core-server/index.ts +++ b/code/core/src/core-server/index.ts @@ -6,4 +6,5 @@ export * from './build-static'; export * from './build-dev'; export * from './withTelemetry'; export { default as build } from './standalone'; +export { mapStaticDir } from './utils/server-statics'; export { StoryIndexGenerator } from './utils/StoryIndexGenerator'; diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts index fe5f7db4f53a..34b852cbb7bc 100644 --- a/code/core/src/core-server/presets/common-preset.ts +++ b/code/core/src/core-server/presets/common-preset.ts @@ -67,39 +67,37 @@ export const favicon = async ( ? staticDirsValue.map((dir) => (typeof dir === 'string' ? dir : `${dir.from}:${dir.to}`)) : []; - if (statics && statics.length > 0) { - const lists = await Promise.all( - statics.map(async (dir) => { - const results = []; - const normalizedDir = - staticDirsValue && !isAbsolute(dir) - ? getDirectoryFromWorkingDir({ - configDir: options.configDir, - workingDir: process.cwd(), - directory: dir, - }) - : dir; - - const { staticPath, targetEndpoint } = await parseStaticDir(normalizedDir); - - if (targetEndpoint === '/') { - const url = 'favicon.svg'; - const path = join(staticPath, url); - if (existsSync(path)) { - results.push(path); - } + if (statics.length > 0) { + const lists = statics.map((dir) => { + const results = []; + const normalizedDir = + staticDirsValue && !isAbsolute(dir) + ? getDirectoryFromWorkingDir({ + configDir: options.configDir, + workingDir: process.cwd(), + directory: dir, + }) + : dir; + + const { staticPath, targetEndpoint } = parseStaticDir(normalizedDir); + + if (targetEndpoint === '/') { + const url = 'favicon.svg'; + const path = join(staticPath, url); + if (existsSync(path)) { + results.push(path); } - if (targetEndpoint === '/') { - const url = 'favicon.ico'; - const path = join(staticPath, url); - if (existsSync(path)) { - results.push(path); - } + } + if (targetEndpoint === '/') { + const url = 'favicon.ico'; + const path = join(staticPath, url); + if (existsSync(path)) { + results.push(path); } + } - return results; - }) - ); + return results; + }); const flatlist = lists.reduce((l1, l2) => l1.concat(l2), []); if (flatlist.length > 1) { diff --git a/code/core/src/core-server/utils/__tests__/server-statics.test.ts b/code/core/src/core-server/utils/__tests__/server-statics.test.ts index 0dfaea67f5df..145f855ff136 100644 --- a/code/core/src/core-server/utils/__tests__/server-statics.test.ts +++ b/code/core/src/core-server/utils/__tests__/server-statics.test.ts @@ -15,14 +15,14 @@ describe('parseStaticDir', () => { }); it('returns the static dir/path and default target', async () => { - await expect(parseStaticDir('public')).resolves.toEqual({ + expect(parseStaticDir('public')).toEqual({ staticDir: './public', staticPath: resolve('public'), targetDir: './', targetEndpoint: '/', }); - await expect(parseStaticDir('foo/bar')).resolves.toEqual({ + expect(parseStaticDir('foo/bar')).toEqual({ staticDir: './foo/bar', staticPath: resolve('foo/bar'), targetDir: './', @@ -31,14 +31,14 @@ describe('parseStaticDir', () => { }); it('returns the static dir/path and custom target', async () => { - await expect(parseStaticDir('public:/custom-endpoint')).resolves.toEqual({ + expect(parseStaticDir('public:/custom-endpoint')).toEqual({ staticDir: './public', staticPath: resolve('public'), targetDir: './custom-endpoint', targetEndpoint: '/custom-endpoint', }); - await expect(parseStaticDir('foo/bar:/custom-endpoint')).resolves.toEqual({ + expect(parseStaticDir('foo/bar:/custom-endpoint')).toEqual({ staticDir: './foo/bar', staticPath: resolve('foo/bar'), targetDir: './custom-endpoint', @@ -47,21 +47,21 @@ describe('parseStaticDir', () => { }); it('pins relative endpoint at root', async () => { - const normal = await parseStaticDir('public:relative-endpoint'); + const normal = parseStaticDir('public:relative-endpoint'); expect(normal.targetEndpoint).toBe('/relative-endpoint'); - const windows = await parseStaticDir('C:\\public:relative-endpoint'); + const windows = parseStaticDir('C:\\public:relative-endpoint'); expect(windows.targetEndpoint).toBe('/relative-endpoint'); }); it('checks that the path exists', async () => { existsSyncMock.mockReturnValueOnce(false); - await expect(parseStaticDir('nonexistent')).rejects.toThrow(resolve('nonexistent')); + expect(() => parseStaticDir('nonexistent')).toThrow(resolve('nonexistent')); }); skipWindows(() => { it('supports absolute file paths - posix', async () => { - await expect(parseStaticDir('/foo/bar')).resolves.toEqual({ + expect(parseStaticDir('/foo/bar')).toEqual({ staticDir: '/foo/bar', staticPath: '/foo/bar', targetDir: './', @@ -70,7 +70,7 @@ describe('parseStaticDir', () => { }); it('supports absolute file paths with custom endpoint - posix', async () => { - await expect(parseStaticDir('/foo/bar:/custom-endpoint')).resolves.toEqual({ + expect(parseStaticDir('/foo/bar:/custom-endpoint')).toEqual({ staticDir: '/foo/bar', staticPath: '/foo/bar', targetDir: './custom-endpoint', @@ -81,7 +81,7 @@ describe('parseStaticDir', () => { onlyWindows(() => { it('supports absolute file paths - windows', async () => { - await expect(parseStaticDir('C:\\foo\\bar')).resolves.toEqual({ + expect(parseStaticDir('C:\\foo\\bar')).toEqual({ staticDir: resolve('C:\\foo\\bar'), staticPath: resolve('C:\\foo\\bar'), targetDir: './', @@ -90,14 +90,14 @@ describe('parseStaticDir', () => { }); it('supports absolute file paths with custom endpoint - windows', async () => { - await expect(parseStaticDir('C:\\foo\\bar:/custom-endpoint')).resolves.toEqual({ + expect(parseStaticDir('C:\\foo\\bar:/custom-endpoint')).toEqual({ staticDir: expect.any(String), // can't test this properly on unix staticPath: resolve('C:\\foo\\bar'), targetDir: './custom-endpoint', targetEndpoint: '/custom-endpoint', }); - await expect(parseStaticDir('C:\\foo\\bar:\\custom-endpoint')).resolves.toEqual({ + expect(parseStaticDir('C:\\foo\\bar:\\custom-endpoint')).toEqual({ staticDir: expect.any(String), // can't test this properly on unix staticPath: resolve('C:\\foo\\bar'), targetDir: './custom-endpoint', diff --git a/code/core/src/core-server/utils/copy-all-static-files.ts b/code/core/src/core-server/utils/copy-all-static-files.ts index ba5ccac883c8..2518fa82338c 100644 --- a/code/core/src/core-server/utils/copy-all-static-files.ts +++ b/code/core/src/core-server/utils/copy-all-static-files.ts @@ -14,7 +14,7 @@ export async function copyAllStaticFiles(staticDirs: any[] | undefined, outputDi await Promise.all( staticDirs.map(async (dir) => { try { - const { staticDir, staticPath, targetDir } = await parseStaticDir(dir); + const { staticDir, staticPath, targetDir } = parseStaticDir(dir); const targetPath = join(outputDir, targetDir); // we copy prebuild static files from node_modules/@storybook/manager & preview @@ -54,7 +54,7 @@ export async function copyAllStaticFilesRelativeToMain( await acc; const staticDirAndTarget = typeof dir === 'string' ? dir : `${dir.from}:${dir.to}`; - const { staticPath: from, targetEndpoint: to } = await parseStaticDir( + const { staticPath: from, targetEndpoint: to } = parseStaticDir( getDirectoryFromWorkingDir({ configDir, workingDir, diff --git a/code/core/src/core-server/utils/server-statics.ts b/code/core/src/core-server/utils/server-statics.ts index 3e21b4a3ea58..470d14ceb153 100644 --- a/code/core/src/core-server/utils/server-statics.ts +++ b/code/core/src/core-server/utils/server-statics.ts @@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'; import { basename, isAbsolute, posix, resolve, sep, win32 } from 'node:path'; import { getDirectoryFromWorkingDir } from '@storybook/core/common'; -import type { Options } from '@storybook/core/types'; +import type { Options, StorybookConfigRaw } from '@storybook/core/types'; import { logger } from '@storybook/core/node-logger'; @@ -15,43 +15,31 @@ export async function useStatics(app: Polka.Polka, options: Options): Promise('favicon'); - await Promise.all( - staticDirs - .map((dir) => (typeof dir === 'string' ? dir : `${dir.from}:${dir.to}`)) - .map(async (dir) => { - try { - const normalizedDir = - staticDirs && !isAbsolute(dir) - ? getDirectoryFromWorkingDir({ - configDir: options.configDir, - workingDir: process.cwd(), - directory: dir, - }) - : dir; - const { staticDir, staticPath, targetEndpoint } = await parseStaticDir(normalizedDir); + staticDirs.map((dir) => { + try { + const { staticDir, staticPath, targetEndpoint } = mapStaticDir(dir, options.configDir); - // Don't log for the internal static dir - if (!targetEndpoint.startsWith('/sb-')) { - logger.info( - `=> Serving static files from ${picocolors.cyan(staticDir)} at ${picocolors.cyan(targetEndpoint)}` - ); - } + // Don't log for the internal static dir + if (!targetEndpoint.startsWith('/sb-')) { + logger.info( + `=> Serving static files from ${picocolors.cyan(staticDir)} at ${picocolors.cyan(targetEndpoint)}` + ); + } - app.use( - targetEndpoint, - sirv(staticPath, { - dev: true, - etag: true, - extensions: [], - }) - ); - } catch (e) { - if (e instanceof Error) { - logger.warn(e.message); - } - } - }) - ); + app.use( + targetEndpoint, + sirv(staticPath, { + dev: true, + etag: true, + extensions: [], + }) + ); + } catch (e) { + if (e instanceof Error) { + logger.warn(e.message); + } + } + }); app.get( `/${basename(faviconPath)}`, @@ -63,7 +51,7 @@ export async function useStatics(app: Polka.Polka, options: Options): Promise { +export const parseStaticDir = (arg: string) => { // Split on last index of ':', for Windows compatibility (e.g. 'C:\some\dir:\foo') const lastColonIndex = arg.lastIndexOf(':'); const isWindowsAbsolute = win32.isAbsolute(arg); @@ -90,3 +78,15 @@ export const parseStaticDir = async (arg: string) => { return { staticDir, staticPath, targetDir, targetEndpoint }; }; + +export const mapStaticDir = ( + staticDir: NonNullable[number], + configDir: string +) => { + const specifier = typeof staticDir === 'string' ? staticDir : `${staticDir.from}:${staticDir.to}`; + const normalizedDir = isAbsolute(specifier) + ? specifier + : getDirectoryFromWorkingDir({ configDir, workingDir: process.cwd(), directory: specifier }); + + return parseStaticDir(normalizedDir); +}; diff --git a/code/yarn.lock b/code/yarn.lock index 389084993441..66595ced56c3 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6631,6 +6631,7 @@ __metadata: react: "npm:^18.2.0" react-dom: "npm:^18.2.0" semver: "npm:^7.6.3" + sirv: "npm:^2.0.4" slash: "npm:^5.0.0" strip-ansi: "npm:^7.1.0" tinyglobby: "npm:^0.2.10"