/* Copyright 2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import type { StorybookConfig } from "@storybook/react-vite"; import fs from "node:fs"; import { nodePolyfills } from "vite-plugin-node-polyfills"; import { mergeConfig, normalizePath, type Plugin } from "vite"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const srcRoot = normalizePath(join(__dirname, "..", "src")); const sharedComponentsLayer = "shared-components"; // Get a list of available languages so the language selector can display them at runtime const languageFiles = fs.readdirSync(join(__dirname, "..", "src", "i18n", "strings")).map((f) => f.slice(0, -5)); const languages: Record = {}; for (const lang of languageFiles) { const normalizedLanguage = lang.toLowerCase().replace("_", "-"); const languageParts = normalizedLanguage.split("-"); if (languageParts.length === 2 && languageParts[0] === languageParts[1]) { languages[languageParts[0]] = `${lang}.json`; } else { languages[normalizedLanguage] = `${lang}.json`; } } /** * This function is used to resolve the absolute path of a package. * It is needed in projects that use Yarn PnP or are set up within a monorepo. */ function getAbsolutePath(value: string): any { return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`))); } function layerSharedComponentCssModules(): Plugin { return { name: "element-web-shared-components-storybook-css-layer", enforce: "pre", transform(code, id) { const cssPath = normalizePath(id.split("?")[0]); if (!cssPath.startsWith(srcRoot) || !cssPath.endsWith(".module.css")) { return; } return { code: `@layer ${sharedComponentsLayer} {\n${code}\n}\n`, map: null, }; }, }; } const config: StorybookConfig = { stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], addons: [ "@storybook/addon-docs", "@storybook/addon-designs", "@storybook/addon-a11y", "@storybook/addon-vitest", getAbsolutePath("storybook-addon-vis"), ], framework: "@storybook/react-vite", core: { disableTelemetry: true, }, typescript: { reactDocgen: "react-docgen-typescript", reactDocgenTypescriptOptions: { // The default exclude is ["**/**.stories.tsx"] which prevents // docgen from extracting snapshot field descriptions from wrapper // components defined in story files. exclude: [], }, }, async viteFinal(config) { return mergeConfig(config, { plugins: [ layerSharedComponentCssModules(), // Needed for counterpart to work nodePolyfills({ include: ["util"], globals: { global: false } }), { name: "language-middleware", configureServer(server) { server.middlewares.use((req, res, next) => { if (req.url === "/i18n/languages.json") { // Dynamically generate a languages.json file based on what files are available res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(languages)); } else if (req.url === "/usercontent/" || req.url === "/usercontent") { // Mock usercontent endpoint used by encrypted download iframes. res.end("This is where /usercontent/ is loaded."); } else if (req.url?.startsWith("/i18n/")) { // Serve the individual language files, which annoyingly can't be a simple // static dir because the directory structure in src doesn't match what // the app requests. const langFile = req.url.split("/").pop(); res.setHeader("Content-Type", "application/json"); fs.createReadStream(`src/i18n/strings/${langFile}`).pipe(res); } else { next(); } }); }, }, ], server: { allowedHosts: ["localhost", ".docker.internal"], }, }); }, refs: { "compound-web": { title: "Compound Web", url: "https://element-hq.github.io/compound-web/", }, }, env: (config) => ({ ...config, STORYBOOK_LANGUAGES: JSON.stringify(Object.keys(languages)), }), }; export default config;